Verifiable Randomness Functions
Use proper cryptographic randomness in your onchain programs.
Summary
- Attempts at generating randomness within your program are likely to be guessable by users given there’s no true randomness onchain.
- Verifiable Random Functions (VRFs) give developers the opportunity to incorporate securely generated random numbers in their onchain programs.
- A VRF is a public-key pseudorandom function that provides proofs that its outputs were calculated correctly.
- Switchboard offers a developer-friendly VRF for the Nexis ecosystem.
Lesson
Randomness onchain
Random numbers are not natively allowed onchain. This is because Nexis Native is deterministic, every validator runs your code and needs to have the same result. So if you wanted to create a raffle program, you’d have to look outside of the blockchain for your randomness. This is where Verifiable Random Functions (VRFs) come in. VRFs offer developers a secure means of integrating randomness onchain in a decentralized fashion.
Types of Randomness
Before we dive into how random numbers can be generated for a blockchain, we must first understand how they are generated on traditional computer systems. There are really two types of random numbers: true random and pseudorandom. The difference between the two lies in how the numbers are generated.
Computers can acquire true random numbers by taking some type of physical measurement of the outside world as entropy. These measurements take advantage of natural phenomena, such as electronic noise, radioactive decay, or atmospheric noise, to generate random data. Because these processes are intrinsically unpredictable, the numbers they produce are genuinely random and not reproducible.
Pseudorandom numbers, on the other hand, are generated by algorithms that use a deterministic process to produce sequences of numbers that appear to be random. Pseudorandom number generators (PRNGs) start with an initial value called a seed and then use mathematical formulas to generate subsequent numbers in the sequence. Given the same seed, a PRNG will always produce the same sequence of numbers. It’s important to seed with something close to true entropy: an admin-provided “random” input, the last system log, some combination of your system’s clock time and other factors, etc.. Fun fact: older video games have been broken because speedrunners found out how their randomness was calculated. One game in particular used the number of steps you’ve taken in the game as a seed.
Unfortunately, neither type of randomness is natively available in Nexis Native programs, because these programs have to be deterministic. All validators need to come to the same conclusion. There is no way they’d all draw the same random number, and if they used a seed, it’d be prone to attacks. See the Nexis Native FAQs for more. So we’ll have to look outside of the blockchain for randomness with VRFs.
What is Verifiable Randomness?
A Verifiable Random Function (VRF) is a public-key pseudorandom function that provides proofs that its outputs were calculated correctly. This means we can use a cryptographic keypair to generate a random number with a proof, which can then be validated by anyone to ensure the value was calculated correctly without the possibility of leaking the producer’s secret key. Once validated, the random value is stored onchain in an account.
VRFs are a crucial component for achieving verifiable and unpredictable randomness on a blockchain, addressing some of the shortcomings of traditional PRNGs and the challenges with achieving true randomness in a decentralized system.
There are three key properties of a VRF:
- Deterministic - A VRF takes a secret key and a nonce as inputs and deterministically produces an output ( seeding ). The result is a seemingly random value. Given the same secret key and nonce, the VRF will always produce the same output. This property ensures that the random value can be reproduced and verified by anyone.
- Unpredictability - The output of a VRF appears indistinguishable from true randomness to anyone without access to the secret key. This property ensures that even though the VRF is deterministic, you cannot predict the result ahead of time without knowledge of the inputs.
- Verifiability - Anybody can verify the validity of the random value generated by a VRF using the corresponding secret key and nonce.
VRFs are not specific to Nexis Native and have been utilized on other blockchains to generate pseudorandom numbers. Fortunately switchboard offers their implementation of VRF to Nexis Native.
Switchboard VRF Implementation
Switchboard is a decentralized Oracle network that offers VRFs on Nexis Native. Oracles are services that provide external data to a blockchain, allowing them to interact with and respond to real-world events. The Switchboard network is made up of many different individual oracles run by third parties to provide external data and service requests onchain. To learn more about Switchboard’s Oracle network, please refer to our Oracle lesson.
Switchboard’s VRF allows users to request an oracle to produce a randomness output onchain. Once an oracle has been assigned the request, the proof of the VRF result must be verified onchain before it can be used. The VRF proof takes 276 instructions (~48 transactions) to fully verify onchain. Once the proof is verified, the Switchboard program will execute a onchain callback defined by the VRF Account during account creation. From there the program can consume the random data.
You might be wondering how they get paid. In Switchboard’s VRF implementation, you actually pay per request.
Requesting and Consuming VRF
Now that we know what a VRF is and how it fits into the Switchboard Oracle network, let’s take a closer look at how to actually request and consume randomness from a Nexis Native program. At a high level, the process for requesting and consuming randomness from Switchboard looks like this:
- Create a
programAuthority
PDA that will be used as the program authority and sign on behalf of the program. - Create a Switchboard VRF Account with the
programAuthority
as theauthority
and specify thecallback
function the VRF will return the data to. - Invoke the
request_randomness
instruction on the Switchboard program. The program will assign an oracle to our VRF request. - Oracle serves the request and responds to the Switchboard program with the proof calculated using its secret key.
- Oracle executes the 276 instructions to verify the VRF proof.
- Once VRF proof is verified, the Switchboard program will invoke the
callback
that was passed in as the callback in the initial request with the pseudorandom number returned from the Oracle. - Program consumes the random number and can execute business logic with it!
There are a lot of steps here, but don’t worry, we’ll be going through each step of the process in detail.
First there are a couple of accounts that we will have to create ourselves to
request randomness, specifically the authority
and vrf
accounts. The
authority
account is a PDA derived from our program that is requesting the
randomness. So the PDA we create will have our own seeds for our own needs. For
now, we’ll simply set them at VRFAUTH
.
Then, we need to initialize a vrf
account that is owned by the Switchboard
program and mark the PDA we just derived as its authority. The vrf
account has
the following data structure.
Some important fields on this account are authority
, oracle_queue
, and
callback
. The authority
should be a PDA of the program that has the ability
to request randomness on this vrf
account. That way, only that program can
provide the signature needed for the vrf request. The oracle_queue
field
allows you to specify which specific oracle queue you’d like to service the vrf
requests made with this account. If you aren’t familiar with oracle queues on
Switchboard, checkout the
Oracles lesson in the Connecting to Offchain Data course!
Lastly, the callback
field is where you define the callback instruction the
Switchboard program should invoke once the randomness result has be verified.
The callback
field is of type
[CallbackZC](https://github.com/switchboard-xyz/nexis-sdk/blob/9dc3df8a5abe261e23d46d14f9e80a7032bb346c/rust/switchboard-nexis/src/oracle_program/accounts/ecvrf.rs#L25)
.
This is how you define the Callback struct client side.
Now, you can create the vrf
account.
Now that we have all of our needed accounts we can finally call the
request_randomness
instruction on the Switchboard program. It’s important to
note you can invoke the request_randomness
in a client or within a program
with a cross program invocation (CPI). Let’s take a look at what accounts are
required for this request by checking out the Account struct definition in the
actual
Switchboard program.
That’s a lot of accounts, let’s walk through each one and give them some context.
authority
- PDA derived from our programvrf
- Account owned by the Switchboard program- Oracle Queue - Account owned by Switchboard program that contains metadata about the oracle queue to use for this request
- Queue Authority - Authority of the Oracle Queue chosen
- Data Buffer -
Account of the
OracleQueueBuffer
account holding a collection of Oracle pubkeys that have successfully hearbeated before the queuesoracleTimeout
configuration has elapsed. Stored in the Oracle Queue account. - Permission Account Data
- Escrow (Switchboard escrow account) - Token Account
- Switchboard program state account -
Of type
SbState
- Switchboard Program - Switchboard Program
- Payer Token Account - Will be used to pay for fees
- Payer Authority - Authority of the Payer Token Account
- Recent Blockhashes Program - Recent Blockhashes Nexis Native program
- Token Program - Nexis Native Token Program
That’s all the accounts needed for just the randomness request, now let’s see
what it looks like in a Nexis Native program via CPI. To do this, we make use of the
VrfRequestRandomness
data struct from the
SwitchboardV2 rust crate.
This struct has some built-in capabilities to make our lives easier here, most
notably the account structure is defined for us and we can easily call invoke
or invoke_signed
on the object.
Once the Switchboard program is invoked, it does some logic on its end and
assigns an oracle in the vrf
account’s defined oracle queue to serve the
randomness request. The assigned oracle then calculates a random value and sends
it back to the Switchboard program.
Once the result is verified, the Switchboard program then invokes the callback
instruction defined in the vrf
account. The callback instruction is where you
would have written your business logic using the random numbers. In the
following code we store the resulting randomness in our vrf_auth
PDA from our
first step.
Now you have randomness! Hooray! But there is one last thing we have not talked
about yet and that’s how the randomness is returned. Switchboard, gives you your
randomness calling
[get_result()](https://github.com/switchboard-xyz/nexis-sdk/blob/9dc3df8a5abe261e23d46d14f9e80a7032bb346c/rust/switchboard-nexis/src/oracle_program/accounts/vrf.rs#L122)
.
This method returns the current_round.result
field of the vrf
account
SwitchboardDecimal format, which is really just a buffer of 32 random
[u8](https://github.com/switchboard-xyz/nexis-sdk/blob/9dc3df8a5abe261e23d46d14f9e80a7032bb346c/rust/switchboard-nexis/src/oracle_program/accounts/ecvrf.rs#L65C26-L65C26)
unsigned-integers. You can use these unsigned-integers however you see fit in
your program, but a very common method is to treat each integer in the buffer as
its own random number. For example, if you need a dice roll (1-6) just take the
first byte of the array, module it with 6 and add one.
What you do with the random values from there is completely up to you!
That is the essence of requesting randomness with a Switchboard VRF. To recap the steps involved in a VRF request, review this diagram.
Lab
For this lesson’s lab, we will be picking up where we left off in the Oracle lesson. If you haven’t completed the Oracle lesson and demo, we strongly recommend you do as there are a lot of overlapping concepts and we’ll be starting from the Oracle lesson’s codebase.
If you don’t want to complete the Oracle lesson, the starter code for this lab is provided for you in the main branch of the lab Github repository.
The repo contains a “Michael Burry” escrow program. This is a program that allows a user to lock up some nexis funds in escrow that cannot be withdrawn until NZT has reached a predefined price in USD chosen by the user. We will be adding VRF functionality to this program to allow the user to “Get out of jail” by rolling doubles. Our demo today will allow the user to roll two virtual dice, if they roll doubles (the two dice match), the user can withdraw their funds from escrow regardless of the NZT price.
1. Program Setup
If you are cloning the repo from the previous lesson make sure to do the following:
git clone https://github.com/Unboxed-Software/michael-burry-escrow
cd michael-burry-escrow
anchor build
anchor keys list
- Take the resulting key and put it into
Anchor.toml
andprograms/burry-escrow/src/lib.rs
- Take the resulting key and put it into
nexis config get
- Take your Keypair Path and change the
wallet
field in yourAnchor.toml
- Take your Keypair Path and change the
yarn install
anchor test
When all tests pass we’re ready to begin. We will start by filling in some boilerplate stuff, then we’ll implement the functions.
2. Cargo.toml
First, since VRF uses SPL tokens for their fees we need to import anchor-spl
in our Cargo.toml
file.
3. Lib.rs
Next, let’s edit lib.rs
and add the additional functions we’ll be building
today. The functions are as follows:
init_vrf_client
- Creates the VRF authority PDA, which will sign for and consume the randomness.get_out_of_jail
- Requests the randomness from the VRF, effectively rolling the dice.consume_randomness
- The callback function for the VRF where we will check for the dice rolls.
Make sure you replace YOUR_KEY_HERE
with your own program key.
4. State.rs
Next, in state.rs
, add an out_of_jail
flag to EscrowState
. When we finally
roll two matching die, we’ll flip this flag. When the withdraw
function is
called we can transfer the funds without checking the price.
Then, create our second data account for this program: VrfClientState
. This
will hold the state of our dice rolls. It will have the following fields:
bump
- Stores the bump of the account for easy signing later.result_buffer
- This is where the VRF function will dump the raw randomness data.dice_type
- We will set this to 6 as in a 6-sided die.die_result_1
anddie_result_2
- The results of our dice roll.timestamp
- Keeps track of when our last roll was.vrf
- Public key of the VRF account; owned by the Switchboard program. We will create this before we callVrfClientState
’s initialization function.escrow
- Public key of our burry escrow account.
We’re also going to make the VrfClientState
context a zero_copy
struct. This
means that we will initialize it with load_init()
and pass it into accounts
with AccountLoader
. We do this because VRF functions are very account
intensive and we need to be mindful of the stack. If you’d like to learn more
about zero_copy
, take a look at our
Program Architecture lesson.
Lastly we are going to add the VRF_STATE_SEED
to PDA our VRF Client account.
Your state.rs
file should look like this:
5. Errors.rs
Next, let’s take a quick pit stop and add one last error
InvalidVrfAuthorityError
to errors.rs
. We’ll use this when the VRF authority
is incorrect.
6. Mod.rs
Now, let’s modify our mod.rs
file to include our new functions we’ll be
writing.
7. Deposit.rs and Withdraw.rs
Lastly, let’s update our deposit.rs
and withdraw.rs
files to reflect our
soon-to-be new powers.
First, let’s initialize our out_of_jail
flag to false
in deposit.rs
.
Next, let’s write our simple get-out-of-jail logic. Wrap our oracle price-checks
with an if
statement. If the out_of_jail
flag on the escrow_state
account
is false, then we check the price at which to unlock the NZT:
If out_of_jail
is true, then we get out of jail free and can skip the price
check, going straight to our withdrawal.
8. Using VRF
Now that we have the boilerplate out of the way, let’s move on to our first
addition: initializing our VRF Client. Let’s create a new file called
init_vrf_client.rs
in the /instructions
folder.
We’ll add the needed crates, then create the InitVrfClient
context. We’ll need
the following accounts:
user
- the signer who has funds in escrow.escrow_account
- the burry escrow account created when the user locked their funds up.vrf_client_state
- account we will be creating in this instruction to hold state about the user’s dice rolls.vrf
- Our VRF owned by the Switchboard program, we will create this account client-side before we callinit_vrf_client
.system_program
- The system program since we use the init macro forvrf_state
, which callscreate_account
under the hood.
Notice the vrf_state
account is a PDA derived with the VRF_STATE_SEED
string
and the user
, escrow_account
, and vrf
public keys as seeds. This means a
single user can only initialize a single vrf_state
account, just like they can
only have one escrow_account
. Since there is only one, If you wanted to be
thorough, you might want to implement a close_vrf_state
function to get your
rent back.
Now, let’s write some basic initialization logic for this function. First we
load and initialize our vrf_state
account by calling load_init()
. Then we
fill in the values for each field.
9. Get Out of Jail
Now that we have the VrfClientState
account initialized, we can use it in the
get_out_jail
instruction. Create a new file called get_out_of_jail.rs
in the
/instructions
folder.
The get_out_jail
instruction will make our VRF request to Switchboard. We’ll
need to pass in all of the accounts needed for both the VRF request and our
business logic callback function.
VRF Accounts:
payer_wallet
- the token wallet that will pay for the VRF request; theuser
must be the owner of this account.vrf
- The VRF account that was created by the client.oracle_queue
- The oracle queue that will field the randomness result.queue_authority
- The authority over the queue.data_buffer
- The queue’s data buffer account - used by the queue to compute/verify the randomness.permission
- Created when creating thevrf
account. It’s derived from several of the other accounts.switchboard_escrow
- Where the payer sends the tokens for requests.program_state
- State of the Switchboard program.
Programs:
switchboard_program
recent_blockhashes
token_program
system_program
Business Logic Accounts:
user
- The user account who has escrowed the funds.escrow_account
- The burry escrow state account for user.vrf_state
- The VRF client state account initialized in theinit_vrf_client
instruction.
Lastly, we’ll create a new struct RequestRandomnessParams
. We’ll be passing in
some account’s bumps client-side.
Now, we can work on the logic of this instruction. The logic should gather all
of the accounts needed and pass them to
[VrfRequestRandomness](https://github.com/switchboard-xyz/nexis-sdk/blob/fbef37e4a78cbd8b8b6346fcb96af1e20204b861/rust/switchboard-nexis/src/oracle_program/instructions/vrf_request_randomness.rs#L8)
,
which is a really nice struct from Switchboard. Then we’ll sign the request and
send it on it’s way.
10. Consume Randomness
Now that we’ve built the logic to request a VRF from Switchboard, we must build
the callback instruction the Switchboard program will call once the VRF has been
verified. Create a new file called consume_randomness.rs
in the
/instructions
directory.
This function will use the randomness to determine which dice have been rolled.
If doubles are rolled, set the out_of_jail
field on vrf_state
to true.
First, let’s create the ConsumeRandomness
context. Fortunately, it only takes
three accounts.
escrow_account
- state account for user’s escrowed funds.vrf_state
- state account to hold information about dice roll.vrf
- account with the random number that was just calculated by the Switchboard network.
Now let’s write the logic for our consume_randomness_handler
. We’ll first
fetch the results from the vrf
account.
We need to call load()
because the vrf
is passed in as an AccountLoader
.
Remember, AccountLoader
avoids both stack and heap overflows for large
accounts. Then, we call get_result()
to grab the randomness from inside the
VrfAccountData
struct. Finally, we’ll check if the resulting buffer is zeroed
out. If it’s all zeros, it means the Oracles have not yet verified and deposited
the randomness in the account.
Then we load our vrf_state
using load_mut
since we’ll be storing the
randomness and dice rolls within it. We also want to check that the
result_buffer
returned from the vrf
does not match byte for byte the
result_buffer
from the vrf_state
. If they do match, we know the returned
randomness is stale.
Now it’s time to actually use the random result. Since we only use two dice we
only need the first two bytes of the buffer. To convert these random values into
“dice rolls”, we use modular arithmetic. For anyone not familiar with modular
arithmetic,
Wikipedia can help. In
modular arithmetic, numbers “wrap around” upon reaching a given fixed quantity.
This given quantity is known as the modulus to leave as the remainder. Here, the
modulus is the dice_type
stored on the vrf_state
account. We hard-coded this
to 6 when the account was initialized to represent a 6-sided die. When we use
dice_type
, or 6, as the modulus, our result will be a number 0-5. We then add
one, to make the resulting possibilities 1-6.
Fun fact from Christian (one of the editors): one byte per roll is actually a slightly bad option for a dice roll. (Good enough to demo) You have 256 options in a u8. When modulo’d by 6, the number zero has a slight advantage in the distribution (256 is not divisible by 6). Number of 0s: (255-0)/6 + 1 = 43 Number of 1s: (256-1)/6 = 42.6, so 42 occurrences of 1 Number of 2s: (257-2)/6 = 42.5, so 42 occurrences of 2 Number of 3s: (258-3)/6 = 42.5, so 42 occurrences of 3 Number of 4s: (259-4)/6 = 42.5, so 42 occurrences of 4 Number of 5s: (260-5)/6 = 42.5, so 42 occurrences of 5
The very last thing we have to do is update the fields in vrf_state
and
determine is the user rolled doubles. If so, flip the out_of_jail
flag to
true.
If the out_of_jail
becomes true, the user can then call the withdraw
instruction and it will skip over the price check.
And that’s it for the get-out-of-jail functionality! Congrats, you have just
built a program that can consume Switchboard data feeds and submit VRF requests.
Please make sure your program builds successfully by running anchor build
.
11. Testing
Alright, let’s test our program. Historically, we’d need to test the VRF on Devnet. Fortunately, the folks at Switchboard have created some really nice functions to let us run our own VRF oracle locally. For this, we’ll need to set up our local server, grab all of the right accounts, and then call our program.
The first thing we’ll do is pull in some more accounts in our Anchor.toml
file:
Then we create a new test file called vrf-test.ts
and copy and paste the code
below. It copies over the last two tests from the oracle lesson, adds some
imports, and adds a new function called delay
.
Now, we are going to set up our local VRF Oracle server using
SwitchboardTestContext
. This will give us a switchboard
context and an
oracle
node. We call the initialization functions in the before()
function.
This will run and complete before any tests begin. Lastly, let’s add
oracle?.stop()
to the after()
function to clean everything up.
Now let’s run the actual test. We’ll structure the test to keep rolling dice until we get doubles, then we’ll check that we can withdraw the funds.
First, we’ll gather all of the accounts we need. The switchboard
test context
gives us most of these. Then we’ll need to call our initVrfClient
function.
Finally, we’ll roll our dice in a loop and check for doubles.
Note the function where we get our payerTokenWallet
. VRF actually requires the
requester to pay some wrapped NZT. This is part of the incentive mechanism of
the oracle network. Fortunately, with testing, Switchboard gives us this really
nice function to create and fund a test wallet.
And there you have it! You should be able to run and pass all of the tests using
anchor test
.
If something is not working, go back and find where you went wrong.
Alternatively feel free to try out the
solution code on the vrf
branch.
Remember to update your program keys and wallet path like we did in the
the Setup step.
Challenge
Now it’s time to work on something independently. Let’s add some Monopoly rules to our program. Add some logic to the program to track how many times a user rolls. If they roll 3 times without rolling doubles, they should be able to withdraw their funds, just like getting out of jail in Monopoly.
If you get stuck, we have the solution in the
vrf-challenge-solution
branch.