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