alea_verifier/instructions/update_config.rs
1use anchor_lang::prelude::*;
2
3use crate::crypto::constants::{
4 EXPECTED_EVMNET_CHAIN_HASH, EXPECTED_EVMNET_GENESIS_TIME, EXPECTED_EVMNET_PERIOD,
5 EXPECTED_EVMNET_PUBKEY,
6};
7use crate::errors::AleaError;
8use crate::events::ConfigUpdated;
9use crate::state::Config;
10
11/// Accounts for the `update_config` instruction.
12///
13/// `has_one = authority` is the authorization primitive — Anchor emits
14/// `ConstraintHasOne` (error code 2001) automatically if the signer does
15/// not match `config.authority`. NO custom `Unauthorized` variant exists
16/// (T1.06 consolidation; see `program/spec.md §"Error Codes"`).
17///
18/// `bump = config.bump` uses the stored canonical bump instead of
19/// re-deriving (≈10K CU saving per ADR 0028).
20#[derive(Accounts)]
21pub struct UpdateConfig<'info> {
22 #[account(
23 mut,
24 seeds = [b"config"],
25 bump = config.bump,
26 has_one = authority,
27 )]
28 pub config: Account<'info, Config>,
29 pub authority: Signer<'info>,
30}
31
32/// `update_config` handler — same guards as `initialize`, different
33/// authorization path (Anchor `has_one` fires before the handler body).
34///
35/// Does NOT modify `config.authority`. Authority rotation is out of
36/// scope for this instruction to prevent accidental authority loss
37/// through a typo; rotation happens via a separate SetAuthority flow
38/// per ADR 0009.
39///
40/// Emits `ConfigUpdated { authority, chain_hash, pubkey_g2_hash }` where
41/// `pubkey_g2_hash = sha256(config.pubkey_g2)` — a 32-byte digest rather
42/// than the raw 128-byte pubkey to keep event logs small (T3.m).
43pub fn update_config_handler(
44 ctx: Context<UpdateConfig>,
45 pubkey_g2: [u8; 128],
46 genesis_time: u64,
47 period: u64,
48 chain_hash: [u8; 32],
49) -> Result<()> {
50 require!(
51 chain_hash == EXPECTED_EVMNET_CHAIN_HASH,
52 AleaError::WrongChainHash
53 );
54 require!(pubkey_g2 == EXPECTED_EVMNET_PUBKEY, AleaError::WrongPubkey);
55 // T2.E — byte-equality guards for all four Config fields (symmetric
56 // with initialize_handler). Prevents genesis=0 / period=0 / wrong-
57 // constant attacks even if authority is ever compromised.
58 require!(
59 genesis_time == EXPECTED_EVMNET_GENESIS_TIME,
60 AleaError::InvalidGenesisTime
61 );
62 require!(period == EXPECTED_EVMNET_PERIOD, AleaError::InvalidPeriod);
63
64 let config = &mut ctx.accounts.config;
65
66 // T2.D — idempotency guard. If all four fields match stored values,
67 // early-return WITHOUT writing + WITHOUT emitting ConfigUpdated. This
68 // eliminates the event-spam attack surface where a compromised or
69 // buggy authority could spam indexers with no-op updates. Source:
70 // P06-T2-01.
71 if config.pubkey_g2 == pubkey_g2
72 && config.genesis_time == genesis_time
73 && config.period == period
74 && config.chain_hash == chain_hash
75 {
76 return Ok(());
77 }
78
79 config.pubkey_g2 = pubkey_g2;
80 config.genesis_time = genesis_time;
81 config.period = period;
82 config.chain_hash = chain_hash;
83 // config.authority intentionally NOT modified (see doc-comment).
84
85 // T2.H — NOTE: pubkey_g2_hash reflects the POST-write value (hashes
86 // the just-stored config.pubkey_g2). Consumer indexers that want to
87 // detect rotation (old → new) must track state externally or query
88 // the previous slot's Config. Schema frozen per ADR 0028; adding a
89 // prev_hash field would require an ADR amendment.
90 let pubkey_g2_hash = anchor_lang::solana_program::hash::hash(&config.pubkey_g2).to_bytes();
91 emit!(ConfigUpdated {
92 authority: config.authority,
93 chain_hash: config.chain_hash,
94 pubkey_g2_hash,
95 });
96 Ok(())
97}