Skip to content

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.

FieldValue
Program ID (vanity)ALEAydzHd4cN2EWcdHKp4hehAE4B88b16gqVtVqsck2U
LoaderBPFLoaderUpgradeab1e11111111111111111111111
FrameworkAnchor 0.30.1
IDLsdk/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.

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:

OffsetSizeFieldTypeDescription
08(discriminator)[u8; 8]Anchor account discriminator
8128pubkey_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)
1368genesis_timeu64 LEdrand chain genesis, Unix seconds
1448periodu64 LEbeacon cadence, seconds
15232chain_hash[u8; 32]evmnet chain hash
18432authorityPubkeyAdmin key gating update_config (persistent, NOT init-only)
2161bumpu8Canonical 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.

Order matters. The verify instruction takes exactly two account slots:

IndexNameWritableSignerNotes
0configNoNoThe Config PDA above
1payerNoYesPays 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.

[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.

Total instruction data size: 80 bytes.

OffsetSizeFieldTypeNotes
08discriminator[u8; 8]Anchor discriminator above
88roundu64 LEdrand round number
1664signature[u8; 64]drand G1 signature, uncompressed (x || y) big-endian

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.

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.

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.

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.

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.