Generate on-chain randomness on Solana. 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.
For VRF Callback functionality guide please explore the callback folder.
This repository provides off-chain Rust and JS web3 SDKs for requesting on-chain randomness using ORAO VRF program.
Program account (devnet/mainnet): VRFzZoJdhFWL8rkvu87LpKM3RbcVezpMEc6X5GVDr7y
Since v0.4.0 the cost of a request is significantly reduced, but this imposes some caveats:
-
You should now use
RandomnessAccountData
to decode the randomness account:let request_address = randomness_account_address(&orao_vrf.id(), &self.seed); // This old version will fail on new accounts: // let randomness_account_data = orao_vrf // .account::<Randomness>(request_address) // .await?; // Now you should use this code to load the account let randomness_account_data = orao_vrf .account::<RandomnessAccountData>(request_address) .await?; // Respectively the following snippet will decode raw account data: // let request = RandomnessAccountData::try_deserialize(&mut raw_account_data)?;
CPI is an abbreviation for Cross Program Invocation on Solana – a way for one contract to call another contract within a single transaction. This section will illustrate this (full code is available on GitHub).
The contract we'll use to illustrate the CPI is a simple single-player Russian Roulette where the outcome of a round is derived from a fulfilled randomness.
Note: the randomness will not be immediately available for your contract, so you'll need to design it in a way that it'll wait for randomness being fulfilled. In our example a player won't be able to start another round until the current one is finished (until the randomness is fulfilled).
This examples is based on the Anchor Framework. Please consult the Anchor Book on how to create a contract.
Note: we use Anchor v0.29
To perform a CPI call you'll need to add the orao VRF rust SDK with the cpi
feature
into the list of your dependencies:
[dependencies]
# ...
orao-solana-vrf = { version = "0.4.0", default-features = false, features = ["cpi"] }
Each Solana instruction requires a proper list of accounts. We'll need to call the RequestV2 instruction so here is the list of required accounts:
- payer – VRF client
- network_state – VRF on-chain state address
- treasury - address of the VRF treasury (taken from the VRF on-chain state)
- request - PDA to store the randomness (derived from the seed)
- system_program – required to create the request account
Above means that our instruction needs all of these accounts besides it's own accounts. Particularly our Russian-Roulette instruction will require the following list of accounts:
#[derive(Accounts)]
#[instruction(force: [u8; 32])]
pub struct SpinAndPullTheTrigger<'info> {
/// Player will be the `payer` account in the CPI call.
#[account(mut)]
pub player: Signer<'info>,
/// This is the player state account, it is required by Russian-Roulette to store player data
// (number of rounds played and info to derive the last round outcome)
#[account(
init_if_needed,
payer = player,
space = 8 + PlayerState::SIZE,
seeds = [
PLAYER_STATE_ACCOUNT_SEED,
player.key().as_ref()
],
bump
)]
pub player_state: Account<'info, PlayerState>,
/// This account points to the last VRF request, it is necessary to validate that the player
/// is alive and is able to play another round.
/// CHECK:
#[account(
seeds = [RANDOMNESS_ACCOUNT_SEED, player_state.force.as_ref()],
bump,
seeds::program = orao_solana_vrf::ID
)]
pub prev_round: AccountInfo<'info>,
/// This account is the current VRF request account, it'll be the `request` account in the CPI call.
/// CHECK:
#[account(
mut,
seeds = [RANDOMNESS_ACCOUNT_SEED, &force],
bump,
seeds::program = orao_solana_vrf::ID
)]
pub random: AccountInfo<'info>,
/// VRF treasury account, it'll be the `treasury` account in the CPI call.
/// CHECK:
#[account(mut)]
pub treasury: AccountInfo<'info>,
#[account(
mut,
seeds = [CONFIG_ACCOUNT_SEED],
bump,
seeds::program = orao_solana_vrf::ID
)]
/// VRF on-chain state account, it'll be the `network_state` account in the CPI call.
pub config: Account<'info, NetworkState>,
/// VRF program address to invoke CPI
pub vrf: Program<'info, OraoVrf>,
/// System program address to create player_state and to be used in CPI call.
pub system_program: Program<'info, System>,
}
In the Anchor Framework there is a CpiContext
for this purpose (please consult
the corresponding section
of the Anchor Book):
let cpi_program = ctx.accounts.vrf.to_account_info();
let cpi_accounts = orao_solana_vrf::cpi::accounts::RequestV2 {
payer: ctx.accounts.player.to_account_info(),
network_state: ctx.accounts.config.to_account_info(),
treasury: ctx.accounts.treasury.to_account_info(),
request: ctx.accounts.random.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
};
let cpi_ctx = anchor_lang::context::CpiContext::new(cpi_program, cpi_accounts);
orao_solana_vrf::cpi::request_v2(cpi_ctx, force)?;
Our contract derives round outcome from the fulfilled randomness, round considered to be in-progress if randomness is not yet fulfilled:
/// Last round outcome.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CurrentState {
/// Player is alive and able to play.
Alive,
/// Player is dead and can't play anymore.
Dead,
/// Player is waiting for current round to finish.
Playing,
}
/// Derives last round outcome.
pub fn current_state(randomness: &RandomnessAccountData) -> CurrentState {
if let Some(randomness) = randomness.fulfilled_randomness() {
if is_dead(randomness) {
CurrentState::Dead
} else {
CurrentState::Alive
}
} else {
CurrentState::Playing
}
}
/// Decides whether player is dead or alive.
fn is_dead(randomness: &[u8; 64]) -> bool {
// use only first 8 bytes for simplicity
let value = randomness[0..size_of::<u64>()].try_into().unwrap();
u64::from_le_bytes(value) % 6 == 0
}
- Rust SDK (source code) is based on the
anchor-client
library, so you'll need to acquire theProgram
instance to use it:
// please choose the necessary commitment level
let commitment_config = CommitmentConfig::confirmed();
// get this from the solana configuration
let payer: Keypair = ..;
// we'll wrap payer into an Arc so it plays well with the tokio runtime
let payer = std::sync::Arc::new(payer);
// please choose the proper cluster
let client = Client::new_with_options(Cluster::Devnet, payer, commitment_config);
let program = client.program(orao_solana_vrf::id());
Check out the source code.
It's simple to integrate ORAO VRF into an on-chain game. We've built a Russian Roulette contract and CLI. New developers can reference it to get insight into doing Solana CPI - Cross Program Invocation.
Browse through js SDK and it's subdirectories for more info. Check out sample Typescript integration
Note that anchor test
will run it for the cpi tests.
Here is an example:
solana-test-validator -r \
--bpf-program VRFzZoJdhFWL8rkvu87LpKM3RbcVezpMEc6X5GVDr7y js/dist/orao_vrf.so \
--ledger /tmp/test-ledger