Do people want a RANDAO(n) Function?
I am looking to revive the discussion around the potential for a RANDAO(n) function, where n is the block.number
or slot
. This opcode would return the randao
value for a specified block/slot. (It could also be something like PREVRANDAO(n) to keep consistency).
I read through the EIP, articles, and this discussion here: EIP-4399: Supplant DIFFICULTY opcode with RANDOM. This lead me to think that many people wanted this extended functionality of prevrandao
to be able to input a specific block.
Is this feasible?
If so, I think it could serve a purpose for smaller protocols to finally have secure on-chain randomness without any oracle integration. Larger protocols may continue to want off chain oracles, because there does appear to be relatively cheap attacks (block reward opportunity cost) to refuse to publish a block, or get 2^n bits/attempts per proposer that a malicious actor owns.
Why is it needed
I’ve written an opinion piece here discussing the current failure of the prevrandao
use case, and a potential fix with RANDAO(n)
.
In short, block.prevrandao
has minimal use cases because the value it uses must be from a block that already exists. There is no way to “delay” the request for randomness to a later unknown time in the future when the randomness production should be revealed. The return value it uses must be from a block that already exists, and it’s randao
value is determined by the immediate previous block that the transaction was finalized in. Using block.prevrandao
can be taken advantage of in a simple manner. A malicious function can call the target protocol’s getRandomness
type of function, which uses block.prevrandao
as the powerhouse to generate it’s random number for users. This malicious function can revert if the generated random number is unfavorable, and only choose to execute when the number generated is favorable (see medium post).
In contrast, if we had RANDAO(n)
, this appears to be a step forward in achieving more secure on chain randomness. I’ve built a coin flip game to demonstrate the use case for RANDAO(n)
.
// This doesn't compile because randao(n) doesn't exist.
// DO NOT USE IN PRODUCTION
mapping(address => uint256) public usersBlockNumber;
error FailedEthTransfer();
error IncorrectPayment();
error AlreadyHasBlockNumber();
error NotMatured();
function setUsersBlockNumber() public payable {
// User must not have a block number.
if (usersBlockNumber[msg.sender] != 0) revert AlreadyHasBlockNumber();
// User must pay in advance (0.01 ETH to play).
if (msg.value != 1e16) revert IncorrectPayment();
// Let the users block number be 4 epochs after this call.
usersBlockNumber[msg.sender] = block.number + 128;
}
function generateAndUseRandomness() public payable {
// Get user's block number.
uint256 usersBlock = usersBlockNumber[msg.sender];
// Must have a matured and valid block number associated with user.
if (usersBlock < block.number) revert NotMatured();
// Generate random number from the block
randomNumber = (block.randao(usersBlock) % 100) + 1;
// Give user 0.02 ETH if won coinflip
// [51, 100] inclusive = 50 numbers = 50%.
if (randomNumber > 50) {
(bool success,) = msg.sender.call{value: 2e16}("");
if (!success) revert FailedEthTransfer();
}
// Always set users block back to 0 after using randomness.
delete usersBlockNumber[msg.sender];
}
Notice now the user has no way of manipulating the outcome after they have requested that a block be associated with them. The user cannot create a function that reverts when the randomness is unfavorable, because the randomness is not known at time of request, and can never be, because the request always references a block far into the future. At time of execution, the users block associated with them is always X distance away.
We have the same security techniques as requesting a random number from Chainlink VRF, disallowing users to re-request randomness, and freezing any function calls for the user while they have a number associated with them but not yet used. Also, the depth of the request should be at least the maximum re-org depth I would guess. I’m unsure if a full 4 epochs of waiting time would be necessary for “lotteries” with less than the block reward value.
If RANDAO
were to use the slot instead of block, and the slot was empty, as mentioned in the original EIP-4399 discussion, it would have to look back further in the past to find a non-empty slot, or have some other technique. For this reason using the block number may be superior, even if the timing is not perfectly consistent, always having a valid number feels better?
I am interested by what others have to say about the feasibility and impact or lack thereof for this new opcode that would act as an extension of PREVRANDAO
.