Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/magicblock-labs/magicblock-engine-examples/llms.txt

Use this file to discover all available pages before exploring further.

Roll Dice

The Roll Dice example demonstrates how to integrate MagicBlock’s Verifiable Random Function (VRF) to generate provably fair random numbers within Ephemeral Rollups. This is essential for games, lotteries, and any application requiring unpredictable, tamper-proof randomness.

What is VRF?

A Verifiable Random Function (VRF) generates random numbers that are:
  • Unpredictable: Cannot be predicted before generation
  • Verifiable: Can be proven to be random
  • Tamper-proof: Cannot be manipulated by validators or users
  • Deterministic: Given the same input, produces the same output (verifiable)
Never use simple on-chain methods like Clock::get()?.unix_timestamp for randomness - they are predictable and can be exploited!

Two Approaches

This example includes two implementations:
The standard approach where the player account remains on the base layer:
programs/roll-dice/src/lib.rs
use ephemeral_vrf_sdk::anchor::vrf;
use ephemeral_vrf_sdk::instructions::{create_request_randomness_ix, RequestRandomnessParams};

#[program]
pub mod random_dice {
    pub fn roll_dice(ctx: Context<DoRollDiceCtx>, client_seed: u8) -> Result<()> {
        msg!("Requesting randomness...");
        let ix = create_request_randomness_ix(RequestRandomnessParams {
            payer: ctx.accounts.payer.key(),
            oracle_queue: ctx.accounts.oracle_queue.key(),
            callback_program_id: ID,
            callback_discriminator: instruction::CallbackRollDice::DISCRIMINATOR.to_vec(),
            caller_seed: [client_seed; 32],
            accounts_metas: Some(vec![SerializableAccountMeta {
                pubkey: ctx.accounts.player.key(),
                is_signer: false,
                is_writable: true,
            }]),
            ..Default::default()
        });
        ctx.accounts
            .invoke_signed_vrf(&ctx.accounts.payer.to_account_info(), &ix)?;
        Ok()
    }
}

How It Works

1
Request Randomness
2
Your program requests random numbers from the VRF oracle:
3
let ix = create_request_randomness_ix(RequestRandomnessParams {
    payer: ctx.accounts.payer.key(),
    oracle_queue: ctx.accounts.oracle_queue.key(),
    callback_program_id: ID,
    callback_discriminator: instruction::CallbackRollDice::DISCRIMINATOR.to_vec(),
    caller_seed: [client_seed; 32],
    accounts_metas: Some(vec![SerializableAccountMeta {
        pubkey: ctx.accounts.player.key(),
        is_signer: false,
        is_writable: true,
    }]),
    ..Default::default()
});
ctx.accounts.invoke_signed_vrf(&ctx.accounts.payer.to_account_info(), &ix)?;
4
Mark Context with #[vrf]
5
Annotate your request context with the #[vrf] macro:
6
#[vrf]
#[derive(Accounts)]
pub struct DoRollDiceDelegatedCtx<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(seeds = [PLAYER_SEED, payer.key().to_bytes().as_slice()], bump)]
    pub player: Account<'info, Player>,
    /// CHECK: The oracle queue
    #[account(mut, address = ephemeral_vrf_sdk::consts::DEFAULT_EPHEMERAL_QUEUE)]
    pub oracle_queue: AccountInfo<'info>,
}
7
Implement the Callback
8
Define a callback function that receives the random number:
9
pub fn callback_roll_dice_simple(
    ctx: Context<CallbackRollDiceSimpleCtx>,
    randomness: [u8; 32],
) -> Result<()> {
    let player = &mut ctx.accounts.player;
    let rnd_u8 = ephemeral_vrf_sdk::rnd::random_u8_with_range(&randomness, 1, 6);
    msg!("Consuming random number: {:?}", rnd_u8);
    player.rollnum = player.rollnum.saturating_add(1);
    msg!("Roll number: {:?}", player.rollnum);
    player.last_result = rnd_u8;
    Ok()
}
10
Verify the VRF Signer
11
The callback context must verify it’s called by the VRF program:
12
#[derive(Accounts)]
pub struct CallbackRollDiceSimpleCtx<'info> {
    /// This check ensures that the vrf_program_identity (which is a PDA) is a signer
    /// enforcing the callback is executed by the VRF program through CPI
    #[account(address = ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY)]
    pub vrf_program_identity: Signer<'info>,
    #[account(mut)]
    pub player: Account<'info, Player>,
}

Oracle Queues

Different oracle queues for different environments:
use ephemeral_vrf_sdk::consts::DEFAULT_QUEUE;

#[account(mut, address = DEFAULT_QUEUE)]
pub oracle_queue: AccountInfo<'info>,

Player Account Structure

programs/roll-dice-delegated/src/lib.rs
#[account]
pub struct Player {
    pub last_result: u8,  // The result of the last dice roll (1-6)
    pub rollnum: u8,      // Number of times the player has rolled
}

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    msg!(
        "Initializing player account: {:?}",
        ctx.accounts.player.key()
    );
    let player = &mut ctx.accounts.player;
    player.last_result = 0;
    player.rollnum = 0;
    Ok()
}

Random Number Utilities

The VRF SDK provides helper functions for common use cases:
use ephemeral_vrf_sdk::rnd::random_u8_with_range;

// Roll a die (1-6)
let dice_result = random_u8_with_range(&randomness, 1, 6);

// Coin flip (0-1)
let coin_flip = random_u8_with_range(&randomness, 0, 1);

// Percentage (1-100)
let percentage = random_u8_with_range(&randomness, 1, 100);

Delegated vs Non-Delegated

Benefits:
  • Ultra-low latency rolls in the ER
  • Rapid successive rolls without base layer delays
  • Can batch multiple rolls before committing
  • Perfect for real-time gaming
Setup:
#[ephemeral]
#[program]
pub mod random_dice_delegated {
    // Use DEFAULT_EPHEMERAL_QUEUE
}

Delegating the Player Account

For the delegated approach, delegate the player account to the ER:
programs/roll-dice-delegated/src/lib.rs
pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> {
    ctx.accounts.delegate_player(
        &ctx.accounts.user,
        &[PLAYER_SEED, &ctx.accounts.user.key().to_bytes().as_slice()],
        DelegateConfig {
            validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
            ..Default::default()
        },
    )?;
    Ok()
}

#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(mut, del, seeds = [PLAYER_SEED, user.key().to_bytes().as_slice()], bump)]
    pub player: Account<'info, Player>,
}

Required Dependencies

Cargo.toml
[dependencies]
ephemeral-rollups-sdk = "0.1.0"
ephemeral-vrf-sdk = "0.1.0"
anchor-lang = "0.32.1"

What Makes This Advanced?

This example demonstrates several advanced concepts:
  1. VRF Integration: Secure, verifiable randomness generation
  2. Callback Pattern: Asynchronous request-response flow
  3. ER-Optimized VRF: Using the ephemeral oracle queue for low-latency randomness
  4. Delegation: Managing player accounts in ERs for instant rolls
  5. Seed Management: Using client seeds for additional entropy

Use Cases

  • Dice Games: Provably fair dice rolls (shown in this example)
  • Loot Drops: Random item generation in games
  • Lotteries: Fair winner selection
  • Card Shuffling: Randomized deck ordering
  • Procedural Generation: Random dungeon/world generation
  • NFT Traits: Random trait assignment at mint

Live Demo

Experience the dice rolling in action: The demo showcases the difference in latency between delegated and non-delegated approaches.

Frontend Integration

Here’s how to call the roll dice function from your frontend:
import { Program } from "@coral-xyz/anchor";

// Roll the dice
const clientSeed = Math.floor(Math.random() * 256); // Random seed
const tx = await program.methods
  .rollDiceDelegated(clientSeed)
  .accounts({
    payer: wallet.publicKey,
    player: playerPDA,
    oracleQueue: DEFAULT_EPHEMERAL_QUEUE,
  })
  .rpc();

// Wait a moment for the callback
await new Promise(resolve => setTimeout(resolve, 1000));

// Fetch the result
const playerAccount = await program.account.player.fetch(playerPDA);
console.log(`You rolled a ${playerAccount.lastResult}!`);

Security Considerations

Important:
  • Always use the VRF oracle for randomness in production
  • Never trust client-provided random numbers
  • Verify the VRF program identity in callbacks
  • The caller_seed adds entropy but doesn’t replace VRF security
  • Store critical game logic on-chain, not in the client

Next Steps

Build docs developers (and LLMs) love