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.
cargo add alea-sdkCurrent 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.
Integration guide
Section titled “Integration guide”Wire up the Accounts struct
Section titled “Wire up the Accounts struct”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.
Call the CPI
Section titled “Call the CPI”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).
Compute budget from the caller
Section titled “Compute budget from the caller”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.
Return-data ordering footgun
Section titled “Return-data ordering footgun”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 safelet randomness = alea_sdk::cpi::verify(/* args */)?;token::transfer(transfer_ctx, amount)?;// randomness is still valid
// WRONG — the transfer overwrites Alea's return datatoken::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.
Full API reference
Section titled “Full API reference”Constants
Section titled “Constants”| Name | Type | Description |
|---|---|---|
PROGRAM_ID | Pubkey | Canonical 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. |
Functions
Section titled “Functions”cpi::verify
Section titled “cpi::verify”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 accountconfig— the Alea Config PDA (must be derived withseeds::program = alea_program.key()in the consumer’s Accounts struct)payer— signer, passed through to Alea’s Verify accountsround— drand round numbersignature— 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.
config_pda
Section titled “config_pda”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.
is_round_recent
Section titled “is_round_recent”pub fn is_round_recent( round: u64, config: &Config, clock: &Clock, max_age_seconds: u64,) -> boolReturns 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 consumers3— one drand round, for adversarial MEV-sensitive contexts300— relaxed, for slow settle flows where beacon age matters less than liveness
AleaVerify<'info>
Section titled “AleaVerify<'info>”#[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.
Config
Section titled “Config”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.
AleaVerifier
Section titled “AleaVerifier”pub use alea_verifier::program::AleaVerifier;The Alea program type, for Anchor’s Program<'info, AleaVerifier> account binding.
Errors
Section titled “Errors”alea_sdk::AleaError is re-exported from alea_verifier::errors::AleaError. The variants your CPI can actually see:
| Code | Variant | When |
|---|---|---|
| 6000 | InvalidSignature | Pairing returned Some(false) — signature invalid for this round. |
| 6001 | InvalidG1Point | Signature bytes fail the on-curve check. Wrong encoding or x ≥ field prime. |
| 6002 | RoundZero | Round is 0. Drand rounds start at 1. |
| 6004 | NoSquareRoot | SVDW: all three candidates fail sqrt. Not reachable under honest drand input. File an issue if seen. |
| 6006 | PairingError | alt_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.
Version pinning
Section titled “Version pinning”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.
Related
Section titled “Related”- TypeScript SDK — off-chain client
- CPI Integration Guide — full lottery walkthrough
- Common Pitfalls — known integration footguns
- Errors reference — Cmd+F error-code lookup