Program interface
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 wraps all of this. If you’re using TypeScript, @alea-drand/sdk wraps all of this. This page is for the cases where those aren’t options.
Program
Section titled “Program”| Field | Value |
|---|---|
| Program ID (vanity) | ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U |
| Loader | BPFLoaderUpgradeab1e11111111111111111111111 |
| Framework | Anchor 0.30.1 |
| IDL | 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
Section titled “Accounts”Config PDA
Section titled “Config PDA”Seeds: [b"config"]. Derived via Pubkey::find_program_address.
Devnet address: 6anALRxD98Tw7zbA9d5i4NJfTvxDsNBHohHVJWxv2Xm8
Layout (Anchor-style, 8-byte account discriminator prefix). Borsh serializes in declaration order; pinned by the roundtrip test at state.rs lines 55–95:
| 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
Section titled “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
Section titled “verify instruction”Discriminator
Section titled “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
Section titled “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
Section titled “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
Section titled “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 for the full table.
Minimum compute budget
Section titled “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)
Section titled “Raw-send example (TypeScript, no SDK)”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 evmnetconst 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 dataconst info = await connection.getTransaction(sig, { commitment: "confirmed", maxSupportedTransactionVersion: 0,});const [dataB64] = (info?.meta as any).returnData.data;const randomness = Buffer.from(dataB64, "base64"); // 32 bytesFull wire protocol in ~50 lines of JavaScript. No Anchor, no SDK.
BeaconVerified event
Section titled “BeaconVerified event”Every successful verify call emits an Anchor event via emit!:
#[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.
Related
Section titled “Related”- Architecture — what the instruction does internally
- Errors reference — failure codes
- Cluster addresses — all canonical values in one place
- TypeScript SDK
createVerifyInstruction— the SDK’s version of this raw builder