alea-sdk
First production drand BN254 verifier on Solana. One-line CPI, free public good.
Any Anchor program can receive cryptographically verified on-chain randomness with a single CPI call — no oracles, no callbacks, no off-chain coordination.
Warning: Read CAVEATS.md before integrating. This crate is live on Solana devnet with an internal audit passing (avg 8.66/10 across 10 Claude + 5 Codex persona rounds); external paid firm review and mainnet deployment are Phase 5 gates.
Install
[]
= "0.1"
# Required pin — without this, cargo check / cargo build-sbf fails on
# rustc < 1.95 (current stable is 1.94, Solana BPF is 1.89-dev). See
# Troubleshooting below for context. Re-check after every `cargo update`.
= "=0.4.2"
or
Quick Start — Palestra-Style Integration
use ;
use *;
use Clock;
const MAX_BEACON_AGE_SECONDS: u64 = 30; // reject drand rounds older than 30s
The example is annotated
rust,ignorebecause it references a hypotheticalYourGameStateaccount type. Seeprograms/example-lottery/in the repo for a complete, compiling reference consumer.
Security: Mandatory Constraints for Consumer Programs
Two constraints are MANDATORY for any consumer program. Omitting either ships an exploitable program.
1. seeds::program = alea_program.key() on alea_config
pub alea_config: ,
Without this, an attacker can substitute a fake Config PDA owned by a different program. Anchor re-derives the PDA using Alea's program ID as the signer only when seeds::program is present — it is NOT enforced by default. This guards against total compromise of your randomness source. (ADR 0034)
2. is_round_recent() before trusting randomness
require!;
Without recency enforcement, an attacker can replay an old drand round whose randomness they already know to bias a resolution. Alea's verify accepts any round — recency is the consumer's responsibility.
3. (Privacy-sensitive only) Route through program PDA signer
For applications where the fact that a user is consulting randomness leaks game state (anonymous lotteries, sealed-bid auctions), route the verify CPI through a program-owned PDA signer rather than the end-user wallet. The on-chain BeaconVerified event records the payer. Public-by-design applications can use the end-user wallet directly.
CPI Return Data Ordering Warning
Solana's return data is single-slot — each CPI call overwrites the previous value. Capture the randomness immediately:
// CORRECT — capture first, then other CPIs
let randomness = verify?;
transfer?; // safe
// WRONG — overwrites Alea's return data before you read it
transfer?;
let randomness = verify?; // stale
Compute Budget Requirement
Every transaction calling Alea MUST include a compute budget instruction of at least 900,000 CU. Solana's default is 200K; Alea's verify needs up to 454K, plus consumer headroom.
// Add this before your instruction in every transaction:
let cu_ix = set_compute_unit_limit;
The TypeScript SDK (@alea-drand/sdk) injects this automatically. Rust consumers must add it manually.
Program IDs
| Cluster | Program ID |
|---|---|
| Devnet | ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U |
| Mainnet | Pending Phase 5 (same vanity ID — cluster binding identical) |
Devnet-verified across 10 live rounds. Mainnet traffic begins Phase 5.
Error Codes
Canonical source: programs/alea-verifier/src/errors.rs.
CI enforces table-to-enum coherence on every PR (Phase 6).
| Code | Name | Meaning |
|---|---|---|
| 6000 | InvalidSignature |
BLS pairing check returned non-1 — signature does not attest this (round, pubkey) pair |
| 6001 | InvalidG1Point |
Signature bytes decode but are not on the BN254 G1 curve (y² ≠ x³ + 3 mod p) |
| 6002 | RoundZero |
Round must be > 0 (drand's round 0 is a sentinel, never emitted) |
| 6003 | InvalidFieldElement |
Reserved (unreachable in v1) — do not treat as retryable |
| 6004 | NoSquareRoot |
hash_round_to_g1 exhausted all SVDW candidates; constant corruption or syscall regression (not attacker-reachable) |
| 6005 | InvalidG2Point |
Reserved (unreachable under ADR 0027 fallback path) — do not retry |
| 6006 | PairingError |
alt_bn128_pairing syscall returned Err or wrong-length output (infrastructure) |
| 6007 | WrongChainHash |
Config.chain_hash does not match EXPECTED_EVMNET_CHAIN_HASH (wrong-chain deployment) |
| 6008 | WrongPubkey |
Config.pubkey_g2 does not match EXPECTED_EVMNET_G2_PUBKEY — also emitted by alea_sdk::cpi::verify's owner check on the config account (T1-08, Phase 4.5) |
| 6009 | ReturnDataMissing |
Reserved (unreachable under ADR 0030 Pattern A) |
| 6010 | InvalidGenesisTime |
Config.genesis_time does not match EXPECTED_EVMNET_GENESIS_TIME |
| 6011 | InvalidPeriod |
Config.period does not match EXPECTED_EVMNET_PERIOD |
| 6012 | UnauthorizedInit |
initialize signer does not equal the program's upgrade_authority_address |
| 2001 | Anchor ConstraintHasOne |
update_config signer is not the stored config authority (framework code) |
| 3010 | Anchor AccountNotSigner |
authority account passed without a signature (framework code, fires before 2001) |
Troubleshooting
constant_time_eq@0.4.3 requires rustc 1.95 (both cargo check AND cargo build-sbf)
This affects any Rust toolchain older than 1.95, not just BPF. Even on
native cargo check / cargo doc with stable rustc 1.94 (the current
release at the time of writing), the resolver picks constant_time_eq 0.4.3 which requires rustc 1.95+. The Solana BPF toolchain's embedded
rustc is ~1.89-dev so BPF builds hit it too.
Workaround — pin the offending transitive in your own Cargo.toml:
[]
= "0.1"
# Phase 4.5 fix: pin this transitive to a version that compiles on
# both current stable rustc and Solana BPF toolchain's 1.89-dev:
= "=0.4.2"
Re-verify after every cargo update — if cargo bumps constant_time_eq
past 0.4.2 the pin gets silently overridden unless you use =0.4.2
(exact version, not 0.4.2 which is semver-compatible).
Anchor's declare_id! macro — misleading error cascade on bad base58 length
If you copy the Quick Start and paste a placeholder program ID like
MyProg111111111111111111111111111111111, you'll see:
error: pubkey array is not 32 bytes long: len=N
error[E0425]: cannot find value `ID` in crate `crate`
= help: consider importing this module: `anchor_lang::system_program::ID`
Ignore the rustc helper suggestion about system_program::ID. It's a
red herring — rustc can't see that the cascade is caused by the first
error (a proc-macro panic inside declare_id!) and guesses at imports.
The actual fix is: declare_id! requires exactly a 44-character
base58-encoded ed25519 pubkey (32 bytes = 44 base58 chars). Generate a
real one with:
This is an Anchor UX issue, not an Alea-specific one, but new consumers hit it on first integration.
See 2026-04-19-solana-bpf-rustc-lag-external-consumers for the full list of commonly-affected transitives (updated as Solana's toolchain evolves).
anchor build fails with E0599: no method named 'source_file'
Anchor 0.30.1's anchor-syn crate calls a proc-macro2 API removed in 1.0.82+. This is an Anchor issue, not an alea-sdk issue, but consumers will hit it. Either:
- Pin proc-macro2 to
<=1.0.81in your workspace - Or use
cargo-build-sbfdirectly + hand-managed IDL (seeprograms/alea-verifier/workflow)
Compute budget exceeded / "Program failed to complete"
Every tx that CPIs into Alea MUST include ComputeBudgetInstruction::set_compute_unit_limit(900_000). Solana's default is 200K; Alea needs up to 454K, plus consumer headroom. The @alea-drand/sdk TypeScript SDK injects this automatically; Rust consumers building raw txs must add it manually.
AleaError::WrongPubkey (6008) when the on-chain Config looks correct
This can come from two code paths:
- On-chain
verifyhandler —Config.pubkey_g2 != EXPECTED_EVMNET_G2_PUBKEY(wrong-chain init); redeploy with the correctchain_hash/pubkey_g2 alea_sdk::cpi::verifyhelper — the suppliedconfigaccount's owner is notalea_sdk::PROGRAM_ID(defense-in-depth check added Phase 4.5 T1-08 for non-Anchor callers)
If you're an Anchor program user with #[account(seeds = [b"config"], bump, seeds::program = alea_program.key())] on your config, the second case is not reachable for you — the PDA re-derivation catches it first.
Links
- GitHub: alea-drand/alea
- Full docs site: Coming Phase 6
- License: Apache 2.0 — see LICENSE
- Maturity disclosures: CAVEATS.md