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).
GameStatestruct — 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.
# 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.
# 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
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.
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.
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.