Skip to main content

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}