/

Request randomness

One CPI call inside whatever instruction starts a round in your game. DICE takes over from there — commits, reveals, aggregation, callback.

The CPI call

dice::cpi::request_randomness_auto takes exactly three accounts — the caller (your player), the channel PDA, and the System program — plus one argument: how many devices to recruit for this round. Here's the full handler in context:

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

// Your own program's pubkey — see Quickstart for why.
declare_id!("7xAbcYourProgramIdHereReplaceMePlease111111");

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

    pub fn play(ctx: Context<Play>, bet: u8, wager: u64) -> Result<()> {
        // 1. Game setup — record the bet and 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. Fire the DICE CPI. The hardware round starts the moment this
        // TX confirms; DICE will call us back on the same channel when done.
        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 — see below
        )?;
        Ok(())
    }
}

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

    /// The round's game state. Your own PDA.
    #[account(
        init,
        payer = player,
        space = 8 + GameState::INIT_SPACE,
        seeds = [b"game", player.key().as_ref()],
        bump,
    )]
    pub game: Account<'info, GameState>,

    /// CHECK: the DICE channel PDA. DICE verifies it inside the CPI.
    #[account(mut)]
    pub dice_channel: UncheckedAccount<'info>,

    /// CHECK: DICE program id. Anchor's CPI invocation cross-checks it.
    pub dice_program: UncheckedAccount<'info>,

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

Atomic with your game logic

Because it's a CPI inside your instruction, the DICE request is atomic with the rest of your play() handler. If anything later in the TX reverts, the randomness request reverts too. You don't need a two-phase commit in your code.

The Accounts struct

Three accounts on the DICE side. Your struct adds its own — whatever your game needs (game PDA, player signer, etc.). Four details worth knowing:

  • authority is your player, not an admin. DICE uses it only as the signer that pre-authorises the channel debit.
  • channel is the PDA you created in channel setup. Passed as mut because the round fee is debited in place.
  • dice_program is the DICE program itself. Pass dice.programId from the client.
  • system_program is standard Solana System; Anchor requires it for the fee transfer.

Picking node_count

Minimum 4, maximum 50. The security property scales linearly — 1 of N honest nodes makes the output unbiasable, so higher N tolerates more Byzantine devices. The fee is flat (0.002 SOL) regardless of N; operators split it proportionally.

node_countRecommended forNotes
4Default · games · mintsFastest path. Tolerates 3 faulty.
8–16High-stakes roundsMore devices sign → more costly to collude against.
32–50Regulated / RWA drawsSlight latency bump; maximum assurance.

Failure modes

  • InsufficientNodes — fewer than 4 nodes were online at the moment of request. Retry with a smaller node_count or wait.
  • InvalidNodeCount— you passed < 4 or > 50.
  • EscrowInsufficient — channel is out of SOL. Fund it.

A full table of error variants lives on the errors reference.

Client-side invocation

Nothing DICE-specific for the caller — your player signs play() on your program and the CPI is internal. A minimal TS client:

client.ts
// Your game's play() call from a TS client.
await myGame.methods
  .play(3, new BN(100_000))       // bet = 3, wager = 0.0001 SOL
  .accounts({
    player: wallet.publicKey,
    game: gamePda,                 // derived from ["game", player]
    diceChannel: diceChannelPda,   // created in init-dice-channel.ts
    diceProgram: dice.programId,
    systemProgram: anchor.web3.SystemProgram.programId,
  })
  .rpc()

// That's it. ~4 seconds later your GameState will be settled.
// Poll or subscribe:
const sub = anchor.AccountClient.subscribe
  ? myGame.account.gameState.subscribe(gamePda)
  : null

Subscribe, don't poll

Because the callback lands in a separate TX (a few slots later), an account subscription on your GameState is the cleanest way to get an instant UI update when settled flips to true.

Testing without hardware

tests/harness/mock_firmware_node in the DICE repo simulates 10 devices running the real CBOR protocol and ECDSA signing. Point your devnet coordinator at it and your integration tests will exercise every code path without touching a physical ESP32.