# Alea — Complete content dump Stateless drand BN254 verification on Solana. This file is the canonical full-debrief payload for LLM agents: every public docs page concatenated into a single plaintext stream, preserving headings and code blocks. - Canonical site: https://alea.so - Source code: https://github.com/alea-drand/alea - License: MIT - Last regenerated: 2026-04-22 - Splash page: https://alea.so/ (live drand evmnet beacon + click-to-verify demo, real devnet tx) - Quickstart: https://alea.so/quickstart - Privacy: https://alea.so/privacy - Docs index: https://alea.so/docs The sections below are the verbatim documentation pages, in reading order. Each page is preceded by `SOURCE: ` so citations can link back. --- SOURCE: https://alea.so/docs # Alea documentation > Drand BN254 verification on Solana — docs index. import { Card, CardGrid, LinkCard } from "@astrojs/starlight/components"; Alea verifies drand BN254 BLS signatures inside a Solana program via a single CPI call. These docs cover the on-chain program and both SDKs. ```bash cargo add alea-sdk npm install @alea-drand/sdk ``` Devnet is live at [`ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U`](https://explorer.solana.com/address/ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U?cluster=devnet). Mainnet deploy is next. ## Integrating ## Understanding ## Reference ## Where Alea lives | Where | What | |-------|------| | Devnet program | [`ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U`](https://explorer.solana.com/address/ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U?cluster=devnet) | | Rust crate | [`alea-sdk`](https://crates.io/crates/alea-sdk) on crates.io | | npm package | [`@alea-drand/sdk`](https://www.npmjs.com/package/@alea-drand/sdk) | | Source | [github.com/alea-drand/alea](https://github.com/alea-drand/alea) | | Releases | [github.com/alea-drand/alea/releases](https://github.com/alea-drand/alea/releases) | | Drand chain | [api.drand.sh](https://api.drand.sh) (evmnet, 3-second cadence) | | Contributing | [CONTRIBUTING.md](https://github.com/alea-drand/alea/blob/main/CONTRIBUTING.md) | License is Apache 2.0. Alea is a public good — no commercial tier exists and none is planned. Alea exists because of prior work by [Randamu](https://randa.mu) (who runs drand and built the BN254 Solana prototype that never shipped) and [kevincharm](https://github.com/kevincharm) (who wrote the BN254 scheme into drand itself and built [Anyrand](https://anyrand.com), the EVM version of what Alea does). Full acknowledgments on [Comparison](/docs/concepts/comparison#prior-art-and-acknowledgments). --- SOURCE: https://alea.so/docs/getting-started/intro # Introduction > What Alea is, what drand is, why on Solana. Alea is an on-chain verifier for [drand](https://drand.love) random beacons on Solana. Rust SDK on crates.io, TypeScript SDK on npm, Apache 2.0, devnet today. The job is narrow. Drand emits a verifiable random value every three seconds. Alea proves a single round on-chain in roughly 407,000 compute units, returns the 32-byte randomness as program output, and reverts on any invalid signature. No per-call state. No operator. The upgrade authority today is a solo deployer keypair; long-term plan is transfer to a Squads multisig and eventual immutability, no fixed timeline. ## What you get A single CPI from your Anchor program into Alea: ```rust use alea_sdk::{cpi, AleaVerify}; let randomness: [u8; 32] = cpi::verify( ctx.accounts.alea_program.to_account_info(), ctx.accounts.alea_config.to_account_info(), ctx.accounts.payer.to_account_info(), round, signature, )?; ``` The returned bytes are the same 32 bytes every drand consumer in the world sees for that round number. SHA-256 of the BLS signature, computed identically on Ethereum, Filecoin, your laptop, and Solana. There is no Alea-specific format. ## Who this is for Anyone on Solana who needs unbiasable randomness that every caller sees identically — lotteries, validator-rotation seeds, NFT trait reveals anchored to a future round. Choose Alea when you want one transaction end-to-end, no keeper or operator, and can tolerate drand's trust assumption. When you need per-caller unique randomness (each request getting its own value bound to the caller's context), [ORAO VRF](https://orao.network) is the right tool — see [Comparison](/docs/concepts/comparison) for the full map. ## What you don't get Alea proves drand. It does not generate randomness. The trust assumption is drand's: the [League of Entropy](https://drand.love/loe) federation must not collude to forge signatures. That federation is currently 15 independent organizations — Cloudflare, Protocol Labs, Ethereum Foundation, Kudelski, universities, a handful of infrastructure companies. If they collude, every drand consumer is broken, not just yours. You're downstream of that assumption, not on top of it. Alea also does not enforce beacon freshness. A signature for a year-old drand round is still mathematically valid; your program is responsible for rejecting stale rounds. The Rust SDK ships `is_round_recent()` for this. Skip it and your settle has no replay defense. ## Where Alea is today Devnet live at [`ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U`](https://explorer.solana.com/address/ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U?cluster=devnet). Rust crate [`alea-sdk`](https://crates.io/crates/alea-sdk) on crates.io. npm package [`@alea-drand/sdk`](https://www.npmjs.com/package/@alea-drand/sdk). Mainnet deploy is next; the vanity program ID is the same on both clusters by design. When you're ready to integrate, go to [Install](/docs/getting-started/install) and then the [CPI Integration Guide](/docs/sdks/cpi-integration). --- SOURCE: https://alea.so/docs/getting-started/install # Install > Install the Rust and TypeScript SDKs. Pin the Solana BPF rustc constraint. ## Rust ```bash cargo add alea-sdk ``` Latest: `0.1.0` on [crates.io](https://crates.io/crates/alea-sdk). Crate name is `alea-sdk`, the importable module is `alea_sdk`. It re-exports the types your Anchor program needs for CPI, plus the two helpers `config_pda()` and `is_round_recent()`. ### BPF rustc lag pin Solana's `cargo-build-sbf` embeds a rustc that lags the public release by several minor versions. The transitive dependency `constant_time_eq` bumps its MSRV faster than that. If your `cargo build-sbf` fails with `requires rustc 1.95.0`, pin the dependency in your consumer `Cargo.toml`: ```toml [dependencies] constant_time_eq = "=0.4.2" ``` A future Solana toolchain release will move the floor up. Until then every BPF consumer of `alea-sdk` (and many other crates) needs this pin. See [Common Pitfalls](/docs/sdks/common-pitfalls) for the full story. ## TypeScript ```bash npm install @alea-drand/sdk ``` Latest: `0.2.0` on [npmjs.com](https://www.npmjs.com/package/@alea-drand/sdk). Peer dependencies you must already have: ```bash npm install @solana/web3.js@^1.95 @coral-xyz/anchor@^0.30.1 ``` Why pin web3.js to 1.95+ rather than 2.x: Alea was built against the current Anchor toolchain, which is itself web3.js 1.x. Anchor 2 / web3.js 2 will land later and the SDK will follow. The specific incompatibility between Anchor 0.30.1 and web3.js 1.98+ is documented in [Common Pitfalls](/docs/sdks/common-pitfalls) — the SDK works around it internally. ## Verify the install Rust: ```rust use alea_sdk::PROGRAM_ID; println!("{}", PROGRAM_ID); // ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U ``` TypeScript: ```ts import { DEVNET_PROGRAM_ID } from "@alea-drand/sdk"; console.log(DEVNET_PROGRAM_ID.toBase58()); // ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U ``` If both print that base58 address, the install is good. Next: [CPI Integration Guide](/docs/sdks/cpi-integration) walks through a complete program. --- SOURCE: https://alea.so/docs/concepts/architecture # Architecture > How the verify instruction works, where the trust boundary sits, why the program holds almost no state. The Alea program is one Anchor instruction wrapped around Solana's `alt_bn128_pairing` syscall. ## The verify call, end to end ```text caller ──► verify(round, signature) │ ├─ require round > 0 (else revert 6002 RoundZero) ├─ require signature on G1 (canonical) (else revert 6001 InvalidG1Point) │ ├─ msg_hash = keccak256(round.to_be_bytes()) [8 bytes BE → 32] ├─ H = map_to_curve_g1(msg_hash) [deterministic SVDW] │ ├─ alt_bn128_pairing( ← Solana syscall, BN254 optimal Ate │ [(signature, G2_generator), │ (negate(H), Config.pubkey_g2)] │ ) │ ├─ if returns Some(true): set_return_data(sha256(signature)) [32 bytes] ├─ if returns Some(false): revert with AleaError::InvalidSignature (6000) └─ if returns None (syscall Err): revert with AleaError::PairingError (6006) ``` If the pairing check returns 1, the signature is a valid BLS aggregate produced by the drand threshold of nodes for that round. If it returns 0, the bytes don't verify. ## Why almost stateless The program holds a single `Config` PDA at seeds `[b"config"]` containing the drand chain's group public key, chain hash, genesis time, and period. No per-call state, no user accounts, no balances. The program has two admin instructions that touch Config — `initialize` and `update_config` — plus the read-only `verify`. `initialize` writes Config once and is gated to the BPFLoader upgrade authority via `AleaError::UnauthorizedInit` (6012). `update_config` is gated by Anchor's `has_one = authority` against `Config.authority`, the init signer. Both admin instructions enforce byte-equality against `EXPECTED_EVMNET_*` constants at the program level — Config cannot be written with non-evmnet values without first shipping an upgrade that changes those constants. Which requires the upgrade authority. A few consequences of this shape. Two calls with the same `(round, signature)` produce identical on-chain effects — a return-data emission and a successful exit — so replay defense is your consumer program's job (via `is_round_recent`, below), not Alea's. The program owns no lamports and makes no transfers; there's nothing to drain. And a future upgrade can add new instructions but cannot retroactively change what `verify(round, signature)` returned for inputs that already verified today. The math is fixed the moment a signature passes. Why is `is_round_recent` a separate helper instead of baked into `verify`? Because making it mandatory inside the verifier would force Alea to take a stance on what "recent enough" means — and the honest answer is "it depends." A lottery needs 30 seconds of slack, an MEV-resistant draw needs 3, a commit-reveal needs no freshness check at all because the round is known in advance. Baking in a single window either tightens it past legitimate use cases or loosens it past adversarial ones. Externalizing the check pushes the decision back to the consumer, which is the only party that can make it. ## Why 407K compute units Measured breakdown from a devnet verify in April 2026: | Op | CU | Note | |---------------------------------|--------|------| | `alt_bn128_pairing` (2 pairs) | ~250K | The BLS verify itself | | `map_to_curve_g1` (SVDW) | ~120K | Hash-to-curve preimage | | Anchor accounts + return data | ~37K | Discriminator parse, PDA load, `sol_set_return_data` | Typical measured verify is ~407K. Worst case observed in benchmarks across input variance: ~454K (documented in the SDK's rustdoc). Set your `ComputeBudgetProgram::set_compute_unit_limit` to **900,000** whenever you call Alea, whether via CPI from another program or as a standalone tx — 900K covers 454K worst-case plus enough headroom for state writes and a second CPI on top. The TypeScript SDK injects the compute-budget instruction automatically; from Rust, prepend it yourself. ## What the trust boundary looks like Alea's security rests on two assumptions: 1. **Drand's threshold-BLS group public key is correct.** If the public key in the Config PDA is wrong — either never-correct from a botched init, or replaced via a hostile upgrade — every signature fails. Detectable at integration time: cross-verified fixtures for drand rounds 1 and 9,337,227 live in the repo, and your CI should run them against the live program on deploy day. 2. **Solana's `alt_bn128_pairing` syscall is correctly implemented.** The syscall has been live on mainnet-beta for several protocol versions. [SIMD-0334](https://github.com/solana-foundation/solana-improvement-documents/pull/334) activated in March 2026 was a length-check hardening, not a new feature. A bug in the syscall affects every BN254-using Solana program, not Alea specifically. Blast radius is the network. Nothing on this list depends on Alea's deploy keypair or a single human's intent. The Config PDA is write-gated by the program's byte-equality guards: non-evmnet values are rejected at the instruction boundary. A malicious upgrade could brick the program — ship a binary that reverts every call, or rewrites Config with wrong constants after changing the `EXPECTED_EVMNET_*` values — but it cannot forge a signature. A bad signature still fails the pairing check on any deployed binary. For the governance plan (deployer keypair today, planned multisig and eventual immutability with no fixed timeline), see the [Security model](/docs/concepts/security-model#authority-compromise--what-an-attacker-gets) page — both upgrade authority and `Config.authority` are covered there. ## The `Config` PDA The on-chain account at devnet address [`6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8`](https://explorer.solana.com/address/6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8?cluster=devnet), derived from `[b"config"]` seeded against the Alea program. Declaration order matches Borsh serialization order: ```rust pub struct Config { pub pubkey_g2: [u8; 128], // drand 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 gating update_config pub bump: u8, // canonical PDA bump } ``` Consumer programs reference it read-only. The `seeds::program = alea_program.key()` constraint on your Accounts struct re-derives the PDA under Alea's program ID, so a Config account owned by a different program cannot be substituted. Omitting that constraint ships a program with a full-compromise bug — see [Security model](/docs/concepts/security-model) for the fake-Config scenario. ## Related - [drand & BN254](/docs/concepts/drand-bn254) — the cryptographic primer - [Security model](/docs/concepts/security-model) — threat boundary and what's been verified - [Program interface](/docs/reference/program-interface) — raw on-chain instruction contract for non-Anchor callers --- SOURCE: https://alea.so/docs/concepts/drand-bn254 # drand & BN254 > What drand is, why evmnet picked BN254, what threshold-BLS gives you over a single-signer VRF. ## Plain-language overview Drand is a network of independent organizations that produce a verifiable random value every three seconds. Each round, member organizations participate in a threshold-BLS signing ceremony: a configured threshold of the members collectively produces one aggregate signature. No single member, and no subset smaller than the threshold, can forge one alone. The signature itself is the randomness — pseudorandom under standard cryptographic assumptions, public and verifiable by anyone. The federation is called the [League of Entropy](https://drand.love/loe). It has been producing beacons continuously since 2019. Members include Cloudflare, Protocol Labs, Ethereum Foundation, Kudelski, ChainSafe, EPFL, University of Chile, UCL, and a handful of others. Drand publishes several "chains" in parallel, each with its own group public key and cadence. Alea verifies the **evmnet** chain: 3-second cadence, BN254 curve, designed specifically for cross-VM verification. ### Why threshold-BLS instead of a VRF A VRF (Verifiable Random Function) lets a single key-holder produce a verifiable random output. The trust story is "the key-holder won't grind, won't front-run, won't withhold." That's a real assumption: a VRF operator can refuse to publish a value whose outcome they don't like. Threshold-BLS distributes the trust. To bias drand you need to compromise enough member organizations to produce a forged aggregate signature. The federation was deliberately built with adversarial relationships between members across continents and institutional types. If the threshold collude, every drand consumer globally is broken, not just yours. That's stronger than any single-operator VRF you can get on Solana today. ### What "the randomness is the signature" means A BLS signature on a message `m` is a single elliptic-curve point. For a fixed group public key, that point is uniquely determined by the inputs: there is exactly one valid signature, nobody can choose between two. Once enough members have signed, the signature is fixed. The drand convention takes `sha256(signature_bytes)` as the 32-byte randomness output. Every drand consumer computes the same 32 bytes for the same round number. The bytes Alea returns from `verify()` are the bytes drand defines. There is no Alea-specific transformation. This means: - You can pre-commit to a future round and reveal the randomness deterministically when it publishes. - You can cross-reference. If round 9,337,227 returns randomness X on Alea, drand on Ethereum, and `curl https://api.drand.sh//public/9337227`, all three return the same X. ## The math behind the verify Notation below. Skip to [Pointers](#pointers) if you're wiring a lottery; read on if you're auditing the program or porting the verifier to another chain. ### Why BN254 for evmnet Drand's mainnet chain runs on BLS12-381, which is the modern standard for post-2020 BLS work. BN254 (the Barreto–Naehrig curve, 2005) has weaker concrete security — roughly 100-bit against BLS12-381's 128-bit — but evmnet picked it for two practical advantages. On the EVM side, Ethereum has supported pairings over BN254 since [EIP-197](https://eips.ethereum.org/EIPS/eip-197) in 2017. Verifying a BN254 BLS signature costs around 120K gas. BLS12-381 verification on EVM requires either a custom precompile (only landed in Pectra, 2025) or a far more expensive in-EVM implementation. On Solana, `alt_bn128_pairing` exists for the same reason — cheap BN254-compatible cryptography. Verifying a BN254 BLS signature on Solana costs ~250K CU for the pairing call. BLS12-381 has no equivalent syscall; implementing it in pure Rust runs into the tens of millions of CU, which is well past what Solana can execute in one transaction. BN254's weaker concrete security is a real tradeoff, not a marketing footnote. 100-bit security doesn't mean "broken"; it means drand and the consumer accept a wider safety margin against future factoring / discrete-log advances than BLS12-381 consumers do. Whether that margin is large enough for your use case is your call. For randomness feeding a lottery with a one-day settle window, yes. For multi-year immutable commitments anchored to a single BN254 signature, talk to a cryptographer first. Drand's evmnet chain is explicitly the cross-VM-friendly chain. Use it when you want the same randomness verifiable cheaply on both EVM and Solana from the same beacon. Use drand's mainnet (BLS12-381) chain if you want stronger concrete security and your verifier has efficient access to BLS12-381 precompiles. ### The verification equation BLS signature verification reduces to a single pairing check. Let: - `σ` ∈ G1 — the aggregate signature (64 bytes uncompressed, what drand publishes) - `pk` ∈ G2 — the group public key (128 bytes uncompressed, stored in `Config.pubkey_g2`) - `G2` — the G2 generator - `m_hash` = `keccak256(round.to_be_bytes())` — 32 bytes from the 8-byte big-endian round number - `H(m_hash)` — hash-to-curve via Shallue–van de Woestijne on G1 A valid signature satisfies: ```text e(σ, G2) == e(H(m_hash), pk) ``` Equivalently (what Alea actually evaluates): ```text e(σ, G2) · e(-H(m_hash), pk) == 1 ``` The `alt_bn128_pairing` syscall takes a list of (G1, G2) pairs, multiplies their pairings, and returns 1 if the product is 1 in GT. Alea passes two pairs: `(σ, G2)` and `(-H(m_hash), pk)`. Syscall returns `Some(true)`, the signature is valid; `Some(false)`, invalid; `None` (syscall Err), inputs were malformed. Note: the message hashed to the curve is keccak256 of the round bytes, nothing else. No domain separator, no chain_hash concatenation, no RFC 9380 expand_message prefix. This is the evmnet scheme (`bls-bn254-unchained-on-g1`); changing the preimage would require a different drand chain. Cross-chain substitution is caught at the pairing step, not via message construction — a signature produced against a different group public key simply fails the pairing check. ### Hash-to-curve: Shallue–van de Woestijne Mapping the 32-byte message to a G1 point must be deterministic and admit no manipulable relationship between message and point. The SVDW algorithm does this: a closed-form map from field elements to curve points that covers the curve image evenly and has no known structure a signer could exploit. Alea's on-chain implementation is a port of [kevincharm/bls-bn254](https://github.com/kevincharm/bls-bn254) — the Solidity reference for BN254 BLS verification. The Rust port is in `programs/alea-verifier/src/crypto/hash_to_g1.rs`. SVDW runs in ~120K CU on Solana, dominated by field-element exponentiations. ## Pointers - [drand foundation docs](https://drand.love/docs/) — protocol-level, not Alea-specific - [The original drand paper](https://eprint.iacr.org/2020/096) — Mary Maller et al., 2020 - [SIMD-0334](https://github.com/solana-foundation/solana-improvement-documents/pull/334) — Solana's `alt_bn128_pairing` input-length fix activated March 2026 - [EIP-197](https://eips.ethereum.org/EIPS/eip-197) — Ethereum's BN254 pairing precompile - [kevincharm/bls-bn254](https://github.com/kevincharm/bls-bn254) — Solidity reference; Alea's SVDW port source. Same author wrote the BN254 scheme into drand itself, and built [Anyrand](https://anyrand.com) (the EVM equivalent of what Alea does on Solana). - [randa-mu/bls-solana](https://github.com/randa-mu/bls-solana) — Randamu's BN254 drand verifier prototype for Solana. Proved the shape of the problem; Alea takes it to production. Randamu also runs drand itself. --- SOURCE: https://alea.so/docs/concepts/security-model # Security model > What Alea defends against, what it explicitly does not, and where the trust boundary sits. ## What Alea defends against The BLS pairing check is the only defense against signature forgery, and it's sufficient. The verifier computes `msg_hash = keccak256(round.to_be_bytes())`, maps it to G1 via SVDW, and runs `alt_bn128_pairing([(σ, G2_gen), (-H, pubkey_g2)])`. Valid signatures return pairing-equals-identity; invalid ones revert with `AleaError::InvalidSignature` (6000). The pairing math is a Solana syscall, not a Rust library that could be subtly broken. Pipeline pinned in [`verify.rs`](https://github.com/alea-drand/alea/blob/main/programs/alea-verifier/src/instructions/verify.rs). Signatures are bound to the round number through the message hash, so a signature valid for round N cannot be replayed as round M — `H` is different and the pairing fails. No stored round state, no window, no chance of misbinding. Cross-chain signature substitution (someone trying to pass a mainnet or fastnet drand signature as an evmnet one) is caught by the pairing check itself. The `verify` path never reads `Config.chain_hash` — a signature produced against a different chain's group public key simply fails pairing against evmnet's `pubkey_g2` and reverts 6000. The `WrongChainHash` (6007) and `WrongPubkey` (6008) guards are belt-and-braces protections at init / update time, not runtime verify-path checks. They ensure Alea was deployed against evmnet in the first place; pairing does the per-call work. **Fake Config substitution is the integrator footgun.** The `seeds::program = alea_program.key()` constraint on your Accounts struct is mandatory. Without it, an attacker can pass a PDA derived under Alea's program ID but at different seeds — or a future-version Alea PDA that happens to fit the `Config` layout — and feed chosen public keys into the pairing. Anchor's `Account<'info, Config>` type guarantees the account is owned by Alea's program (so a totally-fake account from another program is rejected at deserialize), but without `seeds::program`, it does NOT guarantee the account was derived from `[b"config"]` against Alea's program ID specifically. That second check is what closes the substitution hole. The Rust SDK's [`AleaVerify`](https://github.com/alea-drand/alea/blob/main/sdk/rust/src/accounts.rs) reference struct includes both constraints; if you copy the two fields into your own Accounts struct rather than use the reference, do not omit `seeds::program`. A hostile RPC can refuse to broadcast your transaction or silently drop the result. What it cannot do is make a signature verify that wouldn't, or vice versa — `verify` executes deterministically on every validator's local copy of the program. The attack surface is availability, not soundness. ### Authority compromise — what an attacker gets Two authority keys matter, and they do different things: 1. **BPFLoader upgrade authority** controls program-binary replacement. Compromise ships a malicious binary. The blast radius is forward-only: past verifies remain mathematically valid (the signatures were genuinely checked against the real pubkey at the time), but every subsequent verify becomes attacker-controlled. Consumers defend by pinning against the known-good binary SHA and re-auditing on upgrades. 2. **`Config.authority`** is the persistent admin key gating `update_config`. An attacker with this key can call `update_config`, but the handler enforces byte-equality against `EXPECTED_EVMNET_*` constants — any non-evmnet value is rejected. Combined with the idempotency guard in `update_config.rs` (no-op when the stored values already match), a compromise today produces no observable effect: the same evmnet values go in, the same evmnet values stay. If a future program upgrade ever changes the `EXPECTED_EVMNET_*` constants, `Config.authority` becomes privileged against those new values. No in-band rotation path exists; rotating `Config.authority` requires a program upgrade that rewrites the relevant code. Today both keys are the deployer keypair. Planned transfer to a Squads multisig, eventual immutability, no fixed timeline. When the multisig transfer happens, `Config.authority` rotates alongside the BPFLoader upgrade authority via an explicit program upgrade. ### Supply chain On-chain crate runtime deps are `anchor-lang`, `solana-program`, and the arkworks field / curve stack (`ark-bn254`, `ark-ff`, `ark-ec`), all version-pinned at `=0.5.0` in the workspace. Pairing runs as a syscall, not through a Rust crate, so the arkworks stack handles only pre-pairing point arithmetic and SVDW. A compromised off-chain SDK could round-downgrade before submission (feed your code a stale beacon whose randomness the attacker already knows), but cannot make the on-chain program accept invalid signatures. Defend against round downgrade with `is_round_recent`. ## What Alea explicitly does not defend against If the threshold of [League of Entropy](https://drand.love/loe) members collude, they can produce signatures that verify against the published group public key. That breaks every drand consumer — Ethereum, Filecoin, your laptop, Alea. Alea sits downstream of drand's trust assumption, not on top of it. A sufficient-stake Solana attacker can re-order or censor verify transactions. They cannot make an invalid signature verify (the syscall runs on every validator), but they can withhold a valid one. For high-stakes settlements — lottery payouts above a few thousand dollars, validator-rotation seeds, prediction-market resolution — wait for `finalized` commitment. `confirmed` has non-zero reorg risk. Bugs in `alt_bn128_pairing` itself are another floor Alea inherits. The syscall has been through Solana validator-client review, but it's software. [SIMD-0334](https://github.com/solana-foundation/solana-improvement-documents/pull/334) (activated on mainnet-beta March 2026) was a real input-length validation hardening. A future bug in the syscall would affect every BN254-using Solana program, not Alea specifically. Treat it as part of the trust base. ### Stale beacons are your problem, not Alea's Alea verifies any mathematically valid signature, regardless of when drand produced the round. Replay protection sits in the consumer program. The SDK ships `is_round_recent(round, config, clock, max_age_seconds)` that compares the round's drand-derived timestamp against the on-chain `Clock` sysvar: ```rust require!( alea_sdk::is_round_recent(round, &ctx.accounts.alea_config, &ctx.accounts.clock, 30), YourError::StaleBeacon, ); ``` 30 seconds is a sensible default. 3 seconds (one drand round) fits MEV-sensitive contexts where even one round of wiggle is exploitable. Skip the check entirely and your settle has no replay defense — an attacker submits a year-old valid drand round whose randomness they already know and biases the outcome. ### Application-level misuse Alea returns 32 bytes. What you do with them — modular reduction for an index, range mapping, multi-byte concatenation — is on you. The most common bug is `bytes[0..8] mod N` for small `N`, which gives a non-uniform distribution. Use rejection sampling, or the closest unbiased method to your range. The constraints look pedantic because they are: a lottery that biases 1-in-256 outcomes is trivially drainable by anyone who notices. ### Front-running between verify and settle Alea verifies a beacon. It does not order your state transitions. If your `settle()` reads the verified randomness and then writes a winner, the gap between those steps is a window for a same-block adversary who sees the transaction in the mempool. Do verify and state-write in the same instruction, atomically. Split them across two transactions and the property is gone. ## What's been verified Full breakdown in [Testing & guarantees](/docs/security/testing). Worth citing inline because auditors look for it first: - Fixture verification: drand rounds 1 and 9,337,227 cross-verified against [`@kevincharm/noble-bn254-drand`](https://github.com/kevincharm/noble-bn254-drand), [`arkworks-rs/algebra`](https://github.com/arkworks-rs/algebra), and the drand REST API. Test sources: [`verify.rs` tests](https://github.com/alea-drand/alea/blob/main/programs/alea-verifier/src/instructions/verify.rs#L123-L153). Testing page covers the fuzz baseline, the reproducible-build SHA, and the testing gaps (external audit, formal verification, constant-time characterization) without repeating them here. ## Reporting Vulnerability disclosures: [security@alea.so](mailto:security@alea.so) or via [GitHub Security Advisory](https://github.com/alea-drand/alea/security/advisories/new). Standard responsible-disclosure window is 90 days from acknowledgment. No paid bug bounty currently. See [Responsible disclosure](/docs/security/disclosure) for the full process. --- SOURCE: https://alea.so/docs/concepts/comparison # Comparison > Alea vs ORAO VRF, Switchboard randomness, and DIY commit-reveal. When to pick each. Four ways to get randomness on Solana today: Alea, ORAO VRF, Switchboard On-Demand, or a DIY commit-reveal. Here's where each wins and where each loses. *Last reviewed: 2026-04-20. Competitor pricing, trust models, and integration footprints change — check each project's own docs before sizing your budget.* ## The shapes at a glance | Option | Cost | Latency | CU | Tx count | Per-caller unique? | Who you trust | |---|---|---|---|---|---|---| | Alea | tx fee | 3s | ~407K | 1 CPI | No | **No one** | | [ORAO VRF](https://orao.network) | tx + ORAO fee | 1–3s | Callback only | 2 | Yes | ORAO | | [Switchboard](https://switchboard.xyz) | tx + oracle fee | 1–3s | Varies | 2 | Yes | Switchboard + TEE | | DIY commit-reveal | tx only | Your window | Your code | 2+ | Yes | Your committers | **Per-caller unique** is the axis that decides most of the choice. Alea gives every caller of round N the same 32 bytes. ORAO and Switchboard give each request its own unpredictable value bound to caller context. Different tools for different jobs. ## When to pick Alea Alea fits when all three of these are acceptable: the only cost you want to pay is a Solana transaction fee, you don't need each caller to get a distinct random value, and drand's trust assumption (a threshold of the League of Entropy federation must not collude) is within your threat model. That's a narrow shape — one transaction end-to-end, no keeper, no per-draw service fee — but common enough to matter. Concrete fits: periodic lotteries where every participant sees the same draw. Validator rotation seeds. NFT trait reveals anchored to a future round and revealed on publish. Cross-chain games where Ethereum and Solana consumers need to agree on the same value. Public-good randomness as a CPI dependency with no token to buy. ## When NOT to pick Alea If you need per-caller unique randomness (each user's draw gets its own unpredictable value bound to their context), use [ORAO VRF](https://orao.network). Alea's shape is "one beacon per round, shared across all callers." A raffle where every user draws in isolation needs a different primitive. Drand beacons don't bind to caller state. Sub-second latency is also wrong for Alea. Drand's cadence is 3 seconds and Alea inherits that period. If randomness must resolve within the same user-request cycle, use an oracle network with lower latency or a commit-reveal with short windows. Alea also won't give you private randomness. Drand beacons are public the moment they're signed — anyone watching the chain sees the same bytes you do. Note: none of the options on this page provide private randomness on-chain. VRF outputs (ORAO, Switchboard) are public once fulfilled too. For private-to-caller randomness, use MPC or FHE constructions, not any randomness-on-Solana primitive. If you can't tolerate the drand trust assumption — that the threshold of the 15-org federation won't collude — no drand-based verifier fits. That's the floor Alea sits on. ## What Alea's doing that's worse than the alternatives Not on mainnet yet. ORAO and Switchboard have been in production on Solana for years with a track record of shipped integrations; Alea is devnet-only today, with mainnet deploy next and no external audit behind it. If production maturity is your first filter, you're not ready for Alea. Higher per-verify CU cost. Alea burns ~407K CU per verify — heavier than ORAO's callback path, which matters for transactions that are already bumping against CU limits. A fixed 3-second cadence is inherited from drand, not a knob to turn. You also get one person maintaining this as a public good, not a team with support channels and SLAs — responses are best-effort. And the testing claims (fuzz, cross-verification, reproducible build) all live in the repo; what's missing from them is an external audit. See [Testing & guarantees](/docs/security/testing) for the full disclosure. If any of these rule Alea out, one of the tools below will fit better. ## The alternatives, in more detail ### ORAO VRF — per-caller unique randomness ORAO is a VRF that produces a random output bound to the caller's request context, with the VRF proof verified on-chain. Two transactions (request + callback). Live on Solana mainnet since 2022 with a catalog of existing integrations — on-chain games, loot boxes, per-user NFT reveals, raffles, giveaways. Pick ORAO over Alea when you need per-caller unique randomness and want production-proven infrastructure. Read their own docs for the current trust-model details; the on-chain VRF proof check is the load-bearing piece of their security story. ### Switchboard On-Demand randomness — oracle-service trust Switchboard's On-Demand randomness product uses their oracle infrastructure with TEE-attested outputs. Total cost is the Solana tx fee plus an oracle fee. Read their docs for the current trust-model details; the specifics of how the TEE attestation is verified on-chain are where the real security story lives. Pick Switchboard when you're already running Switchboard feeds (price oracles, custom functions), want per-request uniqueness, and the TEE-attested oracle model fits your threat model. ### Commit-reveal (DIY) — zero direct service cost, coordinator required Users, or a coordinator, commit to a hash before the draw, then reveal the preimage. The randomness is derived from revealed preimages. Zero on-chain fee beyond transaction cost. Pick commit-reveal when you have a small trusted set of participants, can tolerate a two-phase protocol, and want zero external-service dependency. Doesn't scale to public applications — any committer can refuse to reveal if the outcome is bad for them (withhold attacks). ## Prior art and acknowledgments Alea exists because of two pieces of earlier work. **[Randamu](https://randa.mu)** runs drand itself, and built [`randa-mu/bls-solana`](https://github.com/randa-mu/bls-solana), a BN254 drand verifier prototype for Solana that never shipped to any cluster. They did the hard part — proving the shape of the problem and the on-chain primitives needed for drand verification on Solana. Alea is attempting to take that work to mainnet. **[kevincharm](https://github.com/kevincharm)** wrote the BN254 scheme into drand itself and built [Anyrand](https://anyrand.com), the EVM version of what Alea does. Alea's SVDW hash-to-curve and BN254 constants port from his Solidity reference at [`kevincharm/bls-bn254`](https://github.com/kevincharm/bls-bn254). The cross-verification vectors used in Alea's test suite come from [`kevincharm/noble-bn254-drand`](https://github.com/kevincharm/noble-bn254-drand). If any of these tools fit your use case better than Alea, use them. --- SOURCE: https://alea.so/docs/sdks/rust # Rust SDK > alea-sdk crate — types, CPI helper, Anchor accounts, error enum, version pins. The Rust SDK is [`alea-sdk`](https://crates.io/crates/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. ```bash 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. ## Integration guide ### 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. ```rust 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](/docs/concepts/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 ```rust const MAX_BEACON_AGE_SECONDS: u64 = 30; pub fn settle_match(ctx: Context, 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 ```rust 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 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. ```rust // 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. ## Full API reference ### 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 #### `cpi::verify` ```rust 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. #### `config_pda` ```rust 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` ```rust 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 ### Types #### `AleaVerify<'info>` ```rust #[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` ```rust 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](https://github.com/alea-drand/alea/blob/main/programs/alea-verifier/src/state.rs#L55-L95) — 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` ```rust pub use alea_verifier::program::AleaVerifier; ``` The Alea program type, for Anchor's `Program<'info, AleaVerifier>` account binding. ### 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](/docs/reference/errors) for the Cmd+F table and decision tree. ## 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](/docs/getting-started/install#bpf-rustc-lag-pin). ## Related - [TypeScript SDK](/docs/sdks/typescript) — off-chain client - [CPI Integration Guide](/docs/sdks/cpi-integration) — full lottery walkthrough - [Common Pitfalls](/docs/sdks/common-pitfalls) — known integration footguns - [Errors reference](/docs/reference/errors) — Cmd+F error-code lookup --- SOURCE: https://alea.so/docs/sdks/typescript # TypeScript SDK > @alea-drand/sdk — verifyDrandBeacon, getVerifiedRandomness, fetchBeacon, and every exported constant and type. The TypeScript SDK is [`@alea-drand/sdk`](https://www.npmjs.com/package/@alea-drand/sdk) on npm. It builds, signs, and sends the verify transaction for you, polls for confirmation, and extracts the 32-byte randomness from transaction return data. ```bash npm install @alea-drand/sdk ``` Current version: `0.2.0`. Apache 2.0. ESM-only. Peer dependencies: ```bash npm install @solana/web3.js@^1.95 @coral-xyz/anchor@^0.30.1 ``` This page is guide first, full reference second. The reference at the bottom covers every export from `src/index.ts`. ## Integration guide ### The two main entry points #### `getVerifiedRandomness` — fetch + verify in one call ```ts import { getVerifiedRandomness } from "@alea-drand/sdk"; import { Connection, Keypair } from "@solana/web3.js"; const connection = new Connection("https://api.devnet.solana.com", "confirmed"); const signer = Keypair.fromSecretKey(/* ... */); const randomness: Uint8Array = await getVerifiedRandomness({ connection, signer, }); // randomness.length === 32, freshly verified on-chain ``` Fetches the latest drand round from the evmnet chain, builds an Anchor verify instruction, signs with `signer`, broadcasts via `sendRawTransaction`, polls for confirmation, and returns the 32-byte randomness. Throws `AleaError` on any failure. #### `verifyDrandBeacon` — verify a specific round ```ts import { verifyDrandBeacon, fetchBeacon } from "@alea-drand/sdk"; const beacon = await fetchBeacon(9337227n); // beacon.round === 9337227n // beacon.signature is a 64-byte Uint8Array const randomness = await verifyDrandBeacon({ connection, signer, round: beacon.round, signature: beacon.signature, }); ``` Use this when you need to verify a specific round (commit-reveal patterns, replaying a round for inspection, reproducing a historical draw). ### Browser wallets Both functions accept a `Keypair` or a wallet-adapter `Wallet`. If you pass a wallet-adapter wallet, its `signTransaction()` method is used — the user's wallet signs in the browser, no private keys touch your code: ```tsx import { useConnection, useWallet } from "@solana/wallet-adapter-react"; import { getVerifiedRandomness } from "@alea-drand/sdk"; function DrawButton() { const { connection } = useConnection(); const wallet = useWallet(); async function handleDraw() { if (!wallet.connected) return; const randomness = await getVerifiedRandomness({ connection, signer: wallet, // wallet-adapter Wallet shape; structurally compatible }); console.log(Buffer.from(randomness).toString("hex")); } } ``` The SDK detects browser wallets structurally (looks for `sendTransaction`), so it works with any wallet-adapter implementation without importing wallet-adapter types at runtime. `@solana/wallet-adapter-base` is an optional peer dependency. ### Error handling ```ts import { AleaError, ERRORS } from "@alea-drand/sdk"; try { await getVerifiedRandomness({ connection, signer }); } catch (err) { if (err instanceof AleaError) { console.error(`Alea ${err.code}: ${err.message}`); const humanName = ERRORS[err.code]; // humanName = "InvalidSignature: BLS signature verification failed", etc. } else { // Network error, RPC error, etc. — not an Alea error. throw err; } } ``` `err.code` is either an Alea custom program error code (the 6xxx range, defined by Alea's `AleaError` enum) or an Anchor framework code (2xxx/3xxx, e.g. `2001 ConstraintHasOne`). The SDK's `ERRORS` map includes both. Full table in [Errors reference](/docs/reference/errors). ### Cluster selection Connection URL determines the cluster, not the SDK. The program ID is cluster-agnostic — same vanity address on devnet and mainnet by design. ```ts import { Connection } from "@solana/web3.js"; import { getVerifiedRandomness, DEVNET_PROGRAM_ID } from "@alea-drand/sdk"; // Devnet (today) const devnet = new Connection("https://api.devnet.solana.com", "confirmed"); const r1 = await getVerifiedRandomness({ connection: devnet, signer }); // Mainnet (when live). Explicitly pass programId once MAINNET_PROGRAM_ID is set. const mainnet = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); const r2 = await getVerifiedRandomness({ connection: mainnet, signer, // programId: MAINNET_PROGRAM_ID, // currently throws on access — set post-deploy }); ``` `MAINNET_PROGRAM_ID` is exported as a Proxy that throws on any property access until the mainnet program deploys. Importing it is safe; using it before the mainnet release version of the SDK throws `Error: MAINNET_PROGRAM_ID not set`. ### SSR and bundlers The SDK is ESM-only and loads its Anchor IDL via `readFileSync` from a bundled JSON. That means it runs cleanly in Node-compatible environments — Vite, Next.js (Node runtime), esbuild, plain Node servers. It does NOT run on edge runtimes that lack `fs` — Cloudflare Workers, Vercel Edge, Deno Deploy. On those, either run the verify server-side and return the randomness to the edge, or fork the SDK and inline the IDL at build time via a bundler transform. ## Full API reference ### Core functions #### `getVerifiedRandomness` ```ts function getVerifiedRandomness(options: { connection: Connection; signer: Keypair | Wallet; programId?: PublicKey; commitment?: Commitment; round?: bigint; computeUnits?: number; }): Promise ``` Fetches the latest drand round (or the specified `round`), builds, signs, sends, and confirms the verify transaction. Returns 32 bytes of verified randomness. - `connection` — a `web3.js` Connection to the target cluster - `signer` — a `Keypair` or wallet-adapter `Wallet` that pays the tx fee - `programId` — override the Alea program ID. Defaults to `DEVNET_PROGRAM_ID`. - `commitment` — Solana commitment level. Defaults to `"confirmed"`. - `round` — specific drand round to verify. Defaults to current round. - `computeUnits` — override the CU budget. Defaults to `900_000`. #### `verifyDrandBeacon` ```ts function verifyDrandBeacon(args: { connection: Connection; signer: Keypair | Wallet; round: bigint; signature: Uint8Array; programId?: PublicKey; computeUnits?: number; }): Promise ``` Verifies a `(round, signature)` pair you already have. Same return / throwing behavior as `getVerifiedRandomness`; use this one when the beacon is already fetched. - `round` — drand round number as `bigint` - `signature` — 64-byte G1 point, uncompressed `(x || y)` big-endian ### Drand helpers #### `fetchBeacon` ```ts function fetchBeacon(round?: bigint): Promise ``` Fetches a drand evmnet beacon. Cycles through 5 endpoints (api.drand.sh + two mirrors + Cloudflare + a secureweb3 backup) with a 5-second per-request timeout. Up to 3 retry passes, so worst-case 15 individual HTTP attempts before giving up. If `round` is omitted, fetches the latest round. If a 404 comes back on "latest" (round not yet produced), backs off by one round and retries. Returns `{ round: bigint, signature: Uint8Array, unverifiedRandomness: string }`. The `unverifiedRandomness` field is drand's own hex string — **not on-chain verified**. Use `getVerifiedRandomness` or `verifyDrandBeacon` for trust. #### `getCurrentRound` ```ts function getCurrentRound(): bigint ``` Computes the current drand round number from `Date.now()`, genesis time, and period. Pure function, no I/O. Year-2086 safe (returns `bigint`). #### `getRoundAt` ```ts function getRoundAt(timestamp: bigint): bigint ``` Returns the drand round number active at a given Unix-seconds timestamp. Useful for anchoring a commit to a future round. #### `isRoundRecent` ```ts function isRoundRecent( round: bigint, config: { genesisTime: bigint; period: bigint }, clock: { unixTimestamp: bigint }, maxAgeSeconds: bigint, ): boolean ``` Pure, no I/O. Returns `true` if the round's drand-derived timestamp is within `maxAgeSeconds` of the given clock. Returns `false` for round 0, and for future rounds whose expected timestamp is past `clock.unixTimestamp`. Useful for pre-flight freshness checks in off-chain code. Behavior differs slightly from the Rust SDK's `is_round_recent` on future-dated rounds: the Rust version uses saturating arithmetic on `u64` and returns `true` when the round's timestamp is in the future (saturates to 0 age), while this TS version rejects future rounds explicitly. Either side catches stale beacons; future-round handling differs because the Rust version runs on-chain against Solana's own clock (which shouldn't see future rounds in practice) and the TS version is a pre-flight guard where future rounds indicate caller confusion. The on-chain authoritative check runs via `require!(alea_sdk::is_round_recent(...))` in your Rust program regardless — don't skip that step. ### Instruction helpers #### `createVerifyInstruction` ```ts function createVerifyInstruction(options: { round: bigint; signature: Uint8Array; programId?: PublicKey; }): TransactionInstruction ``` Low-level instruction builder. Returns a `TransactionInstruction` with the verify discriminator and data correctly encoded, and the Config PDA set as the first account key. **It does not include the payer signer key.** The on-chain `Verify` accounts struct expects `[config, payer]` in that order, so you must append the payer key yourself before sending: ```ts import { createVerifyInstruction } from "@alea-drand/sdk"; const ix = createVerifyInstruction({ round, signature }); ix.keys.push({ pubkey: payer.publicKey, isSigner: true, isWritable: false }); // now ix.keys is [config, payer] — the order Anchor expects ``` Use this when you're composing the verify into a multi-instruction transaction and need to control tx layout precisely. For most applications, `getVerifiedRandomness` or `verifyDrandBeacon` are simpler — they wire up keys, signer, compute budget, and retry logic end-to-end. A future SDK release will likely change this helper to accept a `payer: PublicKey` argument and append the key for you. #### `getConfigAddress` ```ts function getConfigAddress(programId?: PublicKey): PublicKey ``` Derives the Alea Config PDA from seeds `[Buffer.from("config")]` for a given program ID. Defaults to `DEVNET_PROGRAM_ID`. ### Constants | Name | Value | Purpose | |---|---|---| | `DRAND_CHAIN_HASH` | `"04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3"` | evmnet chain hash — explicit chain pin for drand requests | | `DRAND_GENESIS_TIME` | `1727521075` | Unix seconds for evmnet genesis (2024-09-28T13:37:55Z) | | `DRAND_PERIOD` | `3` | Beacon cadence in seconds | | `DRAND_ENDPOINTS` | `readonly string[]` (5 URLs) | Fallback endpoint list used by `fetchBeacon` | | `DEVNET_PROGRAM_ID` | `PublicKey("ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U")` | Devnet program ID (same vanity used for mainnet) | | `MAINNET_PROGRAM_ID` | `PublicKey` (throwing Proxy) | Placeholder until mainnet release. Accessing any property throws until the SDK ships with the live mainnet ID set. | ### Types #### `DrandBeacon` ```ts interface DrandBeacon { round: bigint; signature: Uint8Array; // 64 bytes, uncompressed G1 unverifiedRandomness: string; // hex — NOT on-chain verified } ``` #### `DrandConfig` ```ts interface DrandConfig { genesisTime: bigint; period: bigint; } ``` Shape-compatible with the subset of on-chain `Config` fields used by `isRoundRecent`. #### `SolanaClock` ```ts interface SolanaClock { unixTimestamp: bigint; } ``` Shape-compatible with `solana-web3.js`'s Clock sysvar. #### `BeaconResult` ```ts interface BeaconResult { round: bigint; signature: Uint8Array; unverifiedRandomness: string; } ``` Alias for `DrandBeacon`, kept for future public-API extension. #### `VerifyOptions` ```ts interface VerifyOptions { programId?: PublicKey; computeUnits?: number; } ``` ### Errors #### `AleaError` ```ts class AleaError extends Error { code: number; constructor(code: number, message: string); } ``` Thrown by both core functions when the on-chain program reverts or when the SDK can't extract return data. #### `ERRORS` ```ts const ERRORS: Record ``` Maps error codes the SDK knows about to human-readable descriptions. Used internally to produce `AleaError.message`: | Code | Description | |---|---| | 2001 | `ConstraintHasOne`: Signer is not the config authority (Anchor framework) | | 6000 | `InvalidSignature`: BLS signature verification failed | | 6001 | `InvalidG1Point`: Signature bytes are not a valid G1 point | | 6002 | `RoundZero`: Round number must be greater than 0 | | 6003 | `InvalidFieldElement`: Field element out of valid range (reserved, unreachable) | | 6004 | `NoSquareRoot`: Square root does not exist (infrastructure) | | 6005 | `InvalidG2Point`: Public key bytes are not a valid G2 point (reserved, unreachable) | | 6006 | `PairingError`: `alt_bn128_pairing` syscall failed | | 6007 | `WrongChainHash`: chain_hash does not match evmnet (init-time only) | | 6008 | `WrongPubkey`: pubkey_g2 does not match evmnet (init-time only) | | 6009 | `ReturnDataMissing`: TS-SDK-only. Thrown when `getTransaction().meta.returnData` is absent after a successful send (indexer lag, stripped RPC response). The on-chain program doesn't emit this code directly. | The v0.2.0 TS SDK's `ERRORS` map ends at 6009. The on-chain program added 6010 `InvalidGenesisTime`, 6011 `InvalidPeriod`, and 6012 `UnauthorizedInit` in later program releases — all init-time guards that don't fire during `verify`. A future SDK release will extend the map. See [Errors reference](/docs/reference/errors) for the canonical table. A reverting verify from your consumer code most commonly throws 6000 (bad signature) or 6002 (zero round). 6004/6006 are infrastructure-level and shouldn't appear for drand-issued beacons. ## What the SDK handles internally The SDK loads its Anchor IDL via `readFileSync` rather than an `import ... with { type: "json" }` assertion. This sidesteps the Node 18/22 syntax split entirely — no import-assertion version headaches across runtime targets. Anchor 0.30.1 is incompatible with web3.js 1.98+ on the error-handling path: Anchor's `.rpc()` loses custom error codes against modern web3.js. The SDK builds transactions via Anchor but sends them with raw `connection.sendRawTransaction`, then reads errors from `getTransaction().meta.err`. See [Common Pitfalls §1](/docs/sdks/common-pitfalls). `skipPreflight: true` is required for Alea verifies because the pairing syscall's CU usage can outpace preflight simulation's blockhash window under high-CU load. The SDK sets it by default. Helius indexer lag is real. After `sendRawTransaction` returns, `getTransaction()` can return `null` for 2–5 seconds while the RPC indexer catches up. The SDK retries up to 15 times with 1-second backoff before giving up. See [Common Pitfalls §4](/docs/sdks/common-pitfalls). The SDK also prepends `ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 })` to every verify transaction. Override the limit via the `computeUnits` option if you need headroom for additional on-chain work in the same tx. ## Related - [Rust SDK](/docs/sdks/rust) — on-chain CPI side - [CPI Integration Guide](/docs/sdks/cpi-integration) — full lottery walkthrough - [Common Pitfalls](/docs/sdks/common-pitfalls) — integration footguns - [Errors reference](/docs/reference/errors) — Cmd+F lookup --- SOURCE: https://alea.so/docs/sdks/cpi-integration # CPI Integration Guide > A working lottery program that buys tickets, settles via Alea-verified randomness, and pays the winner. A full integration end-to-end: a small lottery program on-chain, a TypeScript client that fetches drand and submits the settle. Runnable against devnet. Reference implementation: [`example-lottery/`](https://github.com/alea-drand/alea/tree/main/example-lottery) on GitHub. ## What we're building A single-round lottery: 1. Anyone can call `buy_ticket(lamports)` to put SOL into the prize pool 2. After a deadline, anyone can call `settle(round, signature)` which: - Verifies the drand round on-chain via Alea CPI - Uses the 32-byte randomness to pick a winning ticket index - Transfers the prize to that ticket's holder 3. No admin, no oracle, no operator. Whoever submits the valid round on time pays gas; the lottery participants share the entropy guarantee. ## On-chain program ### State Two accounts matter: `Lottery` holds state, and a separate `pool` System-owned PDA holds the prize SOL. Keeping state and lamports in separate accounts is the idiomatic Anchor pattern — System Program transfers require the destination to be System-owned, and Anchor `#[account]` types are owned by your program. ```rust use anchor_lang::prelude::*; use alea_sdk::{self, AleaVerifier}; declare_id!("11111111111111111111111111111111"); // placeholder — replace with your program's declare_id #[account] pub struct Lottery { pub deadline: i64, // unix timestamp after which settle is allowed pub ticket_count: u64, // monotonic counter pub prize_lamports: u64, // tracks the expected balance of `pool` pub winner: Option, // None until settled pub bump: u8, pub pool_bump: u8, // bump for the prize-pool PDA } #[account] pub struct Ticket { pub lottery: Pubkey, pub holder: Pubkey, pub idx: u64, pub bump: u8, } ``` The prize pool is a PDA with no data, owned by the System Program, derived from `[b"pool", lottery.key().as_ref()]`. Lamports transfer in via `system_program::transfer` on `buy_ticket`, and out via the program signing with seeds on `settle`. ### `buy_ticket` ```rust #[program] pub mod lottery { use super::*; pub fn buy_ticket(ctx: Context, lamports: u64) -> Result<()> { let lottery = &mut ctx.accounts.lottery; require!( Clock::get()?.unix_timestamp < lottery.deadline, LotteryError::Closed ); require!(lamports > 0, LotteryError::ZeroAmount); // Transfer from buyer to the System-owned pool PDA. The destination // must be System-owned for system_program::transfer to succeed. anchor_lang::system_program::transfer( CpiContext::new( ctx.accounts.system_program.to_account_info(), anchor_lang::system_program::Transfer { from: ctx.accounts.buyer.to_account_info(), to: ctx.accounts.pool.to_account_info(), }, ), lamports, )?; let ticket = &mut ctx.accounts.ticket; ticket.lottery = lottery.key(); ticket.holder = ctx.accounts.buyer.key(); ticket.idx = lottery.ticket_count; ticket.bump = ctx.bumps.ticket; lottery.ticket_count += 1; lottery.prize_lamports += lamports; Ok(()) } } #[derive(Accounts)] pub struct BuyTicket<'info> { #[account(mut)] pub lottery: Account<'info, Lottery>, /// CHECK: System-owned prize pool PDA. Receives SOL only; no data. #[account( mut, seeds = [b"pool", lottery.key().as_ref()], bump = lottery.pool_bump, )] pub pool: AccountInfo<'info>, #[account( init, payer = buyer, space = 8 + 32 + 32 + 8 + 1, seeds = [b"ticket", lottery.key().as_ref(), lottery.ticket_count.to_le_bytes().as_ref()], bump, )] pub ticket: Account<'info, Ticket>, #[account(mut)] pub buyer: Signer<'info>, pub system_program: Program<'info, System>, } ``` ### `settle` — this is where Alea enters ```rust pub fn settle(ctx: Context, round: u64, signature: [u8; 64]) -> Result<()> { let lottery = &mut ctx.accounts.lottery; // Preconditions require!(lottery.winner.is_none(), LotteryError::AlreadySettled); require!(Clock::get()?.unix_timestamp >= lottery.deadline, LotteryError::TooEarly); require!(lottery.ticket_count > 0, LotteryError::NoTickets); // Freshness: drand round must be within 60s of settle-time require!( alea_sdk::is_round_recent( round, &ctx.accounts.alea_config, &ctx.accounts.clock, 60, ), LotteryError::StaleBeacon, ); // CPI to Alea. Returns the 32-byte verified randomness as a raw array. 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, )?; // Capture FIRST, then do any other CPI. Return data is single-slot. let r = u64::from_le_bytes(randomness[0..8].try_into().unwrap()); let winner_idx = r % lottery.ticket_count; let winning_ticket = &ctx.accounts.winning_ticket; require!(winning_ticket.idx == winner_idx, LotteryError::WrongTicketAccount); require!(winning_ticket.lottery == lottery.key(), LotteryError::WrongLottery); // Transfer the whole pool balance to the winner. The pool is a System-owned // PDA, so we sign with its seeds and use System Program transfer. let lottery_key = lottery.key(); let seeds: &[&[u8]] = &[b"pool", lottery_key.as_ref(), &[lottery.pool_bump]]; let prize = lottery.prize_lamports; anchor_lang::system_program::transfer( CpiContext::new_with_signer( ctx.accounts.system_program.to_account_info(), anchor_lang::system_program::Transfer { from: ctx.accounts.pool.to_account_info(), to: ctx.accounts.winner.to_account_info(), }, &[seeds], ), prize, )?; lottery.winner = Some(winning_ticket.holder); lottery.prize_lamports = 0; Ok(()) } #[derive(Accounts)] pub struct Settle<'info> { #[account(mut)] pub lottery: Account<'info, Lottery>, /// CHECK: System-owned prize pool PDA, signed into by the program. #[account( mut, seeds = [b"pool", lottery.key().as_ref()], bump = lottery.pool_bump, )] pub pool: AccountInfo<'info>, #[account( constraint = winning_ticket.lottery == lottery.key() @ LotteryError::WrongLottery, )] pub winning_ticket: Account<'info, Ticket>, /// CHECK: lamport-receiver; validated equal to winning_ticket.holder #[account(mut, constraint = winner.key() == winning_ticket.holder @ LotteryError::WrongWinner)] pub winner: AccountInfo<'info>, // Alea accounts. The seeds::program constraint on alea_config is required // — without it, a fake Config owned by a different program can be substituted. // bump = alea_config.bump reuses Alea's stored canonical bump, saving ~10K CU. pub alea_program: Program<'info, AleaVerifier>, #[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>, pub system_program: Program<'info, System>, } #[error_code] pub enum LotteryError { #[msg("lottery is closed for ticket purchases")] Closed, #[msg("ticket cost must be greater than zero")] ZeroAmount, #[msg("lottery has already been settled")] AlreadySettled, #[msg("settle called before deadline")] TooEarly, #[msg("no tickets sold; settle would divide by zero")] NoTickets, #[msg("drand round is older than 60 seconds")] StaleBeacon, #[msg("ticket idx does not match computed winner idx")] WrongTicketAccount, #[msg("ticket belongs to a different lottery")] WrongLottery, #[msg("winner account does not match ticket holder")] WrongWinner, } ``` ## TypeScript client ```ts import { Connection, Keypair, PublicKey, Transaction, ComputeBudgetProgram, SYSVAR_CLOCK_PUBKEY, } from "@solana/web3.js"; import { AnchorProvider, Program, Wallet, BN } from "@coral-xyz/anchor"; import { fetchBeacon, getCurrentRound, getConfigAddress, DEVNET_PROGRAM_ID as ALEA_ID, } from "@alea-drand/sdk"; import { IDL as LotteryIDL } from "./idl/lottery"; const connection = new Connection(process.env.RPC_URL!, "confirmed"); const settler = Keypair.fromSecretKey(/* your keypair bytes */); const provider = new AnchorProvider(connection, new Wallet(settler), {}); const lottery = new Program(LotteryIDL, provider); const lotteryPda = /* your derived lottery PDA */; const lotteryState = await (lottery.account as any).lottery.fetch(lotteryPda); // 1. Fetch the latest drand round (past the lottery deadline) const round = getCurrentRound(); const beacon = await fetchBeacon(round); // 2. Compute winner index off-chain so we can pass in the right ticket PDA. // Must match the on-chain math exactly: u64::from_le_bytes(rand[0..8]) % count. // You won't know the verified randomness until after settle runs on-chain, // BUT for the client-side ticket-account lookup you can use the drand // unverifiedRandomness (which equals the verified output for valid sigs). // Note: Anchor deserializes Rust snake_case to camelCase in TS. const unverified = Buffer.from(beacon.unverifiedRandomness, "hex"); const r = unverified.readBigUInt64LE(0); const winnerIdx = Number(r % BigInt(lotteryState.ticketCount)); // Derive the winning ticket PDA and the prize-pool PDA const [winningTicketPda] = PublicKey.findProgramAddressSync( [ Buffer.from("ticket"), lotteryPda.toBuffer(), Buffer.from(new BigUint64Array([BigInt(winnerIdx)]).buffer), ], lottery.programId, ); const [poolPda] = PublicKey.findProgramAddressSync( [Buffer.from("pool"), lotteryPda.toBuffer()], lottery.programId, ); const winningTicket = await (lottery.account as any).ticket.fetch(winningTicketPda); // 3. Build settle tx with Alea's required compute budget const configPda = getConfigAddress(ALEA_ID); const { SystemProgram } = await import("@solana/web3.js"); const settleIx = await (lottery.methods as any) .settle(new BN(beacon.round.toString()), Array.from(beacon.signature)) .accounts({ lottery: lotteryPda, pool: poolPda, winningTicket: winningTicketPda, winner: winningTicket.holder, aleaProgram: ALEA_ID, aleaConfig: configPda, payer: settler.publicKey, clock: SYSVAR_CLOCK_PUBKEY, systemProgram: SystemProgram.programId, }) .instruction(); const tx = new Transaction() .add(ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 })) .add(settleIx); tx.recentBlockhash = (await connection.getLatestBlockhash("confirmed")).blockhash; tx.feePayer = settler.publicKey; tx.sign(settler); // skipPreflight: true — pairing CU can outrun preflight's blockhash window const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true }); await connection.confirmTransaction(sig, "confirmed"); ``` ## Integration notes Every transaction that calls Alea via CPI needs `ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 })` as the first instruction. Alea's verify is ~407K CU (worst case ~454K), the Solana default is 200K, and you need room for your own settle work on top. Sending with `skipPreflight: true` is required. The pairing call's CU usage can outpace Solana's preflight blockhash window, so preflight rejects the tx before it ever hits the leader. On-chain execution is identical either way — just skip the simulation. Error-format implications are in [Common Pitfalls §5](/docs/sdks/common-pitfalls). Anyone can call `settle` once the deadline passes. The first valid transaction wins gas, subsequent calls revert with `AlreadySettled`. This is deliberate — no keeper required. If your app can't tolerate "anyone can trigger," gate settle behind a permissioned authority. About the ticket-account lookup: this example pre-computes the winner index client-side using drand's `unverifiedRandomness` field, which equals the verified on-chain output for any valid signature (both are `sha256(signature_bytes)`). Safe here because the on-chain settle re-checks `winning_ticket.idx == winner_idx` before paying out. If you lift this pattern into another program, keep the on-chain constraint — without it, the client can lie and the program has no defense. For a general PDA-iteration approach, use `remaining_accounts`. One last thing. `cpi::verify` returns a raw `[u8; 32]`. Read it into a local variable before any subsequent CPI — Solana return data is single-slot, any later CPI overwrites Alea's output. See [Rust SDK §Return-data ordering footgun](/docs/sdks/rust#return-data-ordering-footgun). ## Related - [Reference implementation](https://github.com/alea-drand/alea/tree/main/example-lottery) - [Common Pitfalls](/docs/sdks/common-pitfalls) — integration footguns - [Rust SDK](/docs/sdks/rust) and [TypeScript SDK](/docs/sdks/typescript) — full API surface --- SOURCE: https://alea.so/docs/sdks/common-pitfalls # Common Pitfalls > Sharp-edged integration footguns. Read this before debugging. The footguns below bit us during integration and shipped-SDK development. Anchor 0.30.1 stopped working cleanly with modern web3.js mid-build. `constant_time_eq` pushed its MSRV past Solana's bundled rustc. The pairing syscall blows past the 200K CU default. Helius indexers lie about confirmation for a few seconds. None of these are Alea bugs, but every one of them will cost you an afternoon if you hit it cold. ## Decision tree — verify reverts with error N Paste the error code from your transaction logs into this tree: - **6000 `InvalidSignature`** — the signature fails pairing. Almost always: wrong signature bytes for the round, OR fetched from a different drand chain. Check `fetchBeacon(round)` is hitting the evmnet chain (default). Don't mix mainnet drand beacons (BLS12-381) with Alea (BN254). - **6001 `InvalidG1Point`** — the signature isn't a valid G1 point. Check it's 64 bytes uncompressed `(x || y)` big-endian, not a 48-byte compressed form or a 96-byte G2 signature. - **6002 `RoundZero`** — you passed round 0. Drand rounds start at 1. - **6003 `InvalidFieldElement` / 6004 `NoSquareRoot` / 6005 `InvalidG2Point` / 6006 `PairingError`** — infrastructure-level errors. Should not happen for drand-issued beacons. If you see these in prod with a genuine drand signature, file an issue with the round number. - **6007 `WrongChainHash`** — you're passing a signature from a different drand chain. `DRAND_CHAIN_HASH` in the SDK is evmnet. Switch to evmnet or stop. - **6008 `WrongPubkey`** — the group public key used for signing doesn't match `Config.pubkey_g2`. Indicates Alea's Config has been reinitialized with a wrong key (should not happen without a governance failure) OR you're signing against a non-drand BLS key. - **6009 `ReturnDataMissing`** — TypeScript-SDK-only. Thrown when `getTransaction().meta.returnData` is absent after a successful send, usually due to indexer lag or a stripped RPC response. Not emitted by the on-chain program. Return-data ordering footgun is still real on the Rust side — read `cpi::verify`'s result into a local BEFORE any subsequent CPI — but it manifests there as wrong bytes, not this error code. - **Anchor wrapper codes (2000-series)** — `ConstraintSeeds` usually means `seeds::program = alea_program.key()` is missing or wrong; `AccountNotInitialized` means the Config PDA doesn't exist on the cluster you're connected to. For a Cmd+F lookup table, see [Errors reference](/docs/reference/errors). ## 1. Anchor 0.30.1 + web3.js ≥ 1.98 — `.rpc()` destroys error codes Calling `program.methods.verify(...).rpc()` (the convenient Anchor method) throws `Error: Unknown action 'undefined'` against any web3.js version 1.98 or later. Every error code is destroyed along the way. Anchor's `AnchorProvider.sendAndConfirm` constructs `new SendTransactionError(message, logs)` using the old positional 2-argument signature. web3.js 1.98 changed that constructor to object destructuring (`{action, signature, transactionMessage, logs}`), so passing a string falls through every switch branch and lands at the default `"Unknown action 'undefined'"`. The actual on-chain error code is lost. The SDK already works around this. If you're rolling your own: build via `.transaction()`, sign manually, send with `connection.sendRawTransaction({skipPreflight: true})`, and read errors from `getTransaction().meta.err` (raw Solana `{InstructionError: [_, {Custom: N}]}` shape). If you wrap Alea in your own Anchor CPI from another program, this doesn't apply. CPI from an on-chain program is unaffected by Anchor's `.rpc()` wrapper. ## 2. Solana BPF rustc lag — `constant_time_eq` MSRV Building a fresh project that depends on `alea-sdk` with `cargo build-sbf` may fail: ``` error: package `constant_time_eq v0.4.3` cannot be built because it requires rustc 1.95.0 or newer ``` Solana's `cargo-build-sbf` ships with an embedded rustc around 1.89-dev. The transitive dep `constant_time_eq` bumped its MSRV to 1.95 in 0.4.3. Public Rust is several minor versions ahead; Solana's BPF toolchain is behind. Pin the transitive dep in your project's `Cargo.toml`: ```toml [dependencies] constant_time_eq = "=0.4.2" ``` Until Solana's BPF rustc catches up (no ETA), every BPF consumer of any crate depending on `constant_time_eq` needs this pin, not just Alea. ## 3. Compute budget — set 900K, not the default 200K Alea's verify is ~407K CU. The Solana default per-instruction limit is 200K. A naive `program.methods.verify(...).rpc()` (or any send without explicit CU budget) fails with `Computational budget exceeded`. Add `ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 })` as the first instruction in your transaction. The SDK's `verifyDrandBeacon` injects this automatically; if you're building the transaction yourself, do it explicitly. ```ts import { ComputeBudgetProgram, Transaction } from "@solana/web3.js"; const tx = new Transaction() .add(ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 })) .add(verifyInstruction); ``` 900K leaves room for state writes and a CPI or two. Heavy settle with multiple transfers? Bump to 1,200K. ## 4. Helius indexer lag — `getTransaction` can return null for seconds After `sendRawTransaction` returns and `confirmTransaction` resolves, calling `getTransaction(sig, { commitment: "confirmed" })` on Helius RPC sometimes returns `null` for 2 to 5 seconds. The transaction is on-chain; the indexer hasn't caught up. Retry `getTransaction` in a loop. The SDK does up to 15 retries with 1-second sleeps. If you're building your own confirmation, do the same: ```ts let info = null; for (let attempt = 0; attempt < 15; attempt++) { info = await connection.getTransaction(sig, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); if (info) break; await new Promise((r) => setTimeout(r, 1000)); } if (!info) throw new Error("getTransaction never returned"); ``` Not Helius-specific. Every RPC indexer lags under load. 15 retries covers the worst we've seen on devnet. ## 5. Skip preflight — error format changes By default, `sendRawTransaction` runs Solana preflight simulation client-side and Anchor wraps the resulting failure in its own error type. With `{skipPreflight: true}` (required for Alea verifies — see §3), failures come back as raw Solana `InstructionError` instead. **The shape difference:** ```ts // With preflight (default): { name: "AnchorError", errorCode: { number: 6000, name: "InvalidSignature" }, } // With skipPreflight: true: // (send call doesn't throw an Anchor error — the failure lives on tx confirmation) info.meta.err = { InstructionError: [0, { Custom: 6000 }] } ``` Always read post-send errors from `getTransaction().meta.err`. Parse for the `{InstructionError: [ixIdx, {Custom: N}]}` shape and map the custom code to the SDK's `ERRORS` map. The SDK does this internally; if you're sending custom transactions, replicate the pattern. ## 6. Signature byte order — 64 uncompressed bytes, not 65, not compressed Drand publishes BLS signatures as 64 bytes: a single G1 elliptic-curve point in uncompressed `(x || y)` format, big-endian. Some BLS libraries default to 96-byte uncompressed G2 or 48-byte compressed G1. Alea expects 64 uncompressed G1 bytes. When fetching from `api.drand.sh//public/` you get hex; decode to bytes; the result is 64 bytes. The SDK's `fetchBeacon` does this for you. If you're parsing signatures from another source, validate `signature.length === 64` before passing to the SDK. Catch it upstream, or eat a 6001 `InvalidG1Point` from the on-chain program. ## 7. evmnet vs mainnet drand chains Drand publishes several chains in parallel: mainnet (BLS12-381), evmnet (BN254), fastnet, quicknet. Alea verifies **evmnet only**. Signatures from other chains will not verify against Alea's configured group public key and will revert with 6007 `WrongChainHash` or 6000 `InvalidSignature` depending on which check fails first. **Default endpoint.** `https://api.drand.sh/public/latest` used to default to mainnet (not evmnet). Use the explicit chain hash to be safe: ``` https://api.drand.sh/04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3/public/latest ``` The chain hash is exported as `DRAND_CHAIN_HASH` in the TypeScript SDK and embedded in `Config.chain_hash` on-chain. `fetchBeacon` uses the evmnet chain by default. ## 8. Fake Config substitution — omitting `seeds::program` ships an exploitable program If your consumer program embeds the two Alea Accounts fields directly (not the SDK's reference struct) and forgets the `seeds::program = alea_program.key()` constraint, an attacker can pass a Config-shaped account owned by a program they control, feed attacker-chosen public keys into the pairing, and make Alea "verify" anything they want. This is full compromise of your randomness consumer. Alea cannot guard against this from its own side — the Config account is passed in by the caller and the on-chain program trusts whatever it gets. The Rust SDK's [`AleaVerify`](https://github.com/alea-drand/alea/blob/main/sdk/rust/src/accounts.rs) reference struct includes the constraint; if you hand-copy the two fields, include it manually: ```rust #[account( seeds = [b"config"], bump = alea_config.bump, seeds::program = alea_program.key(), // ← DO NOT OMIT )] pub alea_config: Account<'info, alea_sdk::Config>, ``` Any consumer program that ships without this constraint is exploitable. Grep your Accounts structs before deploy. ## 9. Round freshness — Alea won't reject stale rounds Alea verifies that a signature is mathematically valid for a given round. It does NOT check whether that round is recent. An adversary can submit a valid drand round from last year and Alea will happily verify it. Use the Rust SDK's `is_round_recent(round, config, clock, max_age_seconds)` helper as a `require!()` at the start of your instruction: ```rust require!( alea_sdk::is_round_recent( round, &ctx.accounts.alea_config, &ctx.accounts.clock, 30, ), MyError::StaleBeacon, ); ``` 30s is the default. 3s (one drand round) if you're MEV-adjacent. Skip the check and you have zero replay defense — Alea won't save you there. ## Related - [Architecture](/docs/concepts/architecture) — why the primitives are shaped this way - [Security model](/docs/concepts/security-model) — what Alea defends against vs not - [Rust SDK](/docs/sdks/rust) and [TypeScript SDK](/docs/sdks/typescript) — full API references - [Errors reference](/docs/reference/errors) — error code table --- SOURCE: https://alea.so/docs/security/testing # Testing & guarantees > What Alea has been tested against, with artifacts per claim. And what hasn't been done. Testing baseline: fuzz coverage across five targets, cross-verification against published drand reference implementations, and a reproducible build SHA. Each claim below points to the artifact that backs it. ## Scope Testing covers the on-chain Anchor program (`programs/alea-verifier/`) at binary SHA `8965062489fdcdbb538597545fc6692f3f580d770d34f2d42000a70560984b1c`. The same binary is deployed to devnet at [`ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U`](https://explorer.solana.com/address/ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U?cluster=devnet). Mainnet will deploy the same binary (or a superset with remediations applied) and the SHA will be published on deploy day. The SDKs ([`alea-sdk`](https://crates.io/crates/alea-sdk) Rust + [`@alea-drand/sdk`](https://www.npmjs.com/package/@alea-drand/sdk) TypeScript) are tested separately via unit tests in each crate. ## Fuzzing 24 cumulative CPU-hours across five `cargo-fuzz` (libFuzzer) targets. Targets live at [`programs/alea-verifier/fuzz/fuzz_targets/`](https://github.com/alea-drand/alea/tree/main/programs/alea-verifier/fuzz/fuzz_targets). Ground truth for what each target actually exercises — below matches the source rather than what we'd like to be fuzzing: | Target | What it fuzzes | |---|---| | `verify_beacon.rs` | End-to-end verify pipeline with fuzzer-controlled `(round, signature)` pairs against `EXPECTED_EVMNET_PUBKEY` (pubkey is NOT fuzzed — Alea only verifies against drand evmnet). Invariant: the pipeline must never panic on any input. | | `hash_to_g1.rs` | SVDW hash-to-curve with two input shapes: `Round(u64)` and `RawMessage(Vec)` up to 1024 bytes. Invariants: no panic; if `hash_to_g1` / `hash_round_to_g1` returns `Ok`, the output must be on the BN254 G1 curve. | | `on_curve_g1.rs` | CVE-2025-30147 regression target. Runs Alea's `on_curve_g1` against an independent arkworks-based curve check on every 64-byte input and asserts byte-for-byte agreement. Catches any reordering of the canonical-form check against the curve-equation check. | | `hash_to_field_canonicity.rs` | RFC 9380 §5 canonicity of `hash_to_field`. Random 32-byte inputs; asserts both returned field elements are strictly below the BN254 modulus and round-trip to ≤32 canonical bytes. | | `pairing_buffer_parses.rs` | Panic-freedom on attacker-controlled G1 bytes for `on_curve_g1` and `negate_g1`, plus the invariant that negation preserves curve membership. Does NOT invoke the pairing syscall — that path is covered by `verify_beacon.rs` end-to-end. | Corpora seeded from real drand fixtures cross-imported from [`@kevincharm/noble-bn254-drand`](https://github.com/kevincharm/noble-bn254-drand) test vectors. Campaign artifacts (corpus directories + libFuzzer run logs) are archived locally; published release tarballs are on the roadmap. Ask via [responsible disclosure](/docs/security/disclosure) if you need them for an independent review. ## Cross-verification Test vectors at drand rounds 1 and 9,337,227 cross-verified against three independent reference implementations: | Reference | Location | Purpose | |---|---|---| | [`@kevincharm/noble-bn254-drand`](https://github.com/kevincharm/noble-bn254-drand) | TypeScript, Noble Cryptography ecosystem | Primary cross-check against the Solidity-ancestor reference | | [`arkworks-rs/algebra`](https://github.com/arkworks-rs/algebra) | Rust | Academic-grade field / curve / pairing library | | Drand REST API | `https://api.drand.sh//public/` | drand's own published outputs | All three agree with Alea on the 32-byte randomness (`sha256(signature_bytes)`) for both fixture rounds. Any divergence surfaces as a CI failure on every commit. Test source: [`verify.rs` integration tests](https://github.com/alea-drand/alea/blob/main/programs/alea-verifier/src/instructions/verify.rs#L123-L153). ## End-to-end verified on devnet | Test | Result | |---|---| | Drand round 1 (genesis evmnet) | Verifies on live devnet program, returns correct 32-byte randomness | | Drand round 9,337,227 (arbitrary later round) | Same | | Either round with any signature byte altered | Reverts with `InvalidSignature` (6000) | | Config PDA content check | Stored evmnet group public key matches drand-published value | Test files: [`sdk/rust/tests/devnet_verify.rs`](https://github.com/alea-drand/alea/blob/main/sdk/rust/tests/devnet_verify.rs), [`sdk/typescript/tests/devnet_integration.test.ts`](https://github.com/alea-drand/alea/blob/main/sdk/typescript/tests/devnet_integration.test.ts). Upgrade-and-redeploy continuity (preserving Config PDA across program upgrades) is a property consumers care about but is not covered by a repro-able CI test today. On the roadmap. ## What has NOT been done No external third-party audit. No firm has formally reviewed the code — the testing baseline above is what exists, there is no audit report to cite, and mainnet will ship without one. No formal verification. The pairing math is not mechanically proven. Alea relies on the correctness of `ark-bn254` and Solana's `alt_bn128_pairing` syscall. No published coverage report. Line, branch, and mutation numbers haven't been measured under a fixed toolchain; no lcov or mutants report in the repo. On the roadmap. Also absent: - Mainnet behavior under real load (Alea is not yet on mainnet) - Constant-time characterization — the program's signature operations use arkworks primitives, which haven't been characterized for constant-time properties on Solana's runtime. Side-channel attacks against an open `verify(round, signature)` function are inherently low-impact (no secret to leak), but noted for completeness. - Adversarial drand-network tests. Testing assumes drand publishes honestly; a compromised drand federation is out of scope for any drand consumer. ## Reproducing the build Deterministic given the pinned toolchain in repo root: ```bash git clone https://github.com/alea-drand/alea cd alea # rust-toolchain.toml pins the toolchain; cargo-build-sbf uses its embedded rustc cargo build-sbf --workspace --release sha256sum target/deploy/alea_verifier.so # 8965062489fdcdbb538597545fc6692f3f580d770d34f2d42000a70560984b1c ``` If your hash differs, check `rustc --version`, `cargo-build-sbf --version`, and your Solana CLI version match the repo's pinned toolchain. Non-deterministic builds would be worth a bug report. ## Reproducing the fuzz runs ```bash cd programs/alea-verifier/fuzz cargo +nightly fuzz run verify_beacon -- -max_total_time=3600 cargo +nightly fuzz run hash_to_g1 -- -max_total_time=3600 cargo +nightly fuzz run on_curve_g1 -- -max_total_time=3600 cargo +nightly fuzz run hash_to_field_canonicity -- -max_total_time=3600 cargo +nightly fuzz run pairing_buffer_parses -- -max_total_time=3600 ``` 24 cumulative CPU-hours across all five is the published baseline. Longer runs are welcome; new findings should be reported via [responsible disclosure](/docs/security/disclosure). ## Related - [Security model](/docs/concepts/security-model) — threat boundary - [Responsible disclosure](/docs/security/disclosure) — how to report findings - [Cluster addresses](/docs/reference/cluster-addresses) — canonical binary SHA, deploy tx --- SOURCE: https://alea.so/docs/security/disclosure # Responsible disclosure > How to report a vulnerability in Alea. Contact, expectations, timeline. ## Contact Primary: [GitHub Security Advisory](https://github.com/alea-drand/alea/security/advisories/new) — the preferred channel. Creates a private thread with the maintainers and tracks the fix through disclosure. Fallback: [security@alea.so](mailto:security@alea.so). Cloudflare Email Routing forwards to a personal inbox. If you don't get an acknowledgment within 72 hours, ping the GitHub Security tab — that's the redundant channel. No paid bug bounty currently. If you want acknowledgment for a finding, we will publish your name and the vulnerability description (with your permission) once the fix has been deployed and verified. ## What to send A reproducible bug report. Ideal contents: - The commit SHA or release tag you're testing against - A minimal reproduction: a Rust test, a TypeScript script, a transaction signature on devnet - Your assessment of impact and severity — we will likely revise; your initial take helps prioritize - Whether you've already disclosed elsewhere Do not include exploit code that targets the live devnet program in public channels. The vulnerable behavior is enough; we'll write our own test cases. ## Disclosure timeline Standard 90-day window from acknowledgment: | Day | Action | |---|---| | 0 | You submit via GitHub Security Advisory or email | | 1–3 | We acknowledge receipt and confirm we can reproduce | | 4–14 | We propose a remediation approach and discuss with you | | 15–60 | We implement, test, review, prepare release | | 61–80 | We coordinate disclosure window with you; pre-announce to integrators if needed | | 81–90 | Public disclosure: GitHub Security Advisory + commit + release notes | Critical bugs (e.g., the on-chain program can be made to verify forged signatures) compress this timeline aggressively — hours, not weeks, between ack and remediation. Low-severity bugs can extend by mutual agreement. ## Out of scope These are not vulnerabilities in Alea: - **Drand network compromise.** Flaws in drand itself (e.g., the threshold-BLS group can be reduced below the t-of-n threshold) belong to drand, not Alea. Report to the [drand security contact](https://drand.love/security). We are downstream. - **Solana network issues.** Validator censorship, RPC misbehavior, MEV on your transactions. Report to the relevant Solana actor. We can help coordinate if the issue intersects Alea's behavior. - **`alt_bn128_pairing` syscall bugs.** Solana protocol bugs. Report to the [Solana Security team](https://solana.org/security) — and let us know so we can pause integrations while it's patched. - **Application-level bugs in programs that integrate Alea.** If your lottery has a bug, file with the lottery's team, not us. The exception: if the bug stems from an Alea API that makes misuse too easy to be accidental, send it to us — the API needs documentation or a guard-rail. - **DoS via legitimate transactions.** Alea has no rate limit and accepts any valid `(round, signature)` input. Submitting many verify calls is not a vulnerability; it's the intended interface. ## What IS in scope Anything that: - Makes the on-chain verifier accept a signature it should reject (forgery) - Makes the on-chain verifier reject a signature it should accept (denial) - Makes the SDK return wrong randomness, or randomness that doesn't match what the on-chain program returned - Lets an attacker bias randomness by manipulating Alea (not drand, not Solana) - Crashes the program or pushes compute past the documented ~407K envelope - Bypasses the on-chain `Config.authority` or upgrade-authority gates before the planned multisig transfer If you're unsure whether something is in scope, send it. We'd rather decline politely than miss something. ## Acknowledgments Reporters with credit (opt-in) are listed in release notes and below. Currently empty — be the first. --- SOURCE: https://alea.so/docs/reference/cluster-addresses # Cluster addresses > Canonical addresses, PDAs, binary SHAs, and drand chain constants for each Solana cluster. ## Devnet — live | Field | Value | |---|---| | Program ID | [`ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U`](https://explorer.solana.com/address/ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U?cluster=devnet) | | Config PDA | [`6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8`](https://explorer.solana.com/address/6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8?cluster=devnet) | | Config PDA seeds | `[b"config"]` | | Loader | `BPFLoaderUpgradeab1e11111111111111111111111` | | BPFLoader upgrade authority | Single deployer keypair today. Planned transfer to Squads multisig and eventual immutability, no fixed timeline. | | `Config.authority` | Same deployer keypair. Gates `update_config`; persistent (not init-only). Planned rotation alongside the upgrade-authority transfer. | | Binary SHA-256 | `8965062489fdcdbb538597545fc6692f3f580d770d34f2d42000a70560984b1c` | | Source | [github.com/alea-drand/alea](https://github.com/alea-drand/alea) | | Release | [github.com/alea-drand/alea/releases](https://github.com/alea-drand/alea/releases) | ## Mainnet — pending Mainnet deploy is next. The program ID will be the same vanity address (`ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U`) and the same Config PDA seeds (`[b"config"]`). The Config PDA address is therefore the same on both clusters. The binary SHA and Config's contents (public key, chain hash) will be published here on deploy day. Governance plan (deployer keypair today, planned multisig + eventual immutability with no fixed timeline) is covered in [Security model](/docs/concepts/security-model#authority-compromise--what-an-attacker-gets). ## Drand chain (embedded in Config PDA) | Field | Value | |---|---| | Chain | evmnet | | Chain hash | `04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3` | | Period | 3 seconds | | Genesis | `2024-09-28T13:37:55Z` (Unix `1727521075`) | | Group public key | 128 bytes uncompressed G2 — full hex in [`sdk/typescript/src/idl/alea_verifier.json`](https://github.com/alea-drand/alea/blob/main/sdk/typescript/src/idl/alea_verifier.json) | | Drand REST (chain-explicit, use this) | `https://api.drand.sh/04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3/public/latest` | | Fallback endpoints | `api2.drand.sh`, `api3.drand.sh`, `drand.cloudflare.com`, `api.drand.secureweb3.com:6875` | Alea verifies evmnet signatures only. Other drand chains (mainnet BLS12-381, fastnet, quicknet) will revert with `WrongChainHash` (6007) or `InvalidSignature` (6000). ## SDK programmatic access ```ts import { DEVNET_PROGRAM_ID, DRAND_CHAIN_HASH, DRAND_GENESIS_TIME, DRAND_PERIOD, getConfigAddress, } from "@alea-drand/sdk"; console.log(DEVNET_PROGRAM_ID.toBase58()); // ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U console.log(getConfigAddress().toBase58()); // 6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8 console.log(DRAND_CHAIN_HASH); // 04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3 ``` ```rust use alea_sdk::{PROGRAM_ID, config_pda}; println!("{}", PROGRAM_ID); // ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U let (config, bump) = config_pda(&PROGRAM_ID); println!("{} (bump {})", config, bump); // 6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8 (bump ...) ``` ## Verifying what's deployed The binary is deterministic. To check that the code at [github.com/alea-drand/alea](https://github.com/alea-drand/alea) matches what's running on devnet: ```bash git clone https://github.com/alea-drand/alea cd alea cargo build-sbf --workspace --release sha256sum target/deploy/alea_verifier.so # 8965062489fdcdbb538597545fc6692f3f580d770d34f2d42000a70560984b1c ``` If your local build produces a different SHA, file an issue — any non-deterministic build is a problem we want to know about immediately. To verify what's on-chain: ```bash solana program dump ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U /tmp/onchain.so --url devnet sha256sum /tmp/onchain.so # Should match the build SHA above. ``` ## Related - [Architecture](/docs/concepts/architecture) — what the program does - [Testing & guarantees](/docs/security/testing) — what testing the binary has - [Program interface](/docs/reference/program-interface) — raw wire format for the verify instruction --- SOURCE: https://alea.so/docs/reference/errors # Errors > Every Alea error code, what causes it, what to do. Cmd+F for your error number. The row tells you what's happening and where to look. ## Error codes ### Fired by `verify` | Code | Variant | Meaning | Most-likely cause | Fix | |---|---|---|---|---| | `6000` | `InvalidSignature` | Pairing returned `Some(false)` — equation didn't hold | Signature doesn't match drand evmnet for this round. Most common cause: feeding a signature from a different drand chain (mainnet BLS12-381, fastnet, quicknet). | Use `fetchBeacon()` (SDK default is evmnet) or pass the evmnet chain hash explicitly | | `6001` | `InvalidG1Point` | Signature bytes fail the on-curve check (`y² != x³ + 3 mod p`) or `x ≥ p` | Wrong encoding — 48-byte compressed G1, 96-byte G2, or x above field prime | Signature must be 64 bytes, uncompressed `(x \|\| y)` big-endian. Drand's REST API returns this shape; the SDK parses it correctly. | | `6002` | `RoundZero` | Round number is 0 | Integer default or off-by-one bug in the caller | Drand rounds start at 1. Use `getCurrentRound()` or pass an explicit `bigint` ≥ 1. | | `6004` | `NoSquareRoot` | SVDW hash-to-curve: all 3 candidate field elements fail the square-root check | Should not occur for honest drand input — SVDW theorem guarantees one candidate succeeds. Indicates constant corruption or syscall-oracle regression. | File an issue. Do not retry. | | `6006` | `PairingError` | `alt_bn128_pairing` syscall returned `Err`, or output length was wrong | Malformed pairing inputs. Should not occur with valid drand beacons and a correct Config. | File an issue with the round number and full transaction log. | ### Fired by `initialize` / `update_config` only These never fire during `verify`. They're init/update-time guards on the Config PDA. | Code | Variant | Meaning | |---|---|---| | `6007` | `WrongChainHash` | `chain_hash` argument doesn't match `EXPECTED_EVMNET_CHAIN_HASH`. Rejects wrong-chain deploys at init time. | | `6008` | `WrongPubkey` | `pubkey_g2` argument doesn't match `EXPECTED_EVMNET_G2_PUBKEY`. Fallback guard from ADR 0027. | | `6010` | `InvalidGenesisTime` | `genesis_time` doesn't match `EXPECTED_EVMNET_GENESIS_TIME` | | `6011` | `InvalidPeriod` | `period` doesn't match `EXPECTED_EVMNET_PERIOD` | | `6012` | `UnauthorizedInit` | The signer calling `initialize` isn't the BPFLoaderUpgradeable upgrade_authority. Closes the deploy-to-init front-run window. | ### Reserved (unreachable in current code) Kept in the enum per ADR 0028's append-only, never-renumber rule so consumer SDKs stay stable across program versions. | Code | Variant | Why unreachable | |---|---|---| | `6003` | `InvalidFieldElement` | Hash-to-field uses `from_be_bytes_mod_order`, so no range check is ever needed. Reserved for a future canonical-Fq guard if one is added. | | `6005` | `InvalidG2Point` | Fallback G2 validation path (ADR 0027) is strictly stronger than subgroup check for this single-chain deployment. Not reachable with the current pubkey. | | `6009` | `ReturnDataMissing` | Anchor 0.30 Pattern A return-data handling (ADR 0030) always sets return data on success. A CPI caller would only see this if the deployed program was replaced by one that doesn't emit return data. | ### Anchor framework codes Not Alea-specific, but you may see them: | Code | Name | Meaning | |---|---|---| | `2001` | `ConstraintHasOne` | `has_one = authority` mismatch. Fires AFTER account deserialization, BEFORE the handler body. Wrong authority signer on an init/admin call. | | `3010` | `AccountNotSigner` | An account declared `Signer<'info>` was passed without a signature. Fires earlier than 2001 during account resolution. | Any code outside this table is not from Alea. Check the error's program ID against `ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U` — if it doesn't match, the error is from another program in your transaction (system program, SPL, your own program's Anchor wrapper). ## Decision tree — my verify is reverting Check these in order. Most production reverts land on one of them. 1. Wrong drand chain. Check whatever `fetchBeacon` / `curl` endpoint you're hitting against `DRAND_CHAIN_HASH`. If they differ, you get 6000 (pairing fails against evmnet pubkey). Use `fetchBeacon` from `@alea-drand/sdk`; it hits evmnet by default. 2. Stale round. If your consumer uses `is_round_recent` you'll see your own `StaleBeacon` (or equivalent) error, not an Alea code. If not — Alea accepts any round, so the failure is application-side. See [Common Pitfalls §9](/docs/sdks/common-pitfalls). 3. Compute budget exceeded. Not a 6xxx code — Solana returns `Computational budget exceeded` in the logs. Add `ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 })` as the first instruction. If none of those fit and you're seeing 6000, 6001, or 6002, re-fetch your `(round, signature)` pair against [`api.drand.sh`](https://api.drand.sh/04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3/public/latest) directly. Codes 6004 or 6006 in production mean something is off at the cryptographic or syscall layer. File an issue — these are not expected for valid drand beacons. ## Related - [Common Pitfalls](/docs/sdks/common-pitfalls) — narrative decision tree with context - [Rust SDK](/docs/sdks/rust#errors) — error enum in Rust form - [TypeScript SDK](/docs/sdks/typescript#errors) — `ERRORS` map in TypeScript - [Architecture](/docs/concepts/architecture) — where each error gets raised in the verify flow --- SOURCE: https://alea.so/docs/reference/program-interface # Program interface > Raw on-chain instruction contract for non-Anchor integrators and custom indexers. Everything below is the canonical on-chain wire format. Use it if you're building a Solana program without Anchor, an indexer that parses Alea transactions, or a SDK in a language Alea doesn't yet have. If you're using Anchor + Rust, the [Rust SDK](/docs/sdks/rust) wraps all of this. If you're using TypeScript, [`@alea-drand/sdk`](/docs/sdks/typescript) wraps all of this. This page is for the cases where those aren't options. ## Program | Field | Value | |---|---| | Program ID (vanity) | `ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U` | | Loader | `BPFLoaderUpgradeab1e11111111111111111111111` | | Framework | Anchor 0.30.1 | | IDL | [`sdk/typescript/src/idl/alea_verifier.json`](https://github.com/alea-drand/alea/blob/main/sdk/typescript/src/idl/alea_verifier.json) | Same vanity ID on devnet and mainnet. Cluster is determined by the RPC endpoint you connect to, not by the program ID. ## Accounts ### `Config` PDA Seeds: `[b"config"]`. Derived via `Pubkey::find_program_address`. Devnet address: [`6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8`](https://explorer.solana.com/address/6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8?cluster=devnet) Layout (Anchor-style, 8-byte account discriminator prefix). Borsh serializes in declaration order; pinned by the roundtrip test at [`state.rs` lines 55–95](https://github.com/alea-drand/alea/blob/main/programs/alea-verifier/src/state.rs#L55-L95): | Offset | Size | Field | Type | Description | |---|---|---|---|---| | 0 | 8 | (discriminator) | `[u8; 8]` | Anchor account discriminator | | 8 | 128 | `pubkey_g2` | `[u8; 128]` | drand evmnet group public key, uncompressed G2 (Kyber byte ordering: `x_c1 \|\| x_c0 \|\| y_c1 \|\| y_c0`, each 32 BE bytes) | | 136 | 8 | `genesis_time` | `u64` LE | drand chain genesis, Unix seconds | | 144 | 8 | `period` | `u64` LE | beacon cadence, seconds | | 152 | 32 | `chain_hash` | `[u8; 32]` | evmnet chain hash | | 184 | 32 | `authority` | `Pubkey` | Admin key gating `update_config` (persistent, NOT init-only) | | 216 | 1 | `bump` | `u8` | Canonical PDA bump | Total size: 217 bytes. Read-only from consumer programs; 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. ### `Verify` instruction accounts Order matters. The `verify` instruction takes exactly two account slots: | Index | Name | Writable | Signer | Notes | |---|---|---|---|---| | 0 | `config` | No | No | The Config PDA above | | 1 | `payer` | No | Yes | Pays the tx fee; no lamports change hands in Alea itself | Anchor accounts helpers emit these in this order; if you're building the instruction by hand, match it exactly. ## `verify` instruction ### Discriminator ``` [133, 161, 141, 48, 120, 198, 88, 150] ``` Eight bytes. First eight bytes of SHA-256 of `"global:verify"` per Anchor's convention. Present in the IDL at `instructions[0].discriminator`. ### Data layout Total instruction data size: **80 bytes**. | Offset | Size | Field | Type | Notes | |---|---|---|---|---| | 0 | 8 | discriminator | `[u8; 8]` | Anchor discriminator above | | 8 | 8 | `round` | `u64` LE | drand round number | | 16 | 64 | `signature` | `[u8; 64]` | drand G1 signature, uncompressed `(x \|\| y)` big-endian | ### Return data On success, Alea calls `sol_set_return_data` with 32 bytes: `sha256(signature_bytes)`. This is the canonical drand randomness for the round. Read via `sol_get_return_data` in the caller (Rust on-chain) or `transaction.meta.returnData.data` in a TypeScript client. Return data is single-slot per transaction — the next CPI call that sets return data overwrites it. ### Failure On any validation or verification failure, the `verify` instruction reverts with one of: `InvalidSignature` (6000), `InvalidG1Point` (6001), `RoundZero` (6002), `NoSquareRoot` (6004), or `PairingError` (6006). No return data is set on failure. Other codes in the `AleaError` enum (6003, 6005, 6007–6012) are either reserved per ADR 0028 or scoped to `initialize` / `update_config` and never fire from `verify` — see [Errors reference](/docs/reference/errors) for the full table. ### Minimum compute budget Typical measured verify is ~407K CU. Worst case observed: ~454K. The Solana default per-instruction limit (200K) is too low. Submit a `ComputeBudgetProgram::set_compute_unit_limit` instruction ahead of the verify with a limit of **900,000** — covers worst-case plus headroom for your own work on top. This matches the limit the SDK injects automatically. ## Raw-send example (TypeScript, no SDK) ```ts import { Connection, Keypair, PublicKey, TransactionInstruction, Transaction, ComputeBudgetProgram, } from "@solana/web3.js"; const ALEA_ID = new PublicKey("ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U"); const CONFIG_PDA = PublicKey.findProgramAddressSync( [Buffer.from("config")], ALEA_ID, )[0]; const DISCRIMINATOR = Buffer.from([133, 161, 141, 48, 120, 198, 88, 150]); function buildVerifyIx(round: bigint, signature: Uint8Array, payer: PublicKey): TransactionInstruction { if (signature.length !== 64) throw new Error("signature must be 64 bytes"); const data = Buffer.alloc(80); DISCRIMINATOR.copy(data, 0); data.writeBigUInt64LE(round, 8); Buffer.from(signature).copy(data, 16); return new TransactionInstruction({ keys: [ { pubkey: CONFIG_PDA, isSigner: false, isWritable: false }, { pubkey: payer, isSigner: true, isWritable: false }, ], programId: ALEA_ID, data, }); } // Fetch a beacon from drand evmnet const CHAIN_HASH = "04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3"; const r = await fetch(`https://api.drand.sh/${CHAIN_HASH}/public/latest`).then(x => x.json()); const round = BigInt(r.round); const sigBytes = Buffer.from(r.signature, "hex"); // 64 bytes const payer = Keypair.fromSecretKey(/* ... */); const connection = new Connection("https://api.devnet.solana.com", "confirmed"); const tx = new Transaction() .add(ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 })) .add(buildVerifyIx(round, sigBytes, payer.publicKey)); tx.recentBlockhash = (await connection.getLatestBlockhash("confirmed")).blockhash; tx.feePayer = payer.publicKey; tx.sign(payer); const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true }); await connection.confirmTransaction(sig, "confirmed"); // Read the 32-byte randomness from tx return data const info = await connection.getTransaction(sig, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); const [dataB64] = (info?.meta as any).returnData.data; const randomness = Buffer.from(dataB64, "base64"); // 32 bytes ``` Full wire protocol in ~50 lines of JavaScript. No Anchor, no SDK. ## `BeaconVerified` event Every successful `verify` call emits an Anchor event via `emit!`: ```rust #[event] pub struct BeaconVerified { pub round: u64, pub randomness: [u8; 32], pub payer: Pubkey, } ``` Consumers building indexers or analytics can subscribe via `connection.onLogs(programId, ...)` and parse the base64-encoded `Program data:` line that Anchor writes into the transaction's log messages. The event is part of the program's IDL at [`sdk/typescript/src/idl/alea_verifier.json`](https://github.com/alea-drand/alea/blob/main/sdk/typescript/src/idl/alea_verifier.json). ## Related - [Architecture](/docs/concepts/architecture) — what the instruction does internally - [Errors reference](/docs/reference/errors) — failure codes - [Cluster addresses](/docs/reference/cluster-addresses) — all canonical values in one place - [TypeScript SDK `createVerifyInstruction`](/docs/sdks/typescript#createverifyinstruction) — the SDK's version of this raw builder ---