Generate on-chain randomness on Solana and invoke a Callback on your contract. ORAO's Verifiable Random Function for Solana offers unbiased, fast and affordable randomness for your Solana programs. Create unique NFT characteristics, generate random levels for games and weapons, randomize airdrops and provide secure, verifiable lottery. Built using Anchor framework.
This repository provides Rust and JS web3 SDKs for ORAO Callback VRF program.
Program account (devnet/mainnet): VRFCBePmGTpZ234BhbzNNzmyg39Rgdd6VgdfhHwKypU
The Callback VRF program is intended for on-chain usage. Here are the steps you must follow to create a new VRF client (every step is considered in detail later in this guide):
- Create, deploy and initialize a client program for Solana
- Register your client program at the Callback VRF
- Fund the new Client PDA created in the previous step
- Request randomness
All the requests to the Callback VRF are made via CPI by a registered client, so the first step is to write a client program.
Here are the minimal requirements for a client program:
- Program must have at least one PDA (bellow called a State PDA) that is
going to be used as a request authority — this PDA will sign the CPI call
to the
Requestinstruction so that the request could be authorized by the Callback VRF. - Program may have an instruction that is going to invoke the
Request/RequestAltinstruction via CPI.
Client programs can have one or more instructions that can be used as callbacks.
Callback instructions have some requirements imposed on their list
of accounts (see the [Callback](#callback) section bellow).
As you can see the minimal client program could have just two instructions: first one to initialize the state PDA and the second one to perform the Request CPI. The complete example of a Callback VRF client program could be found in the rust/examples/cpi folder.
This step will create a new Client PDA on the Callback VRF. This PDA will be used for the following purposes:
- Its balance will be used to pay request fees
- It will be a signer of a callback CPI (for authentication purposes).
Please note that only the program owner (it's upgrade authority)
is able to call the Register instruction — this account becomes an owner of
the new client. The client ownership could later be transferred using the Transfer instruction
(see bellow).
Also note that you can register the same client program multiple times as long as unique State PDAs are used for every registration — every such registration will create it's own independent Client PDA.
Here is the data you need to provide to invoke the Register instruction:
- Address of your client program
- Address of your client program's
ProgramDataaccount - A State PDA address
- State PDAs seeds + bump (via
Registerinstruction parameters) - (Optionally) A Client-level callback (via
Registerinstruction parameters) (more info on callbacks and its kinds is in the Callback section)
In JS SDK there is a helper named RegisterBuilder for the Register instruction:
let vrf = new OraoCb(provider);
let builder = await new RegisterBuilder(
vrf,
clientProgram.programId,
clientStateAddress,
[...clientStateSeeds, Buffer.from([clientStateBump])]
)
// You can omit the callback completely so the
// new client won't have a client-level callback
.withCallback(/* see the Callback section bellow */)
.build();
console.log("Registered in", await builder.rpc());Rust SDK has its own RegisterBuilder helper for the Register instruction:
let vrf: anchor_client::Program<_> = /* ... */;
let state_seeds: Vec<Vec<u8>> = /* State PDA seeds and bump */;
let signature = RegisterBuilder::new(state_seeds)
// You can omit the callback completely so the
// new client won't have a client-level callback
.with_callback(/* see the Callback section bellow */)
.build(vrf, program_address, state_address).await?
.send().await?;
println!("Registered in {signature}");Address Lookup Tables are supported since v0.3.0
An arbitrary client program instruction can be used as a callback instruction as long as it expects the following list of accounts:
- (signer) Client PDA will sign the callback call
- (writable) State PDA will be writable within the callback
- (readonly) VRF's
NetworkStatePDA will be available for reading - (readonly) Fulfilled
RequestAccountPDA will be available for reading - (optional) ... zero or more additional accounts given upon registration
or upon the
Request/RequestAltCPI (see bellow)
From the features perspective there are two types of callbacks:
Callback— normal callbackCallbackAlt— ALT here stands for Address Lookup Tables. This type of callback is able to use Solana's feature that allows developers to create a collection of related addresses to efficiently load more addresses in a single transaction. This is only possible for a request-level callback (see bellow).
From the contract perspective there are two types of callbacks:
- Client-level callback — a callback (if any) that is given upon the client registration — it will be called for every fulfilled request of this client, but may be overridden by the Request-level callback.
- Request-level callback — a callback (if any) that is given upon the
RequestCPI — it will be called for the request it was given to. If there is a Client-level callback defined for this client, then it won't get called if Request-level callback is given.
To define a callback you must provide the following information:
-
Borsh-serialized instruction data.
In rust it is convenient to use the
Callback::from_instruction_dataorCallbackAlt::from_instruction_datahelpers:let cb = Callback::from_instruction_data(SomeInstr::new(/* some params */));
In Typescript you may use the
BorshInstructionCoderfor your IDL:let ixCoder = new anchor.BorshInstructionCoder(clientProgram.idl); let callback = { data: ixCoder.encode("some_instruction_name", { /* some params */ }), // ... };
-
(optional) A list of remaining accounts.
There are three types of remaining accounts:
- Arbitrary read-only account — it is possible to give an arbitrary
read-only account to a callback. Use the
RemainingAccount::readonlyconstructor to build one. - Arbitrary writable account — it is possible to give an arbitrary
writable account as long as it is authorized by the caller - i.e.
if it is given as writable to the corresponding
Request,RequestAltorRegisterinstruction. Use theRemainingAccount::arbitrary_writableconstructor. - Writable PDA — it is always possible to give a writable account as long
as it is a PDA of the client program - just provide the proper seeds so
that VRF is able to verify the address. Use the
RemainingAccount::writableconstructor.
The following helpers exists to add remaining accounts to the callback:
-
for
Callback— useCallback::with_remaining_accountandCallback::with_remaining_accountshelpers. Or just directly extend theCallback::remaining_accountsfield.Examples:
let instruction = MyCallbackInstruction { some_param: 13 }; Callback::from_instruction_data(&instruction) .with_remaining_account(RemainingAccount::writable( my_pda_address, vec![b"MY_PDA_SEED".to_vec(), vec![my_pda_bump]], ))
For Typescript — just populate the
remainingAccountsfield:let ixCoder = new anchor.BorshInstructionCoder(clientProgram.idl); let data = ixCoder.encode("some_instruction_name", { /* some params */ }); let remainingAccounts = [ // Arbitrary read-only account { pubkey: some_address, seeds: null }, // Arbitrary writable account — note the empty array given for `seeds` { pubkey: another_address, seeds: [] }, // Writable PDA { pubkey: my_pda, seeds: [Buffer.from(MY_PDA_SEED), Buffer.from([my_pda_bump])], }, ]; let callback = { data, remainingAccounts };
-
for
CallbackAlt— first create a vec ofRemainingAccounts and then use theCallbackAlt::compile_accountshelper.Examples:
let instruction = MyCallbackAltInstruction { foo: 42 }; let accounts = vec![ RemainingAccount::readonly(address1), RemainingAccount::arbitrary_writable(address2), ]; let callback = CallbackAlt::from_instruction_data(&instruction) .compile_accounts(accounts, &lookup_tables);
For Typescript — use the
compileAccountshelper:let ixCoder = new anchor.BorshInstructionCoder(clientProgram.idl); let data = ixCoder.encode("some_instruction_name", { /* some params */ }); let accounts = [ // Arbitrary read-only account { pubkey: someAddress, seeds: null }, // Arbitrary writable account — note the empty array given for `seeds` { pubkey: anotherAddress, seeds: [] }, // Writable PDA { pubkey: myPda, seeds: [Buffer.from(MY_PDA_SEED), Buffer.from([myPdaBump])], }, ]; let(remainingAccounts, accountsHash) = compileAccounts( accounts, lookupTables ); let callback = { accountsHash, data, remainingAccounts, };
- Arbitrary read-only account — it is possible to give an arbitrary
read-only account to a callback. Use the
Among other important characteristics the oracle’s design was built with an emphasis on ensuring that all client requests must be fulfilled in one way or another. In the context of arbitrary callbacks, this means that the oracle must be able to ignore potential errors and still fulfill the client’s request. The Solana platform does not allow catching CPI errors, so the oracle uses a configurable deadline (expressed in slots) during which the callback has a chance to execute successfully. Once this deadline is reached, the randomness request will be fulfilled, but the callback will be ignored.
For the client, this means their callback should never fail — for debugging purposes, the request explorer is available. Using this tool, you can view your callback logs even if they are not accessible in the Solana explorer, as well as identify other potential issues with your callback (e.g. exceeding the maximum transaction size).
It is possible for a callback to send a directive to the VRF contract
using the DirectiveGuard structure.
There is currently just a single directive:
-
ReimburseTo— Use this directive to override the default VRF behavior of reimbursing extra rent of the fulfilled request.By default VRF will reimburse extra rent back to the rent payer that is the client PDA. If this directive was set, then the extra rent will be reimbursed to the given
pubkey.
There is a SetCallback instruction that allows a client owner to update/remove
the client-level callback:
-
in typescript:
let signature = await cbProgram2.methods .setCallback({ newCallback: { /* see above on how to define a callback */ }, }) .accountsPartial({ client: clientAddress }) .rpc(); console.log("Callback updated in:", signature);
-
in rust:
let vrf = provider.program(orao_solana_vrf_cb::id())?; let signature = SetCallbackBuilder::new((/* see above on how to define a callback */)) .build(&vrf, client_addr).await? .send().await?; println!("Callback updated in {signature}");
As soon as Register instruction is successfully executed a new Client PDA
is allocated. Its address could be easily found using proper helper function:
-
in typescript:
import { clientAddress } from "@orao-network/solana-vrf-cb"; const [clientAddr, clientBump] = clientAddress( exampleClient.programId, clientStateAddr );
-
in rust:
use orao_solana_vrf_cb::state::Client; let (client_addr, client_bump) = Client::find_address( program_addr, state_addr, id(), );
Now to make a Request/RequestAlt CPI the Client PDA must be funded —
it's balance will be used to pay request fees and rent:
- effective VRF fees can be observed in the VRF's
NetworkStateaccount - pending VRF request requires more rent than the fulfilled VRF request,
so the extra rent will be reimbursed back to the Client PDA upon fulfill
(use the
ReimburseTocallback directive to override this behavior)
To fund the client you need to transfer some funds to the Client PDA. You can do it directly or via your program's instruction — in fact Solana imposes no limitations on how accounts can be funded:
// Here we'll fund our Client PDA using the SystemProgram's Transfer instruction.
let transfer = new web3.Transaction().add(
web3.SystemProgram.transfer({
fromPubkey: provider.publicKey,
toPubkey: clientAddr,
lamports: amountToTransfer,
})
);
await provider.sendAndConfirm(transfer);You can withdraw client funds using Callback VRF's Withdraw instruction.
This is a trivial operation but please note that you won't be able to withdraw past Client's funds necessary for rent exemption — there is a helper that returns the available client balance:
-
in typescript use
OraoCb.clientBalancemethod:let vrf = new OraoCb(anchorProvider); let availableBalance = await vrf.clientBalance(clientAddr);
-
in rust use
orao_solana_vrf_cb:sdk::client_balance:let vrf = provider.program(orao_solana_vrf_cb::id())?; let available_balance = client_balance(&vrf, client_addr).await?;
Here is an example of off-chain Withdraw invocation:
-
in typescript:
let vrf = new OraoCb(provider); let signature = vrf.methods .withdraw({ amount: amountInLamports }) .accountsPartial({ client: clientAddr }) .rpc(); console.log("Withdrawn in", signature);
-
in rust there is a
WithdrawBuilderhelper:let vrf = provider.program(orao_solana_vrf_cb::id())?; let signature = WithdrawBuilder::new(amount_in_lamports) .build(&vrf, client_addr).await? .send().await?; println!("Registered in {signature}");
Randomness requests are performed via CPI to the VRF's Request or RequestAlt instruction,
so it's a job for one of your program's instructions. Please note that you
can provide a request-level callback and a list of its remaining accounts —
you can decide that in the logic of your instruction (see the Callback section above).
Every Request/RequestAlt invocation has to provide a unique seed — this seed represents
a commitment necessary to verify the generated randomness. Note that every
registered client has a separate seed space — you don't need to worry on whether
some seed is already used by another client or not, but you still need to make
sure that your seed is not already used by your client - in case it's already used by your client
the Request instruction will error out.
This example shows the simplest possible way for a client program to invoke
the Request instruction - no request-level callback or complex logic will
be used:
/// This is the instruction of our program, that is going to perform `Request` CPI.
///
/// Our simple instruction has no logic in itself so in fact all of its accounts
/// and the `seed` parameter are here only to be proxied to the CPI.
///
#[derive(Accounts)]
#[instruction(seed: [u8; 32])]
pub struct InvokeRequest<'info> {
/// An account that pays transaction fees.
#[account(mut)]
pub payer: Signer<'info>,
// We're going to call VRF's instruction, so we need VRF's the program account.
pub vrf: Program<'info, OraoVrfCb>,
// All of the following accounts are required
// by the VRF's `Request` instruction.
/// State PDA account will authenticate the request.
///
/// The layout of this account might be arbitrary so
/// we omit the `ClientState` definition for brevity
#[account(mut)]
pub state: Account<'info, ClientState>,
/// Client PDA will pay fees and rent.
#[account(mut)]
pub client: Account<'info, Client>,
/// Network state holds the effective VRF configuration
#[account(mut)]
pub network_state: Account<'info, NetworkState>,
/// Treasury will receive fees (it's actual address is in the `NetworkState`)
/// CHECK: Asserted by the CPI
#[account(mut)]
pub treasury: AccountInfo<'info>,
/// The request account we're going to create
/// CHECK: Asserted by the CPI
#[account(mut)]
pub request: AccountInfo<'info>,
/// System program is necessary because `Request` will create an account.
pub system_program: Program<'info, System>,
}
// ..
/// And here is a handler for our `InvokeRequest` instruction
pub fn handler(ctx: Context<InvokeRequest>, seed: [u8; 32]) -> Result<()> {
use orao_solana_vrf_cb::{cpi, RequestParams};
// As stated above our simple instruction will just perform CPI
// without any other logic.
// First lets prepare instruction parameters and accounts.
let cpi_program = ctx.accounts.vrf.to_account_info();
let cpi_params = RequestParams::new(seed);
let mut cpi_accounts = cpi::accounts::Request {
payer: ctx.accounts.payer.to_account_info(),
state: ctx.accounts.state.to_account_info(),
client: ctx.accounts.client.to_account_info(),
network_state: ctx.accounts.network_state.to_account_info(),
treasury: ctx.accounts.treasury.to_account_info(),
request: ctx.accounts.request.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
};
// As said above our state account will authorize the `Request` CPI,
// so VRF expects it to sign the invocation
// (see https://solana.com/developers/guides/getstarted/how-to-cpi-with-signer)
cpi_accounts.state.is_signer = true;
let signers_seeds: &[&[&[u8]]] = &[&[
b"CLIENT_STATE",
&[ctx.accounts.state.bump],
]];
// Now just perform the CPI
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts)
.with_signer(signers_seeds);
cpi::request(cpi_ctx, cpi_params)?;
Ok(())
}This example is similar to the one shown above but it adds one additional parameter to opt-in into a request-level callback (for callbacks and its kinds see the Callback section above).
Additionally our request-level callback will illustrate how one can provide additional accounts to a callback — we're going to provide two accounts
- some read-only account — this illustrates basic functionality
- a writable account — this illustrates how to give a writable Program's PDA to a callback
/// This is the instruction of our program, that is going to perform `Request` CPI.
///
/// In addition to the seed it also takes a boolean that is going to indicate
/// that caller opts-in to a request-level callback.
///
#[derive(Accounts)]
#[instruction(seed: [u8; 32], with_cb: bool)]
pub struct InvokeRequest<'info> {
/// An account that pays transaction fees.
#[account(mut)]
pub payer: Signer<'info>,
// We're going to call VRF's instruction, so we need VRF's program account.
pub vrf: Program<'info, OraoVrfCb>,
// All of the following accounts are required
// by the VRF's `Request` instruction.
// Note that this instruction does not take any of additional accounts
// used by the request-level callback — this is because they are going
// to be provided by the oracle upon the callback invocation.
/// State PDA account will authenticate the request.
///
/// The layout of this account might be arbitrary so
/// we omit the `ClientState` definition for brevity
#[account(mut)]
pub state: Account<'info, ClientState>,
/// Client PDA will pay fees and rent.
#[account(mut)]
pub client: Account<'info, Client>,
/// Network state holds the effective VRF configuration
#[account(mut)]
pub network_state: Account<'info, NetworkState>,
/// Treasury will receive fees (it's actual address is in the `NetworkState`)
/// CHECK: Asserted by the CPI
#[account(mut)]
pub treasury: AccountInfo<'info>,
/// The request account we're going to create
/// CHECK: Asserted by the CPI
#[account(mut)]
pub request: AccountInfo<'info>,
/// System program is necessary because `Request` will create an account.
pub system_program: Program<'info, System>,
}
// ..
/// And here is a handler for our `InvokeRequest` instruction
pub fn handler(
ctx: Context<InvokeRequest>,
seed: [u8; 32],
with_cb: bool,
) -> Result<()> {
use orao_solana_vrf_cb::{
cpi, RequestParams, state::client::Callback, instruction
};
// As said above our simple instruction will just perform CPI
// without any other logic.
// First lets prepare instruction parameters and accounts.
let cpi_program = ctx.accounts.vrf.to_account_info();
let mut cpi_params = RequestParams::new(seed);
let mut cpi_accounts = cpi::accounts::Request {
payer: ctx.accounts.payer.to_account_info(),
state: ctx.accounts.state.to_account_info(),
client: ctx.accounts.client.to_account_info(),
network_state: ctx.accounts.network_state.to_account_info(),
treasury: ctx.accounts.treasury.to_account_info(),
request: ctx.accounts.request.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
};
// As said above our state account will authorize the `Request` CPI,
// so VRF expects it to sign the invocation
// (see https://solana.com/developers/guides/getstarted/how-to-cpi-with-signer)
cpi_accounts.state.is_signer = true;
let signers_seeds: &[&[&[u8]]] = &[&[
b"CLIENT_STATE",
&[ctx.accounts.state.bump],
]];
// Now let's check if caller opted-in to a request-level callback
let callback = with_cb.then(|| {
// Let's pretend that we have two accounts required by our callback
// * the first one is "Data" account holding some necessary information
// our callback needs to read — this is an arbitrary account
// * the second one is the "Statistic" account our callback needs
// to updated — this is our program's PDA
// The `Callback` instruction itself is defined bellow.
let data_address = SOME_KNOWN_ADDRESS;
let (stat_address, stat_bump) = Pubkey::find_program_address(
&[b"STATISTIC"],
&self::id(),
);
// Now let's define a callback giving our read-only and writable accounts
Callback::from_instruction_data(
&instruction::Callback { example_param: 42 },
)
// This gives the read-only account — it will go first
.with_remaining_account(RemainingAccount::readonly(data_address))
// This gives the writable account — it will go second
.with_remaining_account(RemainingAccount::writable(
stat_address,
vec![b"STATISTIC".to_vec(), vec![stat_bump]]
))
});
// And finally let's perform the CPI reflecting the callback that
// may be defined
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts)
.with_signer(signers_seeds);
cpi::request(cpi_ctx, cpi_params)?;
Ok(())
}
/// This is our callback instruction definition.
///
/// All the accounts here follows the callback accounts requirements
/// described in the "Callback" section of this guide except the last
/// two accounts that are the additional accounts we've added to the callback.
///
/// Note that it accepts a parameter named `example_param` and its value was
/// given by the request-level callback definition above and will be available
/// within the instruction handler.
///
/// The instruction handler definition is omitted for brevity.
#[derive(Accounts)]
#[instruction(example_param: u8)]
pub struct Callback<'info> {
/// The first account is always the Client PDA and must be a signer.
///
/// This must be thoroughly verified to avoid unauthorized calls.
#[account(
signer,
seeds = [
orao_solana_vrf_cb::CB_CLIENT_ACCOUNT_SEED,
crate::id().as_ref(),
client.state.as_ref(),
],
bump = client.bump,
seeds::program = orao_solana_vrf_cb::id(),
has_one = state,
)]
pub client: Account<'info, Client>,
/// The layout of this account might be arbitrary so
/// we omit the `ClientState` definition for brevity
#[account(
mut,
seeds = [b"CLIENT_STATE"],
bump = state.bump,
)]
pub state: Account<'info, ClientState>,
/// Effective VRF configuration is available for observation.
#[account(
seeds = [CB_CONFIG_ACCOUNT_SEED],
bump = network_state.bump,
seeds::program = orao_vrf_cb::id(),
)]
pub network_state: Account<'info, NetworkState>,
/// This request will alway be in the fulfilled state.
#[account(
seeds = [CB_REQUEST_ACCOUNT_SEED, client.key().as_ref(), request.seed()],
bump,
seeds::program = orao_vrf_cb::id(),
)]
pub request: Account<'info, RequestAccount>,
// Bellow follows our additional accounts
/// The first additional account is "Data" account.
///
/// We pretend that it is holding some necessary information
/// our callback needs to read (the definition is omitted for brevity).
pub data: Account<'info, Data>,
/// The second additional account is "Statistic" account.
///
/// We pretend that our callback will update this account (the definition
/// is omitted for brevity).
#[account(mut, seeds = [b"STATISTIC"], bump = statistic.bump)]
pub statistic: Account<'info, Statistic>,
}The effective owner of a client is stored in the owner field of a Client PDA.
The owner is able to:
- update client-level callback using
SetCallbackinstruction - withdraw funds from the Client PDA
- transfer ownership of the client
Transfer is a simple instruction requiring only the Client PDA being
transferred and a new owner address, but the signer must be the current
client owner:
-
in typescript:
let vrf = new OraoCb(provider); let signature = await vrf.methods .transfer({ newOwner: newOwnerAddress }) .accountsPartial({ client: clientAddress }) .rpc(); console.log("Transferred in:", signature);
-
in rust:
let vrf = provider.program(orao_solana_vrf_cb::id())?; let signature = TransferBuilder::new(new_owner_address) .build(&vrf, client_addr).await? .send().await?; println!("Transferred in {signature}");
Solana network imposes some limitations on the size and computation complexity of a transaction. In general there are four limits:
- Transaction size limit — maximum size of a serialized transaction can't exceed 1,232 bytes.
A callback could hit this limit if its
datafield is large or if it uses high number of accounts. If one of this statements are true for your callback, and your callback wasn't called by the VRF, then it might be the case that you hit this limit. Consider shorten the callback data or usingRequestAltwith a lookup table that holds all the fixed accounts used by your callback — this should reduce the tx size. - CU limit — VRF will set the maximum possible CU limit of 1.4 million CUs, but you should note that some part of this CU allocation is consumed by the VRF contract. It is less common for your callback to hit this limit, but it's better to always apply some basic optimization techniques.
- Heap size limit — the default Solana heap size is 32KB, but you should note that the default Solana allocator is Bump Allocator — this type of allocators are never actually free so every allocation/reallocation will reduce the remaining heap size. The general rule here is to always either borrow the data that is already available, or to always preallocate the necessary capacity for your containers to avoid reallocations.
- Maximum locked accounts limit — at the moment there is no way for an instruction to lock more than 64 unique accounts even though other limits allows you to define a callback with larger number of accounts. There is no way to increase this limit.