Skip to content

Rust SDK

The Rust SDK is alea-sdk on crates.io. It exposes the on-chain types your CPI needs plus two helper functions. This page is guide first, full reference second.

Terminal window
cargo add alea-sdk

Current version: 0.1.0. Apache 2.0. Rust edition 2021. no_std-compatible.

The crate is a pure library. It has no #[program] of its own — your Anchor program imports it to build a CPI into Alea’s deployed verifier.

Copy the two Alea account fields into your consumer’s Accounts struct. There’s no derive-macro shortcut — you write these two fields explicitly.

use anchor_lang::prelude::*;
use alea_sdk::{self, AleaVerifier};
#[derive(Accounts)]
pub struct SettleMatch<'info> {
// ... your program's accounts ...
#[account(mut)]
pub match_state: Account<'info, MatchState>,
// The Alea program itself (executable account).
pub alea_program: Program<'info, AleaVerifier>,
// Alea's Config PDA. seeds::program is REQUIRED — without it, an
// attacker can substitute a Config account owned by a different program.
#[account(
seeds = [b"config"],
bump = alea_config.bump,
seeds::program = alea_program.key(),
)]
pub alea_config: Account<'info, alea_sdk::Config>,
#[account(mut)]
pub payer: Signer<'info>,
pub clock: Sysvar<'info, Clock>,
}

The seeds::program = alea_program.key() attribute is the security-critical bit. Without it, Anchor will not verify that the alea_config account was derived under Alea’s program ID. An attacker can substitute a Config-shaped account owned by a program they control, feed attacker-chosen public keys into the pairing, and bypass verification entirely. See Security model for the full fake-config scenario.

bump = alea_config.bump reuses Alea’s stored canonical bump instead of re-deriving via find_program_address, saving roughly 10K CU per call. Either form compiles; the stored-bump form is what the on-chain verify accounts struct uses internally.

const MAX_BEACON_AGE_SECONDS: u64 = 30;
pub fn settle_match(ctx: Context<SettleMatch>, round: u64, signature: [u8; 64]) -> Result<()> {
// Reject stale beacons before the CPI — Alea itself won't.
require!(
alea_sdk::is_round_recent(
round,
&ctx.accounts.alea_config,
&ctx.accounts.clock,
MAX_BEACON_AGE_SECONDS,
),
MyError::StaleBeacon,
);
// One CPI call. Returns 32 bytes of verified drand randomness.
let randomness: [u8; 32] = alea_sdk::cpi::verify(
ctx.accounts.alea_program.to_account_info(),
ctx.accounts.alea_config.to_account_info(),
ctx.accounts.payer.to_account_info(),
round,
signature,
)?;
// Read IMMEDIATELY into a local before any subsequent CPI. Solana's
// return data is single-slot: any later CPI overwrites it.
let winner_index = u64::from_le_bytes(randomness[0..8].try_into().unwrap())
% ctx.accounts.match_state.players.len() as u64;
ctx.accounts.match_state.winner =
ctx.accounts.match_state.players[winner_index as usize];
Ok(())
}
#[error_code]
pub enum MyError {
#[msg("drand round is older than 30 seconds")]
StaleBeacon,
}

That’s it for the integration. Set a compute budget of 900,000 when the caller builds the transaction (Alea’s pairing is ~407K CU and the default Solana limit is 200K).

use solana_sdk::compute_budget::ComputeBudgetInstruction;
let cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(900_000);
let settle_ix = /* your settle instruction */;
let tx = Transaction::new_signed_with_payer(
&[cu_ix, settle_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);

The TypeScript SDK injects this for you. The Rust SDK is on-chain only — off-chain transaction building is on the caller.

Solana’s set_return_data / get_return_data use a single transaction-wide slot. Every CPI call that sets return data overwrites the previous value. Alea sets return data; so does any token transfer, any nested CPI, any SPL call.

// CORRECT — capture first, then downstream CPIs are safe
let randomness = alea_sdk::cpi::verify(/* args */)?;
token::transfer(transfer_ctx, amount)?;
// randomness is still valid
// WRONG — the transfer overwrites Alea's return data
token::transfer(transfer_ctx, amount)?;
let randomness = alea_sdk::cpi::verify(/* args */)?;
// Treat return-data ordering as single-slot and unsafe across SDK versions.

Read the [u8; 32] into a local before any other CPI. Always.

NameTypeDescription
PROGRAM_IDPubkeyCanonical Alea program ID (vanity address, same on devnet + mainnet). Re-exports alea_verifier::ID, which is guaranteed to match the deployed program at compile time.
pub fn verify<'info>(
alea_program: AccountInfo<'info>,
config: AccountInfo<'info>,
payer: AccountInfo<'info>,
round: u64,
signature: [u8; 64],
) -> Result<[u8; 32]>

Invokes Alea’s verify instruction via CPI. Returns the 32-byte randomness (sha256(signature_bytes)) on success.

  • alea_program — the Alea program account
  • config — the Alea Config PDA (must be derived with seeds::program = alea_program.key() in the consumer’s Accounts struct)
  • payer — signer, passed through to Alea’s Verify accounts
  • round — drand round number
  • signature — 64-byte G1 elliptic-curve point in uncompressed (x || y) big-endian encoding (what drand publishes)

Reverts with an AleaError if the signature fails pairing, the config is malformed, or the round is zero. See Errors below.

pub fn config_pda(program_id: &Pubkey) -> (Pubkey, u8)

Derives the Alea Config PDA from seeds [b"config"] for a given program ID. Returns (pda, bump). Useful for off-chain tooling that needs the PDA address without hard-coding it.

On-chain consumers should not re-derive — use Anchor’s bump = config.bump constraint instead, which reads the canonical bump from Config directly and saves ~10K CU per call.

pub fn is_round_recent(
round: u64,
config: &Config,
clock: &Clock,
max_age_seconds: u64,
) -> bool

Returns true if the drand round’s emission timestamp is within max_age_seconds of the current on-chain clock. Uses config.genesis_time + config.period to compute the expected round timestamp.

All arithmetic uses saturating_sub / saturating_mul. A malformed round == u64::MAX saturates to “stale” rather than wrapping — safe-by-default rejection.

Why this exists: Alea’s verify instruction accepts any round, including years-old ones. Consumer programs with stakes-on-outcomes (games, lotteries, prediction markets) MUST enforce recency before trusting the randomness, or an attacker replays an old beacon whose value they already know.

Suggested max_age_seconds:

  • 30 — sensible default for most consumers
  • 3 — one drand round, for adversarial MEV-sensitive contexts
  • 300 — relaxed, for slow settle flows where beacon age matters less than liveness
#[derive(Accounts)]
pub struct AleaVerify<'info> {
pub alea_program: Program<'info, AleaVerifier>,
#[account(
seeds = [b"config"],
bump = config.bump,
seeds::program = alea_program.key(),
)]
pub config: Account<'info, Config>,
}

Reference definition of the two Alea fields, exported so you can inspect the expected constraints. In practice you copy the two fields directly into your consumer’s Accounts struct (as shown in the integration guide above) rather than embed AleaVerify as a fragment — Anchor doesn’t support Accounts composition cleanly, so the copy-paste path is what works. The seeds::program constraint is the security-critical part; don’t omit it.

pub struct Config {
pub pubkey_g2: [u8; 128], // drand evmnet group public key, uncompressed G2
pub genesis_time: u64, // drand chain genesis, Unix seconds
pub period: u64, // beacon cadence, seconds (3 for evmnet)
pub chain_hash: [u8; 32], // drand evmnet chain hash
pub authority: Pubkey, // admin key that can call update_config
pub bump: u8, // canonical PDA bump
}

Alea’s on-chain configuration account, re-exported from alea_verifier::state::Config. Declaration order matches Borsh serialization order and is pinned by the roundtrip test at state.rs lines 55–95 — the schema is part of the v1 CPI interface and is frozen per ADR 0028.

Consumer programs reference it read-only. Writable only by Alea’s own initialize and update_config handlers, both of which enforce byte-equality against EXPECTED_EVMNET_* constants before touching any field.

pub use alea_verifier::program::AleaVerifier;

The Alea program type, for Anchor’s Program<'info, AleaVerifier> account binding.

alea_sdk::AleaError is re-exported from alea_verifier::errors::AleaError. The variants your CPI can actually see:

CodeVariantWhen
6000InvalidSignaturePairing returned Some(false) — signature invalid for this round.
6001InvalidG1PointSignature bytes fail the on-curve check. Wrong encoding or x ≥ field prime.
6002RoundZeroRound is 0. Drand rounds start at 1.
6004NoSquareRootSVDW: all three candidates fail sqrt. Not reachable under honest drand input. File an issue if seen.
6006PairingErroralt_bn128_pairing syscall returned Err or wrong-length output.

Other variants exist (6003 InvalidFieldElement, 6005 InvalidG2Point, 6009 ReturnDataMissing) but are unreachable in the current binary; they’re reserved per ADR 0028’s never-renumber rule for CPI stability. Init-only guards (6007 WrongChainHash, 6008 WrongPubkey, 6010 InvalidGenesisTime, 6011 InvalidPeriod, 6012 UnauthorizedInit) fire on initialize/update_config, not verify, so your CPI won’t see them.

See Errors reference for the Cmd+F table and decision tree.

The SDK pins:

  • anchor-lang = "0.30.1" (workspace-pinned)
  • ark-bn254 = "0.5.0", ark-ff = "0.5.0", ark-ec = "0.5.0" (workspace-pinned)

Your consumer program likely already pulls Anchor 0.30.1. If you’re on a different Anchor major, the SDK won’t compile — upgrade Anchor first.

For BPF builds, you also need the constant_time_eq = "=0.4.2" pin per the rustc-lag workaround.