Use proper cryptographic randomness in your onchain programs.
programAuthority
PDA that will be used as the program authority
and sign on behalf of the program.programAuthority
as the
authority
and specify the callback
function the VRF will return the data
to.request_randomness
instruction on the Switchboard program. The
program will assign an oracle to our VRF request.callback
that was passed in as the callback in the initial request with the
pseudorandom number returned from the Oracle.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
.
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.
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)
.
vrf
account.
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.
authority
- PDA derived from our programvrf
-
Account owned by the Switchboard programOracleQueueBuffer
account holding a collection of Oracle
pubkeys that have successfully hearbeated before the queues oracleTimeout
configuration has elapsed. Stored in the Oracle Queue account.SbState
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.
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.
[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.
git clone https://github.com/Unboxed-Software/michael-burry-escrow
cd michael-burry-escrow
anchor build
anchor keys list
Anchor.toml
and
programs/burry-escrow/src/lib.rs
nexis config get
wallet
field in your
Anchor.toml
yarn install
anchor test
anchor-spl
in our Cargo.toml
file.
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.YOUR_KEY_HERE
with your own program key.
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.
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
and die_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 call VrfClientState
’s initialization function.escrow
- Public key of our burry escrow account.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.
VRF_STATE_SEED
to PDA our VRF Client account.
state.rs
file should look like this:
InvalidVrfAuthorityError
to errors.rs
. We’ll use this when the VRF authority
is incorrect.
mod.rs
file to include our new functions we’ll be
writing.
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
.
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:
out_of_jail
is true, then we get out of jail free and can skip the price
check, going straight to our withdrawal.
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 call init_vrf_client
.system_program
- The system program since we use the init macro for
vrf_state
, which calls create_account
under the hood.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.
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; the
user
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 the vrf
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.switchboard_program
recent_blockhashes
token_program
system_program
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 the
init_vrf_client
instruction.RequestRandomnessParams
. We’ll be passing in
some account’s bumps client-side.
[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.
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.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.
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.
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 5The 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.
anchor build
.
Anchor.toml
file:
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
.
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.
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.
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.
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.
vrf-challenge-solution
branch.