Skip to content

SDK for verifiable randomness function (VRF / RNG) on Solana

License

Notifications You must be signed in to change notification settings

orao-network/solana-vrf

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ORAO Solana VRF

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.


This repository provides off-chain Rust and JS web3 SDKs for requesting on-chain randomness using ORAO VRF program.

Program account (devnet/mainnet): VRFzZoJdhFWL8rkvu87LpKM3RbcVezpMEc6X5GVDr7y

Notes on v0.4.0 update

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)?;

Developer Integration Guide - CPI Example

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).

1. Create your contract

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"] }

2. Collect the necessary accounts

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>,
}

3. Perform a CPI call

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)?;

4. Use the fulfilled randomness

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
}
  1. Rust SDK (source code) is based on the anchor-client library, so you'll need to acquire the Program 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());

Rust SDK

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.

JS / TS SDK

Browse through js SDK and it's subdirectories for more info. Check out sample Typescript integration

How to run a test validator.

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