Skip to content

Quickstart: Anchor programs

One attribute injects the account plumbing; one method call sends the request; one helper reads the outcome back. Works on Anchor 0.31, 0.32 and 1.x from the same crates.

Pick your crates

toml
[dependencies]
vrand-anchor = { path = "…/vrand/crates/vrand-anchor" }   # the attribute
vrand-cpi    = { path = "…/vrand/crates/vrand-cpi" }      # the CPI + readers

vrand-cpi is keyed on the Solana crate major, not the Anchor version: module v2 for Anchor 0.31/0.32 (solana-* 2.x), v3 for Anchor 1.x (solana-* 3.x). vrand-anchor is a pure token-emitting proc-macro with no Anchor dependency of its own, which is why one crate serves every generation.

Request

rust
use anchor_lang::prelude::*;
use vrand_anchor::vrand_request_accounts;

#[vrand_request_accounts(payer = participant, requester = game, prover = config.prover)]
#[derive(Accounts)]
#[instruction(seed: [u8; 32])]
pub struct Flip<'info> {
    #[account(mut)]
    pub participant: Signer<'info>,
    #[account(seeds = [GAME_SEED], bump = game.bump)]
    pub game: Account<'info, Game>,
    pub config: Account<'info, Config>,
}

pub fn flip(ctx: Context<Flip>, seed: [u8; 32]) -> Result<()> {
    let bump = ctx.accounts.game.bump;
    let at_risk = ctx.accounts.config.at_risk_lamports;
    ctx.accounts
        .request_vrand(seed, at_risk, &[&[GAME_SEED, &[bump]]])?;
    Ok(())
}

The attribute injects vrand_request, vrand_program, vrand_network, vrand_prover, vrand_terms, vrand_ack, slot_hashes (and system_program if you lack one) and generates request_vrand().

Attribute arguments:

ArgRequiredMeaning
payeryesyour Signer paying rent + fee; its wallet must hold a current Terms acknowledgment (your frontend sends that once — the web SDK does it automatically)
requesteryesthe identity the request address derives from — your program's PDA, signing via seeds
proveryesexpression the prover account is pinned to (address = constraint)
comp_destno (default requester)receives slash compensation — an account your program controls
seedno (default seed)name of the [u8; 32] instruction argument
cpino (default vrand_cpi::v3)use vrand_cpi::v2 on Anchor 0.31/0.32

The prover pin is mandatory on purpose

The prover account arrives from the transaction. Unpinned, a caller can pass a prover whose VRF key they hold — and supply their own luck. Pin it to your own config. If you genuinely accept any prover, write prover = vrand_prover.key() so the choice is visible in your code.

Read the outcome

rust
use vrand_cpi::v3 as vrand;          // v2 on Anchor 0.31/0.32
use vrand_vrf_core::scale;

pub fn settle(ctx: Context<Settle>) -> Result<()> {
    // Owner check + typed pending/slashed errors built in:
    let beta = vrand::fulfilled_beta(&ctx.accounts.vrand_request)?;
    let roll = scale::uniform_below(&beta, 6);   // bias-free — never `% n`
    // … your logic …
    Ok(())
}

fulfilled_beta fails typed: ERR_REQUEST_PENDING while there is no outcome yet, ERR_REQUEST_SLASHED when there never will be one (your comp_dest was already paid — resolve through your backstop), ERR_WRONG_OWNER for an account VRAND does not own.

The three sharp edges

Every integration that has gone wrong has hit one of these:

  1. Size at_risk_lamports at or above the maximum output this randomness can trigger. It is your compensation ceiling; nothing warns you if you under-buy. (And never let the caller choose it — every request reserves that amount against the prover's bond, so caller-chosen coverage is a capacity-exhaustion attack on everyone sharing the prover.)

  2. Never resolve a timeout for less than the full output. If your timeout path is cheaper than the win path, anyone who can see the outcome early holds a free option: force the cheap path exactly when the outcome favored the participant. The full default output, funded by the slash, is the only non-exploitable resolution.

  3. Key your state on outcome data, never on the request address. A closed request's (requester, seed) re-derives the same address and can be created afresh with an independent outcome. Key on beta / fulfilled_slot.

Without the macro

Prefer the accounts spelled out? Copy the documented block from the vrand-cpi crate docs and call request_randomness directly — same wire format, same checks. The macro is convenience, not magic: everything it emits is ordinary Anchor attribute syntax you could have written.

Testing

The repo's examples/coinflip is the smallest complete consumer — request, resolve, and the timeout path (a consumer with only a happy path has a game that can hang). Compile-proof fixtures under tools/anchor-compat build a real consumer on Anchor 0.32.1 and 1.1.2 on every check.

Apache-2.0. Live on Solana devnet.