alea_verifier/state.rs
1use anchor_lang::prelude::*;
2
3/// drand evmnet configuration account.
4///
5/// Single PDA at `["config"]`. Stores the evmnet chain parameters
6/// needed to verify BLS signatures on-chain. Schema is part of the
7/// v1 CPI interface and frozen per ADR 0028 — exact field order and
8/// sizes must not change across program upgrades.
9#[account]
10pub struct Config {
11 /// drand evmnet G2 public key (uncompressed, Kyber byte ordering:
12 /// `x_c1 || x_c0 || y_c1 || y_c0`, each 32 BE bytes).
13 pub pubkey_g2: [u8; 128],
14
15 /// Genesis timestamp of the evmnet chain (Unix seconds).
16 pub genesis_time: u64,
17
18 /// Round period in seconds.
19 pub period: u64,
20
21 /// evmnet chain hash (identifies which drand chain this config points at).
22 pub chain_hash: [u8; 32],
23
24 /// Authority that can call `update_config`. `has_one` in the
25 /// `UpdateConfig` Accounts struct enforces the match.
26 pub authority: Pubkey,
27
28 /// Canonical PDA bump stored at init time. `verify` and
29 /// `update_config` use `bump = config.bump` to skip re-derivation
30 /// (~10K CU saving).
31 pub bump: u8,
32}
33
34impl Config {
35 /// Exact account size: 8 (Anchor discriminator) + 128 + 8 + 8 + 32
36 /// + 32 + 1 = 217 bytes. Per `program/spec.md §"Account Schema"`.
37 pub const LEN: usize = 8 + 128 + 8 + 8 + 32 + 32 + 1;
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use anchor_lang::{AnchorDeserialize, AnchorSerialize};
44
45 #[test]
46 fn config_len_is_217_bytes() {
47 assert_eq!(
48 Config::LEN,
49 217,
50 "Config::LEN must equal spec.md §Account Schema (217 bytes)"
51 );
52 }
53
54 // P08-T3-03 (Phase 2.5 Wave I, Bucket A) — Borsh serialization round-trip.
55 // Schema is frozen per ADR 0028; this test pins the exact field layout
56 // (order, sizes) and byte length so any accidental reordering or type
57 // change surfaces immediately at `cargo test`.
58 #[test]
59 fn config_borsh_roundtrip_pins_v1_schema() {
60 let original = Config {
61 pubkey_g2: [0xAB; 128],
62 genesis_time: 0x1122_3344_5566_7788,
63 period: 3,
64 chain_hash: [0xCD; 32],
65 authority: Pubkey::new_unique(),
66 bump: 255,
67 };
68
69 let bytes = original.try_to_vec().expect("serialize must succeed");
70 // Expected payload = 128 + 8 + 8 + 32 + 32 + 1 = 209 bytes (Config::LEN
71 // minus the 8-byte Anchor discriminator that prefixes on-chain accounts
72 // but not the raw Borsh-serialized struct).
73 assert_eq!(
74 bytes.len(),
75 Config::LEN - 8,
76 "Borsh-serialized Config must be exactly 209 bytes (Config::LEN - 8 discriminator)",
77 );
78
79 // Byte-layout sanity: Borsh writes fields in declaration order, so the
80 // first 128 bytes MUST equal pubkey_g2, the next 8 bytes MUST be
81 // genesis_time (little-endian u64), etc.
82 assert_eq!(
83 &bytes[0..128],
84 &[0xAB; 128],
85 "pubkey_g2 must be first field"
86 );
87 assert_eq!(
88 &bytes[128..136],
89 &0x1122_3344_5566_7788u64.to_le_bytes(),
90 "genesis_time must follow pubkey_g2 as LE u64"
91 );
92 assert_eq!(
93 &bytes[136..144],
94 &3u64.to_le_bytes(),
95 "period must follow genesis_time as LE u64"
96 );
97 assert_eq!(
98 &bytes[144..176],
99 &[0xCD; 32],
100 "chain_hash must follow period"
101 );
102 // authority (Pubkey) at 176..208; bump at 208.
103 assert_eq!(bytes[208], 255, "bump must be the last byte");
104
105 let recovered = Config::try_from_slice(&bytes).expect("deserialize must succeed");
106 assert_eq!(recovered.pubkey_g2, original.pubkey_g2);
107 assert_eq!(recovered.genesis_time, original.genesis_time);
108 assert_eq!(recovered.period, original.period);
109 assert_eq!(recovered.chain_hash, original.chain_hash);
110 assert_eq!(recovered.authority, original.authority);
111 assert_eq!(recovered.bump, original.bump);
112 }
113}