mod fixtures;
use alea_sdk::config_pda;
use fixtures::drand;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
compute_budget::ComputeBudgetInstruction,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{Keypair, Signer},
signer::keypair::read_keypair_file,
transaction::Transaction,
};
use std::collections::HashMap;
use std::str::FromStr;
const DEVNET_RPC: &str = "https://api.devnet.solana.com";
const CU_LIMIT: u32 = 900_000;
fn get_rpc() -> RpcClient {
RpcClient::new_with_commitment(DEVNET_RPC.to_string(), CommitmentConfig::confirmed())
}
fn get_payer() -> Keypair {
let wallet_path = format!(
"{}/.config/solana/alea-deployer.json",
std::env::var("HOME").expect("HOME must be set")
);
read_keypair_file(&wallet_path).unwrap_or_else(|_| panic!("keypair not found at {wallet_path}"))
}
fn build_verify_data(round: u64, signature: &[u8; 64]) -> Vec<u8> {
let mut data = Vec::with_capacity(8 + 8 + 64);
data.extend_from_slice(&drand::VERIFY_DISCRIMINATOR);
data.extend_from_slice(&round.to_le_bytes());
data.extend_from_slice(signature);
data
}
fn build_verify_ix(payer: &Pubkey, round: u64, signature: &[u8; 64]) -> Instruction {
let program_id =
Pubkey::from_str(drand::PROGRAM_ID_STR).expect("PROGRAM_ID_STR must be a valid pubkey");
let (cfg_pda, _bump) = config_pda(&program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new_readonly(cfg_pda, false),
AccountMeta::new_readonly(*payer, true),
],
data: build_verify_data(round, signature),
}
}
fn base64_decode(s: &str) -> Vec<u8> {
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let map: HashMap<u8, u8> = alphabet
.bytes()
.enumerate()
.map(|(i, b)| (b, i as u8))
.collect();
let bytes: Vec<u8> = s.bytes().filter(|&b| b != b'=').collect();
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
let mut i = 0;
while i + 3 < bytes.len() {
let a = *map.get(&bytes[i]).unwrap_or(&0) as u32;
let b = *map.get(&bytes[i + 1]).unwrap_or(&0) as u32;
let c = *map.get(&bytes[i + 2]).unwrap_or(&0) as u32;
let d = *map.get(&bytes[i + 3]).unwrap_or(&0) as u32;
let val = (a << 18) | (b << 12) | (c << 6) | d;
out.push(((val >> 16) & 0xFF) as u8);
out.push(((val >> 8) & 0xFF) as u8);
out.push((val & 0xFF) as u8);
i += 4;
}
if i + 2 < bytes.len() {
let a = *map.get(&bytes[i]).unwrap_or(&0) as u32;
let b = *map.get(&bytes[i + 1]).unwrap_or(&0) as u32;
let c = *map.get(&bytes[i + 2]).unwrap_or(&0) as u32;
let val = (a << 18) | (b << 12) | (c << 6);
out.push(((val >> 16) & 0xFF) as u8);
out.push(((val >> 8) & 0xFF) as u8);
} else if i + 1 < bytes.len() {
let a = *map.get(&bytes[i]).unwrap_or(&0) as u32;
let b = *map.get(&bytes[i + 1]).unwrap_or(&0) as u32;
let val = (a << 18) | (b << 12);
out.push(((val >> 16) & 0xFF) as u8);
}
out
}
fn submit_and_get_meta(
rpc: &RpcClient,
tx: &Transaction,
) -> (String, Option<String>, Option<Vec<u8>>) {
let config = solana_client::rpc_config::RpcSendTransactionConfig {
skip_preflight: true,
..Default::default()
};
let sig = rpc
.send_transaction_with_config(tx, config)
.expect("send_transaction must succeed (network reachable)");
let sig_str = sig.to_string();
let confirmed_config = solana_client::rpc_config::RpcTransactionConfig {
commitment: Some(CommitmentConfig::confirmed()),
max_supported_transaction_version: Some(0),
encoding: Some(solana_transaction_status::UiTransactionEncoding::Base64),
};
for _attempt in 0..15 {
std::thread::sleep(std::time::Duration::from_secs(2));
if let Ok(confirmed) = rpc.get_transaction_with_config(&sig, confirmed_config) {
let meta = confirmed
.transaction
.meta
.as_ref()
.expect("meta must be present");
let meta_err = meta.err.as_ref().map(|e| format!("{e:?}"));
let return_data = {
use solana_transaction_status::option_serializer::OptionSerializer;
match &meta.return_data {
OptionSerializer::Some(rd) => Some(base64_decode(&rd.data.0)),
_ => None,
}
};
return (sig_str, meta_err, return_data);
}
}
panic!("tx {sig_str} not confirmed within 30s on devnet");
}
fn extract_custom_error_code(meta_err: &str) -> Option<u32> {
if let Some(idx) = meta_err.find("Custom(") {
let rest = &meta_err[idx + 7..];
if let Some(end) = rest.find(')') {
if let Ok(code) = rest[..end].trim().parse::<u32>() {
return Some(code);
}
}
}
None
}
#[test]
#[ignore = "hits devnet — run explicitly with -- --ignored; costs ~0.001 SOL"]
fn verify_round_1_fixture_against_devnet() {
let rpc = get_rpc();
let payer = get_payer();
let cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(CU_LIMIT);
let verify_ix = build_verify_ix(&payer.pubkey(), drand::ROUND_1, &drand::ROUND_1_SIGNATURE);
let recent_blockhash = rpc.get_latest_blockhash().expect("get_latest_blockhash");
let tx = Transaction::new_signed_with_payer(
&[cu_ix, verify_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (sig_str, meta_err, return_data) = submit_and_get_meta(&rpc, &tx);
assert!(
meta_err.is_none(),
"round 1 verify must succeed on devnet, got error: {meta_err:?}\ntx: {sig_str}"
);
let randomness = return_data.expect("round 1 verify must produce return data");
assert_eq!(randomness.len(), 32, "return data must be exactly 32 bytes");
assert_eq!(
randomness.as_slice(),
&drand::ROUND_1_EXPECTED_RANDOMNESS,
"round 1 randomness must match expected sha256(sig) per ADR 0036\ntx: {sig_str}"
);
println!(
"[devnet_verify] round 1 ok: tx={sig_str} randomness=0x{}",
randomness.iter().fold(String::new(), |mut s, b| {
s.push_str(&format!("{b:02x}"));
s
})
);
}
#[test]
#[ignore = "hits devnet — run explicitly with -- --ignored; costs ~0.001 SOL"]
fn verify_round_9337227_fixture() {
let rpc = get_rpc();
let payer = get_payer();
let cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(CU_LIMIT);
let verify_ix = build_verify_ix(
&payer.pubkey(),
drand::ROUND_9337227,
&drand::ROUND_9337227_SIGNATURE,
);
let recent_blockhash = rpc.get_latest_blockhash().expect("get_latest_blockhash");
let tx = Transaction::new_signed_with_payer(
&[cu_ix, verify_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (sig_str, meta_err, return_data) = submit_and_get_meta(&rpc, &tx);
assert!(
meta_err.is_none(),
"round 9337227 verify must succeed on devnet, got error: {meta_err:?}\ntx: {sig_str}"
);
let randomness = return_data.expect("round 9337227 verify must produce return data");
assert_eq!(randomness.len(), 32, "return data must be exactly 32 bytes");
assert_eq!(
randomness.as_slice(),
&drand::ROUND_9337227_EXPECTED_RANDOMNESS,
"round 9337227 randomness must match expected sha256(sig) per ADR 0036\ntx: {sig_str}"
);
println!(
"[devnet_verify] round 9337227 ok: tx={sig_str} randomness=0x{}",
randomness.iter().fold(String::new(), |mut s, b| {
s.push_str(&format!("{b:02x}"));
s
})
);
}
#[test]
#[ignore = "hits devnet — run explicitly with -- --ignored; costs ~0.001 SOL"]
fn wrong_round_fails_with_6000() {
let rpc = get_rpc();
let payer = get_payer();
let cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(CU_LIMIT);
let verify_ix = build_verify_ix(&payer.pubkey(), 1, &drand::ROUND_9337227_SIGNATURE);
let recent_blockhash = rpc.get_latest_blockhash().expect("get_latest_blockhash");
let tx = Transaction::new_signed_with_payer(
&[cu_ix, verify_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
let (sig_str, meta_err, _return_data) = submit_and_get_meta(&rpc, &tx);
let err_str = meta_err
.unwrap_or_else(|| panic!("wrong-round tx must fail on-chain; got success\ntx: {sig_str}"));
let code = extract_custom_error_code(&err_str)
.unwrap_or_else(|| panic!("expected Custom error code in: {err_str}\ntx: {sig_str}"));
assert_eq!(
code, 6000,
"wrong-round sig must produce AleaError::InvalidSignature (6000), got {code}\ntx: {sig_str}"
);
println!("[devnet_verify] wrong_round_fails_with_6000 ok: code={code} tx={sig_str}");
}