use anchor_lang::prelude::*;
use crate::crypto::{
constants::G2_GENERATOR,
hash_to_g1::hash_round_to_g1,
pairing::{negate_g1, on_curve_g1, verify_pairing},
};
use crate::errors::AleaError;
use crate::events::BeaconVerified;
use crate::state::Config;
#[derive(Accounts)]
pub struct Verify<'info> {
#[account(
seeds = [b"config"],
bump = config.bump,
)]
pub config: Account<'info, Config>,
pub payer: Signer<'info>,
}
fn verify_beacon_full(round: u64, signature: &[u8; 64], pubkey_g2: &[u8; 128]) -> Result<[u8; 32]> {
require!(round > 0, AleaError::RoundZero); require!(on_curve_g1(signature), AleaError::InvalidG1Point);
let m = hash_round_to_g1(round)?;
debug_assert!(
on_curve_g1(&m),
"SVDW invariant violated: hash_round_to_g1 returned off-curve point"
);
let neg_m = negate_g1(&m);
match verify_pairing(signature, &neg_m, pubkey_g2, &G2_GENERATOR) {
Some(true) => {
let randomness = anchor_lang::solana_program::hash::hash(signature).to_bytes();
Ok(randomness)
}
Some(false) => Err(AleaError::InvalidSignature.into()), None => Err(AleaError::PairingError.into()), }
}
pub fn verify_handler(ctx: Context<Verify>, round: u64, signature: [u8; 64]) -> Result<[u8; 32]> {
let randomness = verify_beacon_full(round, &signature, &ctx.accounts.config.pubkey_g2)?;
emit!(BeaconVerified {
round,
randomness,
payer: ctx.accounts.payer.key(),
});
Ok(randomness)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::constants::EXPECTED_EVMNET_PUBKEY;
use hex_literal::hex;
fn err_code(err: anchor_lang::error::Error) -> u32 {
match err {
anchor_lang::error::Error::AnchorError(ae) => ae.error_code_number,
other => panic!("expected AnchorError, got {other:?}"),
}
}
const ROUND_1_SIG: [u8; 64] = hex!(
"11f812d738a36b2210dc88c2d635ad8039588205f42445d6de09e6530165c346"
"2a23aca348c84badcf8df5321ac24577b7963d5b0d780bc4626baedb45cde373"
);
const ROUND_9337227_SIG: [u8; 64] = hex!(
"01d65d6128f4b2df3d08de85543d8efe06b0281d0770246ae3672e8ddd3efda0"
"269373123458f0b5c0073eeed1c816a06809e127421513e34ee07df6987910b3"
);
#[test]
fn verify_round_1_fixture_produces_drand_randomness() {
let randomness = verify_beacon_full(1, &ROUND_1_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect("round 1 must verify");
assert_eq!(
hex::encode(randomness),
"781b75698adc3af62cfa55db83cf0c73ae54e1ac8c0d4c3a2224126b65369ec5",
"round 1 randomness must match drand API fixture"
);
}
#[test]
fn verify_round_9337227_fixture_produces_drand_randomness() {
let randomness = verify_beacon_full(9337227, &ROUND_9337227_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect("round 9337227 must verify");
assert_eq!(
hex::encode(randomness),
"a1e645cd6193837f626716851f5c42ad4bf63ad75193b2cae40f88c08c8f3bd8",
"round 9337227 randomness must match drand API fixture (randa-mu test vector)"
);
}
#[test]
fn verify_round_zero_returns_6002() {
let err = verify_beacon_full(0, &ROUND_1_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect_err("round 0 must fail");
assert_eq!(
err_code(err),
6002,
"round 0 must return AleaError::RoundZero"
);
}
#[test]
fn verify_non_canonical_g1_x_equals_p_returns_6001() {
let p_be = hex!("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47");
let mut sig = [0u8; 64];
sig[0..32].copy_from_slice(&p_be);
let err = verify_beacon_full(1, &sig, &EXPECTED_EVMNET_PUBKEY)
.expect_err("non-canonical x must fail");
assert_eq!(
err_code(err),
6001,
"x=p must return AleaError::InvalidG1Point"
);
}
#[test]
fn verify_off_curve_signature_returns_6001() {
let mut sig = [0u8; 64];
sig[31] = 1; sig[63] = 1; let err = verify_beacon_full(1, &sig, &EXPECTED_EVMNET_PUBKEY)
.expect_err("off-curve sig must fail");
assert_eq!(
err_code(err),
6001,
"off-curve sig must return AleaError::InvalidG1Point"
);
}
#[test]
fn verify_on_curve_forgery_returns_6000_exact() {
let err = verify_beacon_full(2, &ROUND_1_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect_err("on-curve forgery must fail pairing");
assert_eq!(
err_code(err),
6000,
"on-curve forgery must return EXACTLY InvalidSignature (6000), not 6001 or other"
);
}
#[test]
fn verify_off_curve_bit_flip_returns_6001_exact() {
let mut sig = ROUND_1_SIG;
sig[0] ^= 0xFF;
let err = verify_beacon_full(1, &sig, &EXPECTED_EVMNET_PUBKEY)
.expect_err("off-curve bit flip must fail");
assert_eq!(
err_code(err),
6001,
"off-curve bit flip must return EXACTLY InvalidG1Point (6001)"
);
}
#[test]
fn verify_wrong_round_returns_6000() {
let err = verify_beacon_full(2, &ROUND_1_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect_err("wrong round must fail pairing");
assert_eq!(
err_code(err),
6000,
"wrong round must return AleaError::InvalidSignature"
);
}
#[test]
fn verify_u64_max_round_with_wrong_sig_returns_6000() {
let err = verify_beacon_full(u64::MAX, &ROUND_1_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect_err("u64::MAX round with round-1 sig must fail pairing");
assert_eq!(
err_code(err),
6000,
"u64::MAX round must return InvalidSignature (6000) — numeric boundary handled"
);
}
#[test]
fn verify_same_round_twice_returns_identical_randomness() {
let r1 = verify_beacon_full(1, &ROUND_1_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect("first verify must succeed");
let r2 = verify_beacon_full(1, &ROUND_1_SIG, &EXPECTED_EVMNET_PUBKEY)
.expect("second verify must succeed");
assert_eq!(
r1, r2,
"Alea verify is stateless + replay-safe: same (round, sig) MUST produce byte-identical randomness"
);
}
#[test]
fn pairing_error_6006_code_mapping_stable() {
let err: anchor_lang::error::Error = AleaError::PairingError.into();
assert_eq!(
err_code(err),
6006,
"AleaError::PairingError MUST map to numeric code 6006 per ADR 0028 CPI interface"
);
let err: anchor_lang::error::Error = AleaError::NoSquareRoot.into();
assert_eq!(
err_code(err),
6004,
"AleaError::NoSquareRoot MUST map to numeric code 6004 per ADR 0028"
);
let err: anchor_lang::error::Error = AleaError::InvalidGenesisTime.into();
assert_eq!(
err_code(err),
6010,
"AleaError::InvalidGenesisTime MUST map to numeric code 6010"
);
let err: anchor_lang::error::Error = AleaError::InvalidPeriod.into();
assert_eq!(
err_code(err),
6011,
"AleaError::InvalidPeriod MUST map to numeric code 6011"
);
}
}