/

Quickstart

Add verifiable randomness to an Anchor program in three copy-paste changes. Total integration weight: one dependency, one CPI call, one callback handler.

Before you start

You need an Anchor ≥ 0.30 project, a Solana devnet keypair with ~2 SOL, and somewhere to deploy. If you don't have those yet, run through the Anchor installation guide first.

The three things you change

DICE is not a framework. It is three small mutations to a normal Anchor program:

  • declare_id! — point Anchor at your program's pubkey so the runtime identity check passes.
  • Outcome formula — the one line inside your callback that maps 32 random bytes to whatever your game needs (a die face, a winner index, a boolean).
  • GameState struct — your own account shape (what a "round" means in your game).

Everything else — the commit-reveal round, node selection, hardware signatures, fee routing — is handled by the DICE program and the coordinator. You do not see it in your code.

1 · declare_id!

Add DICE as a dependency, then swap the placeholder program id for your own.

Cargo.toml
# One dependency. The `cpi` feature pulls in dice::cpi::request_randomness_auto.
# `no-entrypoint` prevents the entrypoint symbol collision inside your binary.
[dependencies]
anchor-lang = "0.30"
dice = { git = "https://github.com/hariFED/DICE", features = ["cpi", "no-entrypoint"] }

Why declare_id!?

Every Anchor program has declare_id!("…") at the top of its lib.rs. At runtime Anchor does an identity check — am I deployed at the address I was compiled for? If the deployed address doesn't match, every instruction fails with DeclaredProgramIdMismatch. This prevents anyone from redeploying your bytecode at a different address and passing it off as yours.

So when you copy the DICE quickstart, you replace the placeholder with your own program's pubkey — the public key of target/deploy/<your-program>-keypair.json. DICE's own program id stays inside the dice crate; you never touch it.

shell
# Generate your program's keypair once, at project init.
solana-keygen new -o target/deploy/my_game-keypair.json

# Print the pubkey to paste into lib.rs.
solana-keygen pubkey target/deploy/my_game-keypair.json
programs/my_game/src/lib.rs
// programs/my_game/src/lib.rs
use anchor_lang::prelude::*;

// Replace with YOUR program's pubkey — solana-keygen pubkey target/deploy/my_game-keypair.json
declare_id!("7xAbcYourProgramIdHereReplaceMePlease111111");

#[program]
pub mod my_game {
    use super::*;
    // handlers go here
}

2 · Request randomness

Inside whatever instruction starts a round (play, mint, draw — whatever you call it), add one CPI call to dice::cpi::request_randomness_auto. The hardware round begins the moment this instruction lands.

programs/my_game/src/lib.rs
use anchor_lang::prelude::*;

#[program]
pub mod my_game {
    use super::*;

    pub fn play(ctx: Context<Play>, bet: u8, wager: u64) -> Result<()> {
        // 1. Your game setup: store the bet, take the wager.
        let g = &mut ctx.accounts.game;
        g.player = ctx.accounts.player.key();
        g.bet = bet;
        g.wager = wager;
        g.settled = false;

        // 2. Ask DICE for randomness. Hardware round starts instantly.
        dice::cpi::request_randomness_auto(
            CpiContext::new(
                ctx.accounts.dice_program.to_account_info(),
                dice::cpi::accounts::RequestRandomnessAuto {
                    authority:      ctx.accounts.player.to_account_info(),
                    channel:        ctx.accounts.dice_channel.to_account_info(),
                    system_program: ctx.accounts.system_program.to_account_info(),
                },
            ),
            4, // node_count: minimum 4, maximum 50
        )?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Play<'info> {
    #[account(mut)]
    pub player: Signer<'info>,

    #[account(
        init,
        payer = player,
        space = 8 + GameState::INIT_SPACE,
        seeds = [b"game", player.key().as_ref()],
        bump,
    )]
    pub game: Account<'info, GameState>,

    /// CHECK: validated by the DICE program.
    #[account(mut)]
    pub dice_channel: UncheckedAccount<'info>,

    /// CHECK: the DICE program itself — Anchor verifies program id via CPI.
    pub dice_program: UncheckedAccount<'info>,

    pub system_program: Program<'info, System>,
}

#[account]
#[derive(InitSpace)]
pub struct GameState {
    pub player: Pubkey,
    pub bet: u8,
    pub wager: u64,
    pub outcome: u8,
    pub randomness: [u8; 32],
    pub settled: bool,
}

Pick your node count

node_count = 4 is the floor — fast and cheap. Use 8 or 16 for high-stakes rounds where you want more physical devices signing the result. Maximum is 50.

3 · Handle the callback

When the round finishes, DICE CPIs into your program with the name dice_callback and a single argument — a typed DiceResult struct. Define the struct, declare the accounts, write your outcome line. That's the full integration.

programs/my_game/src/lib.rs
use anchor_lang::prelude::*;

/// Typed payload DICE sends into your callback as instruction data.
/// Borsh-compatible with the positional `(channel_key, randomness)` form
/// but with better IDE autocomplete and room for future fields.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug)]
pub struct DiceResult {
    /// Which DiceChannel produced this randomness.
    pub channel_key: Pubkey,
    /// 32 bytes of hardware-backed entropy.
    pub randomness: [u8; 32],
}

#[derive(Accounts)]
pub struct DiceCallback<'info> {
    /// The game PDA this callback settles. Anchor binds it via seeds;
    /// the constraint blocks double-settlement at compile time.
    #[account(
        mut,
        seeds = [b"game", game.player.as_ref()],
        bump,
        constraint = !game.settled @ GameError::AlreadySettled,
    )]
    pub game: Account<'info, GameState>,

    /// CHECK: cross-checked in the handler against result.channel_key.
    pub dice_channel: UncheckedAccount<'info>,
}

pub fn dice_callback(ctx: Context<DiceCallback>, result: DiceResult) -> Result<()> {
    // Guard against spoofed callbacks from a different channel.
    require_keys_eq!(
        ctx.accounts.dice_channel.key(),
        result.channel_key,
        GameError::WrongChannel,
    );

    let g = &mut ctx.accounts.game;
    // Map the 32 random bytes to a 1..=6 die roll.
    g.outcome = (u32::from_le_bytes([
        result.randomness[0],
        result.randomness[1],
        result.randomness[2],
        result.randomness[3],
    ]) % 6) as u8 + 1;
    g.randomness = result.randomness;
    g.settled = true;
    Ok(())
}

#[error_code]
pub enum GameError {
    #[msg("Game already settled")]
    AlreadySettled,
    #[msg("Wrong channel for this callback")]
    WrongChannel,
}

Why a typed struct and not positional args?

Wire-format-identical. Borsh serialises a two-field struct the exact same way as (Pubkey, [u8; 32]) positionally. The struct form gives you cleaner autocomplete, future-proof fields, and forces you to acknowledge channel_key exists rather than prefixing it with _ and ignoring it.

Your outcome formula

The line g.outcome = (u32::from_le_bytes(…) % 6) as u8 + 1 is the only game-specific logic in the whole integration. Change it and you've shipped a different game. We collected the five formulas that cover ~90% of use-cases on the formulas page.

Next steps

  • Read Channel setup — the one-time PDA you create at deploy-time.
  • Skim the pricing breakdown so channel funding doesn't surprise you.
  • If you're in a regulated environment, switch from streaming to audit mode — +8% latency, fully reproducible device selection.