Interacting with the Randomness Precompile¶
Introduction¶
Moonbeam utilizes verifiable random functions (VRF) to generate randomness that can be verified on-chain. A VRF is a cryptographic function that takes some input and produces random values, along with a proof of authenticity that these random values were generated by the submitter. The proof can be verified by anyone to ensure the random values generated were calculated correctly.
There are two available sources of randomness that provide random inputs based on block producers' VRF keys and past randomness results: local VRF and BABE epoch randomness. Local VRF is determined directly within Moonbeam using the collator of the block's VRF key and the last block's VRF output. On the other hand, BABE epoch randomness is based on all the VRF produced by the relay chain validators during a complete epoch.
For more information on the two sources of randomness, how the request and fulfillment process works, and security considerations, please refer to the Randomness on Moonbeam page.
Moonbeam provides a randomness precompile, which is a Solidity interface that enables smart contract developers to generate randomness via local VRF or BABE epoch randomness using the Ethereum API. Moonbeam also provides a randomness consumer Solidity contract that your contract must inherit from in order to consume fulfilled randomness requests.
This guide will show you how to use the randomness precompile and randomness consumer contract to create a lottery where the winners will randomly be selected. You'll also learn how to interact with the randomness precompile directly to perform actions such as purging an expired randomness request.
The randomness precompile is located at the following address:
0x0000000000000000000000000000000000000809
0x0000000000000000000000000000000000000809
0x0000000000000000000000000000000000000809
Note
There can be some unintended consequences when using the precompiled contracts on Moonbeam. Please refer to the Security Considerations page for more information.
The Randomness Solidity Interface¶
Randomness.sol is a Solidity interface that allows developers to interact with the precompile's methods.
The interface includes functions, constants, events, and enums, as covered in the following sections.
Functions¶
The interface includes the following functions:
- relayEpochIndex() — returns the current relay epoch index, where an epoch represents real time and not a block number
- requiredDeposit() — returns the deposit required to perform a randomness request
- getRequestStatus(uint256 requestId) — returns the request status of a given randomness request
- getRequest(uint256 requestId) — returns the request details of a given randomness request
- requestLocalVRFRandomWords(address refundAddress, uint256 fee, uint64 gasLimit, bytes32 salt, uint8 numWords, uint64 delay) — request random words generated from the parachain VRF
- requestRelayBabeEpochRandomWords(address refundAddress, uint256 fee, uint64 gasLimit, bytes32 salt, uint8 numWords) — request random words generated from the relay chain BABE consensus
- fulfillRequest(uint256 requestId) — fulfill the request which will call the consumer contract method
fulfillRandomWords
. Fees of the caller are refunded if the request is fulfillable - increaseRequestFee(uint256 requestId, uint256 feeIncrease) — increases the fee associated with a given randomness request. This is needed if the gas price increases significantly before the request is fulfilled
- purgeExpiredRequest(uint256 requestId) — removes a given expired request from storage and transfers the request fees to the caller and the deposit back to the original requester
Where the inputs that need to be provided can be defined as:
- requestId - the ID of the randomness request
- refundAddress - the address receiving the left-over fees after the fulfillment
- fee - the amount to set aside to pay for the fulfillment
- gasLimit - the gas limit to use for the fulfillment
- salt - a string that is mixed with the randomness seed to obtain different random words
- numWords - the number of random words requested, up to the maximum number of random words
- delay - the number of blocks that must pass before the request can be fulfilled. This value will need to be between the minimum and maximum number of blocks before a local VRF request can be fulfilled
- feeIncrease - the amount to increase fees by
Constants¶
The interface includes the following constants:
- maxRandomWords - the maximum number of random words being requested
- minBlockDelay - the minimum number of blocks before a request can be fulfilled for local VRF requests
- maxBlockDelay - the maximum number of blocks before a request can be fulfilled for local VRF requests
- deposit - the deposit amount needed to request random words. There is one deposit per request
Variable | Value |
---|---|
MAX_RANDOM_WORDS | 100 words |
MIN_VRF_BLOCKS_DELAY | 2 blocks |
MAX_VRF_BLOCKS_DELAY | 2000 blocks |
REQUEST_DEPOSIT_AMOUNT | 1 GLMR |
Variable | Value |
---|---|
MAX_RANDOM_WORDS | 100 words |
MIN_VRF_BLOCKS_DELAY | 2 blocks |
MAX_VRF_BLOCKS_DELAY | 2000 blocks |
REQUEST_DEPOSIT_AMOUNT | 1 MOVR |
Variable | Value |
---|---|
MAX_RANDOM_WORDS | 100 words |
MIN_VRF_BLOCKS_DELAY | 2 blocks |
MAX_VRF_BLOCKS_DELAY | 2000 blocks |
REQUEST_DEPOSIT_AMOUNT | 1 DEV |
Events¶
The interface includes the following events:
- FulfillmentSucceeded() - emitted when the request has been successfully executed
- FulfillmentFailed() - emitted when the request has failed to execute fulfillment
Enums¶
The interface includes the following enums:
- RequestStatus - the status of the request, which can be
DoesNotExist
(0),Pending
(1),Ready
(2), orExpired
(3) - RandomnessSource - the type of the randomness source, which can be
LocalVRF
(0) orRelayBabeEpoch
(1)
The Randomness Consumer Solidity Interface¶
The RandomnessConsumer.sol
Solidity interface makes it easy for smart contracts to interact with the randomness precompile. Using the randomness consumer ensures the fulfillment comes from the randomness precompile.
The consumer interface includes the following functions:
- fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) - handles the VRF response for given request. This method is triggered by a call to
rawFulfillRandomWords
- rawFulfillRandomWords(uint256 requestId, uint256[] memory randomWords) - executed when the
fulfillRequest
function of the randomness precompile is called. The origin of the call is validated, ensuring the randomness precompile is the origin, and then thefulfillRandomWords
method is called
Request & Fulfill Process¶
To consume randomness, you must have a contract that does the following:
- Imports the
Randomness.sol
precompile andRandomnessConsumer.sol
interface - Inherits from the
RandomnessConsumer.sol
interface - Requests randomness through the precompile's
requestLocalVRFRandomWords
method orrequestRelayBabeEpochRandomWords
method, depending on the source of randomness you want to use - Requests fulfillment through the precompile's
fulfillRequest
method - Consumes randomness through a
fulfillRandomWords
method with the same signature as thefulfillRandomWords
method of theRandomnessConsumer.sol
contract
When randomness is requested through the precompile's requestLocalVRFRandomWords
or requestRelayBabeEpochRandomWords
method, a fee is set aside to pay for the fulfillment of the request. When using local VRF, to increase unpredictability, a specified delay period (in blocks) must pass before the request can be fulfilled. At the very least, the delay period must be greater than one block. For BABE epoch randomness, you do not need to specify a delay but can fulfill the request at the beginning of the 2nd epoch following the current one.
After the delay, fulfillment of the request can be manually executed by anyone through the fulfillRequest
method using the fee that was initially set aside for the request.
When fulfilling the randomness request via the precompile's fulfillRequest
method, the rawFulfillRandomWords
function in the RandomnessConsumer.sol
contract will be called, which will verify that the sender is the randomness precompile. From there, fulfillRandomWords
is called and the requested number of random words are computed using the current block's randomness result and a given salt and returned. If the fulfillment was successful, the FulfillmentSucceeded
event will be emitted; otherwise the FulfillmentFailed
event will be emitted.
For fulfilled requests, the cost of execution will be refunded from the request fee to the caller of fulfillRequest
. Then any excess fees and the request deposit are transferred to the specified refund address.
Your contract's fulfillRandomWords
callback is responsible for handling the fulfillment. For example, in a lottery contract, the callback would use the random words to choose a winner and payout the winnings.
If a request expires it can be purged through the precompile's purgeExpiredRequest
function. When this function is called the request fee is paid out to the caller and the deposit will be returned to the original requester.
The happy path for a randomness request is shown in the following diagram:
Interact with the Solidity Interfaces via Lottery Contract¶
In the following sections of this tutorial, you'll learn how to interact with the randomness precompile, in addition to a lottery contract that requires you to have multiple accounts which you can use to participate in a lottery. The default lottery contract sets the minimum number of participants to three, however you can feel free to change the number in the contract.
Checking Prerequisites¶
Assuming you use the default contract, you will need to have the following:
- MetaMask installed and connected to Moonbase Alpha
- Create or have three accounts on Moonbase Alpha to test out the lottery contract
- All of the accounts will need to be funded with
DEV
tokens. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
Example Lottery Contract¶
In this tutorial, you'll interact with a lottery contract that uses the randomness precompile and consumer. You'll be generating random words which will be used to select the winner of the lottery fairly. You can find a copy of the lottery contract that will be used for this tutorial, RandomnessLotteryDemo.sol
, in the Moonbeam Docs GitHub repository.
The lottery contract imports the Randomness.sol
precompile and the RandomnessConsumer.sol
interface, and inherits from the consumer contract. In the constructor of the contract, you can specify the source of randomness to be either local VRF or BABE epoch randomness.
In general, the lottery contract includes functionality to check the status of the randomness request which will be used to determine whether the lottery is still accepting participants, if it has started, or if it has expired. It will use the requestLocalVRFRandomWords
or requestRelayBabeEpochRandomWords
function to select the random words, depending on which source of randomness you want to use. In addition, it will implement the fulfillRandomWords
method and the callback will fulfill the request and use the random words to randomly pick the lottery winners.
There are also some constants in the contract that can be edited as you see fit, especially the SALT_PREFIX
which can be used to produce unique results.
Remix Set Up¶
You can interact with the randomness precompile and consumer using Remix. To add the interfaces to Remix and follow along with the tutorial, you will need to:
- Get a copy of
RandomnessLotteryDemo.sol
- Paste the file contents into a Remix file named RandomnessLotteryDemo.sol
Compile & Deploy the Lottery Contract¶
Next, you will need to compile the RandomnessLotteryDemo.sol
file in Remix:
- Make sure that you have the RandomnessLotteryDemo.sol file open
- Click on the Compile tab, second from top
- To compile the contract, click on Compile RandomnessLotteryDemo.sol
If the contract was compiled successfully, you will see a green checkmark next to the Compile tab.
Once the contract has been compiled, you can deploy the contract by taking the following steps:
- Click on the Deploy and Run tab directly below the Compile tab in Remix
- Make sure Injected Provider - Metamask is selected in the ENVIRONMENT dropdown. Once selected, you might be prompted by MetaMask to connect your account to Remix
- Make sure the correct account is displayed under ACCOUNT
- You'll need to pay the required deposit for a randomness request. For this contract, the required deposit is 1 DEV. So set the value to
1000000000000000000
and choose Wei from the dropdown on the right - Ensure RandomnessLotteryDemo - RandomnessLotteryDemo.sol is selected in the CONTRACT dropdown
- Next to Deploy enter the source of randomness. This corresponds to the
RandomnessSource
enum. For local VRF, enter0
, and for BABE epoch randomness, enter1
. To follow along with this example, you can select0
and click Deploy - Confirm the MetaMask transaction that appears by clicking Confirm
The RANDOMNESSLOTTERYDEMO contract will appear in the list of Deployed Contracts.
Participate in the Lottery¶
The default contract has a minimum requirement of three participants. To participate you can take the following steps:
- Make sure you've switched to the account you want to participate with in MetaMask. You can verify the account that is connected under ACCOUNT
- Enter the amount you want to contribute to the lottery in the VALUE field. It must be greater than or equal to the
PARTICIPATION_FEE
which is set to100000 gwei
in the default contract - Click on participate
- Confirm the transaction in MetaMask
Since there is a minimum of three participants required to start the lottery, you'll need to repeat these steps until you've participated from three different accounts.
Start the Lottery¶
If you take a closer look at the RandomnessLotteryDemo.sol
contract's startLottery
function, you'll notice that it has the onlyOwner
modifier. As such, you will need to make sure that you switch back to the account that deployed the lottery contract before starting the lottery.
To start the lottery and submit the randomness request, which will call the precompile's requestLocalVRFRandomWords
, you can take the following steps:
- Confirm the account is the owner
- To start the lottery you need to pay a fee which will be used to fulfill the randomness request. You can set the VALUE to
200000
and choose Gwei. The excess fee will be returned to themsg.sender
- Click on startLottery
- Confirm the transaction in MetaMask
Once the transaction goes through, the lottery will start and no more participants will be able to join. Before you can fulfill the randomness request in order to pick the winners, you'll need to wait the delay. The default VRF_BLOCKS_DELAY
is set to 2
blocks.
Pick the Winners¶
To fulfill the request, you can do so using the fulfillRequest
function which will use the contract's requestId
variable to send an internal transaction and call the fulfillRequest
function of the randomness precompile. If successful, the request will be fulfilled and generate the random words and execute the fulfillRandomWords
function defined in the RandomnessLotteryDemo.sol
contract through another internal transaction. The fulfillRandomWords
function callback then calls pickWinners
and the jackpot is distributed through two more internal transactions, one for each of the randomly selected winners. In addition, the cost of execution will be refunded from the request fee to the caller of fulfillRequest
. Then any excess fees and the request deposit are transferred to the specified refund address.
You can initiate the fulfillment from any account after the delay has passed, to do so you'll need to:
- Ensure you're connected to the account that you want to fulfill the request from, it can be any account you choose
- Click on fulfillRequest
- MetaMask does not take into account internal transactions when estimating the gas limit for the transaction. As such, it is recommended to manually edit the gas limit in MetaMask to
200,000
- Confirm the transaction in MetaMask
If the transaction reverts with the following error you may need to call increaseRequestFee
:
{
"code": -32603,
"message": "VM Exception while processing transaction: revert",
"data": "0x476173206c696d69742061742063757272656e74207072696365206d757374206265206c657373207468616e206665657320616c6c6f74746564"
}
The data
field converted to ASCII text reads: Gas limit at current price must be less than fees allotted
. As such, you can use the increaseRequestFee
function to increase the fees for the transaction and try again.
Once the transaction goes through, you can take a look at the transaction details in the Remix console. If you scroll down til you see the logs, you should see four events and the event details. The events you should see are:
"event": "Ended"
- event sent when the lottery ends, which emits the number of participants, the jackpot, and the total winners. Defined in theRandomnessLotteryDemo.sol
contract"event": "Awarded"
- event sent when a winner is awarded, which should get emitted twice since there are two winners per the default contract. It emits the winner, the random word, and the amount won. Defined in theRandomnessLotteryDemo.sol
contract"event": "FulFillmentSucceeded"
- event sent when a request has been fulfilled successfully. Defined in theRandomness.sol
precompile
Congratulations! You've successfully used the randomness precompile and consumer to participate in and start a lottery, and use the generated random words to select a winner.
Interact with the Precompile Solidity Interface Directly¶
In addition to interacting with the randomness precompile via a smart contract, you can also interact with it directly in Remix to perform operations such as creating a randomness request, checking on the status of a request, and purging expired requests. Remember, you need to have a contract that inherits from the consumer contract in order to fulfill requests, as such if you fulfill a request using the precompile directly there will be no way to consume the results.
Remix Set Up¶
To add the interfaces to Remix and follow along with this section of the tutorial, you will need to:
- Get a copy of
Randomness.sol
- Paste the file contents into a Remix file named Randomness.sol
Compile & Access the Randomness Precompile¶
Next, you will need to compile the Randomness.sol
file in Remix. To get started, make sure you have the Randomness.sol file open and take the following steps:
- Click on the Compile tab, second from top
- To compile the contract, click on Compile Randomness.sol
If the contract was compiled successfully, you will see a green checkmark next to the Compile tab.
Instead of deploying the randomness precompile, you will access the interface given the address of the precompiled contract:
- Click on the Deploy and Run tab directly below the Compile tab in Remix. Please note the precompiled contract is already deployed
- Make sure Injected Provider - Metamask is selected in the ENVIRONMENT dropdown. Once selected, you might be prompted by MetaMask to connect your account to Remix
- Make sure the correct account is displayed under ACCOUNT
- Ensure Randomness - Randomness.sol is selected in the CONTRACT dropdown. Since this is a precompiled contract, there is no need to deploy any code. Instead we are going to provide the address of the precompile in the At Address Field
- Provide the address of the batch precompile:
0x0000000000000000000000000000000000000809
and click At Address
The RANDOMNESS precompile will appear in the list of Deployed Contracts. You will use this to fulfill the randomness request made from the lottery contract later on in this tutorial.
Get Request Status & Purge Expired Request¶
Anyone can purge expired requests. You do not need to be the one who requested the randomness to be able to purge it. When you purge an expired request, the request fees will be transferred to you, and the deposit for the request will be returned to the original requester.
To purge a request, first you have to make sure that the request has expired. To do so, you can verify the status of a request using the getRequestStatus
function of the precompile. The number that is returned from this call corresponds to the index of the value in the RequestStatus
enum. As a result, you'll want to verify the number returned is 3
for Expired
.
Once you've verified that the request is expired, you can call the purgeExpiredRequest
function to purge the request.
To verify and purge a request, you can take the following steps:
- Expand the RANDOMNESS contract
- Enter the request ID of the request you want to verify has expired and click on getRequestStatus
- The response will appear just underneath the function. Verify that you received a
3
- Expand the purgeExpiredRequest function and enter the request ID
- Click on transact
- MetaMask will pop-up and you can confirm the transaction
Once the transaction goes through, you can verify the request has been purged by calling the getRequestStatus function again with the same request ID. You should receive a status of 0
, or DoesNotExist
. You can also expect the amount of the request fees to be transferred to your account.