Skip to main content

alea_sdk/
lib.rs

1//! # alea-sdk
2//!
3//! CPI crate for Alea — the first production drand BN254 BLS verifier on
4//! Solana. Any Anchor program can receive verified on-chain randomness with
5//! a single CPI call.
6//!
7//! ## Quick Start
8//!
9//! Add the mandatory constraints to your Accounts struct and call `cpi::verify`:
10//!
11//! ```rust,ignore
12//! use alea_sdk::{self, AleaVerifier};
13//! use anchor_lang::solana_program::sysvar::clock::Clock;
14//!
15//! const MAX_BEACON_AGE_SECONDS: u64 = 30;
16//!
17//! #[derive(Accounts)]
18//! pub struct SettleMatch<'info> {
19//!     pub alea_program: Program<'info, AleaVerifier>,
20//!     #[account(
21//!         seeds = [b"config"],
22//!         bump,
23//!         seeds::program = alea_program.key(),   // ← MANDATORY (ADR 0034)
24//!     )]
25//!     pub alea_config: Account<'info, alea_sdk::Config>,
26//!     pub payer: Signer<'info>,
27//!     pub clock: Sysvar<'info, Clock>,
28//! }
29//!
30//! pub fn settle_match(ctx: Context<SettleMatch>, round: u64, sig: [u8; 64]) -> Result<()> {
31//!     // MANDATORY: reject stale beacons before CPI
32//!     require!(
33//!         alea_sdk::is_round_recent(round, &ctx.accounts.alea_config, &ctx.accounts.clock, MAX_BEACON_AGE_SECONDS),
34//!         YourError::StaleBeacon,
35//!     );
36//!     // One-line CPI. Returns VerifiedRandomness (must_use wrapper).
37//!     let randomness = alea_sdk::cpi::verify(
38//!         ctx.accounts.alea_program.to_account_info(),
39//!         ctx.accounts.alea_config.to_account_info(),
40//!         ctx.accounts.payer.to_account_info(),
41//!         round, sig,
42//!     )?.into_inner();
43//!     // Read IMMEDIATELY — Solana return data is overwritten by any subsequent CPI
44//!     let random_value = u64::from_le_bytes(randomness[0..8].try_into().unwrap());
45//!     // … use randomness …
46//!     Ok(())
47//! }
48//! ```
49//!
50//! ## Security: Mandatory Constraints
51//!
52//! Two constraints are MANDATORY for ANY consumer (omitting either ships an
53//! exploitable program):
54//!
55//! 1. **`seeds::program = alea_program.key()`** on the `alea_config` account.
56//!    Without this, an attacker can substitute a fake Config PDA owned by a
57//!    different program and feed attacker-controlled public keys to the pairing
58//!    check. This is total compromise for any randomness consumer. (ADR 0034)
59//!
60//! 2. **`is_round_recent()` before trusting randomness.** Without recency
61//!    enforcement, an attacker can replay an old drand round whose randomness
62//!    they already know to bias resolution.
63//!
64//! ## CPI Return Data Ordering Warning
65//!
66//! Solana's return data is single-slot — each CPI call overwrites the
67//! previous value. Read `cpi::verify`'s result into a local variable
68//! IMMEDIATELY, before any other CPI calls (token transfers, etc.).
69//!
70//! ## Compute Budget
71//!
72//! Every transaction calling Alea MUST include a compute budget instruction
73//! of at least 900,000 CU (Solana default is 200K; Alea needs up to 454K +
74//! consumer headroom). The TypeScript SDK injects this automatically.
75//!
76//! ## Program IDs
77//!
78//! | Cluster | Program ID |
79//! |---------|-----------|
80//! | Devnet  | `ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U` |
81//! | Mainnet | Pending Phase 5 (same vanity ID — cluster binding is identical) |
82//!
83//! Devnet-verified; mainnet deployment pending Phase 5. Cluster binding
84//! identical (vanity ID usable on both), mainnet traffic begins Phase 5.
85//!
86//! ## Maturity
87//!
88//! See [CAVEATS.md](https://github.com/alea-drand/alea/blob/main/sdk/rust/CAVEATS.md)
89//! for maturity disclosures before integrating.
90
91#![deny(unsafe_code)]
92// Suppress Anchor 0.30.1's harmless `anchor-debug` cfg warning (emitted by
93// the #[derive(Accounts)] macro). Same suppression that
94// programs/alea-verifier/src/lib.rs carries.
95#![allow(unexpected_cfgs)]
96
97pub mod accounts;
98pub mod cpi;
99pub mod errors;
100
101pub use accounts::Config;
102pub use alea_verifier::errors::AleaError;
103pub use alea_verifier::program::AleaVerifier;
104pub use cpi::VerifiedRandomness;
105
106use anchor_lang::prelude::*;
107use anchor_lang::solana_program::sysvar::clock::Clock;
108
109/// Canonical Alea program ID. Vanity, frozen for the lifetime of the
110/// mainnet deployment per ADR 0028. Same ID used across localnet / devnet
111/// / mainnet by design — consumer SDKs do not need to branch per cluster.
112///
113/// Devnet-verified; mainnet deployment pending Phase 5. Cluster binding
114/// identical (vanity ID usable on both), mainnet traffic begins Phase 5.
115///
116/// This re-exports the verifier crate's `declare_id!`-generated `ID`
117/// constant, which guarantees the SDK's PROGRAM_ID can never drift from
118/// the program's on-chain identity at compile time.
119pub const PROGRAM_ID: Pubkey = alea_verifier::ID;
120
121/// Derive the Alea `Config` PDA for a given program ID.
122///
123/// Seeds are `[b"config"]`. The canonical bump is stored in `Config::bump`
124/// at initialization; consumer programs using `bump = config.bump` skip
125/// re-derivation (~10K CU saving per ADR 0028 §"PDA derivation").
126pub fn config_pda(program_id: &Pubkey) -> (Pubkey, u8) {
127    Pubkey::find_program_address(&[b"config"], program_id)
128}
129
130/// Check that a drand round is recent relative to the current on-chain
131/// clock. Returns `true` if the round's emission timestamp is within
132/// `max_age_seconds` of the current slot's `unix_timestamp`.
133///
134/// # Why this exists
135///
136/// Alea's `verify` instruction accepts ANY round — including very old
137/// ones. Any consumer program where randomness resolves a high-stakes
138/// outcome (games, lotteries, prediction markets) MUST enforce recency
139/// before trusting the verified randomness, otherwise an attacker can
140/// replay a known-randomness beacon from months ago. This is a
141/// **consumer-layer responsibility** — Alea itself is stateless by
142/// design and cannot enforce recency without adding accounts / CPI cost.
143///
144/// # Parameters
145///
146/// - `round`: the drand round number being verified
147/// - `config`: the Alea `Config` PDA (read for `genesis_time` and `period`)
148/// - `clock`: the Solana `Clock` sysvar (for `unix_timestamp`)
149/// - `max_age_seconds`: rejection threshold. `30` is a reasonable default
150///   for most consumers; tighten to `3` (one drand round) for adversarial
151///   contexts like MEV-resistant lotteries.
152///
153/// # Overflow safety
154///
155/// `saturating_sub` + `saturating_mul` on all arithmetic. A malformed
156/// `round == u64::MAX` with realistic genesis/period values would
157/// otherwise overflow in `(round - 1) * period`. Saturation is preferred
158/// over wrapping because "stale" is the safe rejection outcome.
159pub fn is_round_recent(round: u64, config: &Config, clock: &Clock, max_age_seconds: u64) -> bool {
160    let round_timestamp = config
161        .genesis_time
162        .saturating_add(round.saturating_sub(1).saturating_mul(config.period));
163    // Phase 4.5 T2-01: clamp negative i64 unix_timestamp to 0 before the u64
164    // cast. Solana's live clock is always positive; this guard handles
165    // localnet clock quirks, misconfigured validators, and hypothetical
166    // future runtime bugs. Without the clamp, a negative i64 wraps to a huge
167    // u64, making all recency checks return stale (false) until clock
168    // normalizes — availability impact for any consumer that calls verify.
169    let current_timestamp = clock.unix_timestamp.max(0) as u64;
170    current_timestamp.saturating_sub(round_timestamp) <= max_age_seconds
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn program_id_matches_expected_vanity() {
179        assert_eq!(
180            PROGRAM_ID.to_string(),
181            "ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U",
182            "PROGRAM_ID must match vanity declared in programs/alea-verifier/src/lib.rs \
183             declare_id! — any drift is a CPI-stability violation per ADR 0028"
184        );
185    }
186
187    #[test]
188    fn config_pda_is_deterministic() {
189        let (pda_a, bump_a) = config_pda(&PROGRAM_ID);
190        let (pda_b, bump_b) = config_pda(&PROGRAM_ID);
191        assert_eq!(pda_a, pda_b);
192        assert_eq!(bump_a, bump_b);
193    }
194}