Skip to main content

espresso_types/v0/impls/
stake_table.rs

1use std::{
2    cmp::{max, min},
3    collections::{HashMap, HashSet},
4    str::FromStr,
5    time::{Duration, Instant},
6};
7#[cfg(feature = "node")]
8use std::{future::Future, sync::Arc};
9
10#[cfg(feature = "node")]
11use alloy::{
12    eips::{BlockId, BlockNumberOrTag},
13    primitives::utils::format_ether,
14    providers::Provider,
15    rpc::types::Filter,
16    sol_types::{SolEvent, SolEventInterface},
17};
18use alloy::{
19    primitives::{Address, U256, address},
20    rpc::types::Log,
21};
22#[cfg(feature = "node")]
23use anyhow::bail;
24use anyhow::{Context, ensure};
25use ark_ec::AffineRepr;
26use ark_serialize::CanonicalSerialize;
27use ark_std::One;
28#[cfg(any(feature = "node", test, feature = "testing"))]
29use async_lock::Mutex as AsyncMutex;
30#[cfg(feature = "node")]
31use async_lock::RwLock as AsyncRwLock;
32use bigdecimal::BigDecimal;
33use committable::{Commitment, Committable, RawCommitmentBuilder};
34use futures::future::BoxFuture;
35#[cfg(feature = "node")]
36use hotshot_contract_adapter::sol_types::{
37    EspToken::{self, EspTokenInstance},
38    StakeTableV3,
39};
40use hotshot_contract_adapter::{
41    sol_types::StakeTableV3::{
42        CommissionUpdated, ConsensusKeysUpdated, ConsensusKeysUpdatedV2, Delegated, P2pAddrUpdated,
43        StakeTableV3Events, Undelegated, UndelegatedV2, ValidatorExit, ValidatorExitV2,
44        ValidatorRegistered, ValidatorRegisteredV2, ValidatorRegisteredV3, X25519KeyUpdated,
45    },
46    stake_table::StakeTableSolError,
47};
48use hotshot_types::{
49    addr::NetAddr,
50    data::{EpochNumber, vid_disperse::VID_TARGET_TOTAL_STAKE},
51    signature_key::{BLSPubKey, SchnorrPubKey},
52    traits::signature_key::SignatureKey as _,
53    x25519,
54};
55use humantime::format_duration;
56use indexmap::IndexMap;
57use itertools::Itertools;
58use num_traits::{FromPrimitive, Zero};
59use thiserror::Error;
60#[cfg(feature = "node")]
61use tokio::spawn;
62use tokio::time::sleep;
63#[cfg(feature = "node")]
64use tracing::Instrument;
65use vbs::version::Version;
66
67#[cfg(feature = "node")]
68use super::L1Client;
69#[cfg(any(test, feature = "testing"))]
70use super::v0_3::DAMembers;
71use super::{
72    Header,
73    traits::StateCatchup,
74    v0_3::{
75        AuthenticatedValidator, ChainConfig, EventKey, Fetcher, MAX_VALIDATORS,
76        RegisteredValidator, StakeTableEvent,
77    },
78};
79#[cfg(feature = "node")]
80use super::{traits::MembershipPersistence, v0_3::StakeTableUpdateTask};
81#[cfg(feature = "node")]
82use crate::traits::EventsPersistenceRead;
83#[cfg(feature = "node")]
84use crate::v0_1::L1Provider;
85#[cfg(feature = "node")]
86use crate::v0_3::{BLOCKS_PER_YEAR, INFLATION_RATE};
87use crate::{
88    v0_1::{ChainId, DECAF_CHAIN_ID, MAINNET_CHAIN_ID},
89    v0_3::{
90        COMMISSION_BASIS_POINTS, EventSortingError, ExpectedStakeTableError, FetchRewardError,
91        MILLISECONDS_PER_YEAR, RewardAmount, StakeTableError,
92    },
93};
94
95pub type RegisteredValidatorMap = IndexMap<Address, RegisteredValidator<BLSPubKey>>;
96pub type AuthenticatedValidatorMap = IndexMap<Address, AuthenticatedValidator<BLSPubKey>>;
97
98pub fn to_registered_validator_map(
99    validators: &AuthenticatedValidatorMap,
100) -> RegisteredValidatorMap {
101    validators
102        .iter()
103        .map(|(addr, v)| (*addr, v.clone().into()))
104        .collect()
105}
106
107pub type StakeTableHash = Commitment<StakeTableState>;
108
109/// The result of applying a stake table event:
110/// - `Ok(Ok(()))`: success
111/// - `Ok(Err(...))`: expected error
112/// - `Err(...)`: serious error
113type ApplyEventResult<T> = Result<Result<T, ExpectedStakeTableError>, StakeTableError>;
114
115/// Format the alloy Log RPC type in a way to make it easy to find the event in an explorer.
116#[cfg_attr(not(feature = "node"), allow(dead_code))]
117trait DisplayLog {
118    fn display(&self) -> String;
119}
120
121impl DisplayLog for Log {
122    fn display(&self) -> String {
123        // These values are all unlikely to be missing because we only create Log variables by
124        // fetching them from the RPC, so for simplicity we use defaults if the any of the values
125        // are missing.
126        let block = self.block_number.unwrap_or_default();
127        let index = self.log_index.unwrap_or_default();
128        let hash = self.transaction_hash.unwrap_or_default();
129        format!("Log(block={block},index={index},transaction_hash={hash})")
130    }
131}
132
133impl TryFrom<StakeTableV3Events> for StakeTableEvent {
134    type Error = anyhow::Error;
135
136    fn try_from(value: StakeTableV3Events) -> anyhow::Result<Self> {
137        match value {
138            StakeTableV3Events::ValidatorRegistered(v) => Ok(StakeTableEvent::Register(v)),
139            StakeTableV3Events::ValidatorRegisteredV2(v) => Ok(StakeTableEvent::RegisterV2(v)),
140            StakeTableV3Events::ValidatorRegisteredV3(v) => Ok(StakeTableEvent::RegisterV3(v)),
141            StakeTableV3Events::ValidatorExit(v) => Ok(StakeTableEvent::Deregister(v)),
142            StakeTableV3Events::ValidatorExitV2(v) => Ok(StakeTableEvent::DeregisterV2(v)),
143            StakeTableV3Events::Delegated(v) => Ok(StakeTableEvent::Delegate(v)),
144            StakeTableV3Events::Undelegated(v) => Ok(StakeTableEvent::Undelegate(v)),
145            StakeTableV3Events::UndelegatedV2(v) => Ok(StakeTableEvent::UndelegateV2(v)),
146            StakeTableV3Events::ConsensusKeysUpdated(v) => Ok(StakeTableEvent::KeyUpdate(v)),
147            StakeTableV3Events::ConsensusKeysUpdatedV2(v) => Ok(StakeTableEvent::KeyUpdateV2(v)),
148            StakeTableV3Events::CommissionUpdated(v) => Ok(StakeTableEvent::CommissionUpdate(v)),
149            StakeTableV3Events::X25519KeyUpdated(v) => Ok(StakeTableEvent::X25519KeyUpdate(v)),
150            StakeTableV3Events::P2pAddrUpdated(v) => Ok(StakeTableEvent::P2pAddrUpdate(v)),
151            StakeTableV3Events::ExitEscrowPeriodUpdated(v) => Err(anyhow::anyhow!(
152                "Unsupported StakeTableV3Events::ExitEscrowPeriodUpdated({v:?})"
153            )),
154            StakeTableV3Events::Initialized(v) => Err(anyhow::anyhow!(
155                "Unsupported StakeTableV3Events::Initialized({v:?})"
156            )),
157            StakeTableV3Events::MaxCommissionIncreaseUpdated(v) => Err(anyhow::anyhow!(
158                "Unsupported StakeTableV3Events::MaxCommissionIncreaseUpdated({v:?})"
159            )),
160            StakeTableV3Events::MinCommissionUpdateIntervalUpdated(v) => Err(anyhow::anyhow!(
161                "Unsupported StakeTableV3Events::MinCommissionUpdateIntervalUpdated({v:?})"
162            )),
163            StakeTableV3Events::OwnershipTransferred(v) => Err(anyhow::anyhow!(
164                "Unsupported StakeTableV3Events::OwnershipTransferred({v:?})"
165            )),
166            StakeTableV3Events::Paused(v) => Err(anyhow::anyhow!(
167                "Unsupported StakeTableV3Events::Paused({v:?})"
168            )),
169            StakeTableV3Events::RoleAdminChanged(v) => Err(anyhow::anyhow!(
170                "Unsupported StakeTableV3Events::RoleAdminChanged({v:?})"
171            )),
172            StakeTableV3Events::RoleGranted(v) => Err(anyhow::anyhow!(
173                "Unsupported StakeTableV3Events::RoleGranted({v:?})"
174            )),
175            StakeTableV3Events::RoleRevoked(v) => Err(anyhow::anyhow!(
176                "Unsupported StakeTableV3Events::RoleRevoked({v:?})"
177            )),
178            StakeTableV3Events::Unpaused(v) => Err(anyhow::anyhow!(
179                "Unsupported StakeTableV3Events::Unpaused({v:?})"
180            )),
181            StakeTableV3Events::Upgraded(v) => Err(anyhow::anyhow!(
182                "Unsupported StakeTableV3Events::Upgraded({v:?})"
183            )),
184            StakeTableV3Events::WithdrawalClaimed(v) => Err(anyhow::anyhow!(
185                "Unsupported StakeTableV3Events::WithdrawalClaimed({v:?})"
186            )),
187            StakeTableV3Events::ValidatorExitClaimed(v) => Err(anyhow::anyhow!(
188                "Unsupported StakeTableV3Events::ValidatorExitClaimed({v:?})"
189            )),
190            StakeTableV3Events::Withdrawal(v) => Err(anyhow::anyhow!(
191                "Unsupported StakeTableV3Events::Withdrawal({v:?})"
192            )),
193            StakeTableV3Events::MetadataUriUpdated(v) => Err(anyhow::anyhow!(
194                "Unsupported StakeTableV3Events::MetadataUriUpdated({v:?})"
195            )),
196            StakeTableV3Events::MinDelegateAmountUpdated(v) => Err(anyhow::anyhow!(
197                "Unsupported StakeTableV3Events::MinDelegateAmountUpdated({v:?})"
198            )),
199        }
200    }
201}
202
203#[cfg_attr(not(feature = "node"), allow(dead_code))]
204fn sort_stake_table_events(
205    event_logs: Vec<(StakeTableV3Events, Log)>,
206) -> Result<Vec<(EventKey, StakeTableEvent)>, EventSortingError> {
207    let mut events: Vec<(EventKey, StakeTableEvent)> = Vec::new();
208
209    let key = |log: &Log| -> Result<EventKey, EventSortingError> {
210        let block_number = log
211            .block_number
212            .ok_or(EventSortingError::MissingBlockNumber)?;
213        let log_index = log.log_index.ok_or(EventSortingError::MissingLogIndex)?;
214        Ok((block_number, log_index))
215    };
216
217    for (e, log) in event_logs {
218        let k = key(&log)?;
219        let evt: StakeTableEvent = e
220            .try_into()
221            .map_err(|_| EventSortingError::InvalidStakeTableEvent)?;
222        events.push((k, evt));
223    }
224
225    events.sort_by_key(|(key, _)| *key);
226    Ok(events)
227}
228
229#[derive(Clone, Debug, Default, PartialEq)]
230pub struct StakeTableState {
231    validators: RegisteredValidatorMap,
232    validator_exits: HashSet<Address>,
233    used_bls_keys: HashSet<BLSPubKey>,
234    used_schnorr_keys: HashSet<SchnorrPubKey>,
235    used_x25519_keys: HashSet<x25519::PublicKey>,
236}
237
238#[cfg(feature = "rlp")]
239mod stake_table_state_rlp {
240    use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
241
242    use super::*;
243
244    #[derive(RlpEncodable, RlpDecodable)]
245    struct StakeTableStateRlp {
246        validators: Vec<RegisteredValidator<BLSPubKey>>,
247        validator_exits: Vec<Address>,
248        used_bls_keys: Vec<Vec<u8>>,
249        used_schnorr_keys: Vec<Vec<u8>>,
250        used_x25519_keys: Vec<Vec<u8>>,
251    }
252
253    impl TryFrom<&StakeTableState> for StakeTableStateRlp {
254        type Error = anyhow::Error;
255
256        fn try_from(state: &StakeTableState) -> Result<Self, Self::Error> {
257            // The HashSets of used keys do not have a canonical order, so we need to sort them to
258            // make this format canonical.
259            let mut validators = state.validators.values().cloned().collect::<Vec<_>>();
260            validators.sort_by_key(|v| v.account);
261
262            let mut validator_exits = state.validator_exits.iter().copied().collect::<Vec<_>>();
263            validator_exits.sort();
264
265            let mut used_bls_keys = state
266                .used_bls_keys
267                .iter()
268                .map(bincode::serialize)
269                .collect::<Result<Vec<_>, _>>()
270                .context("serializing used_bls_keys")?;
271            used_bls_keys.sort();
272
273            let mut used_schnorr_keys = state
274                .used_schnorr_keys
275                .iter()
276                .map(bincode::serialize)
277                .collect::<Result<Vec<_>, _>>()
278                .context("serializing used_schnorr_keys")?;
279            used_schnorr_keys.sort();
280
281            let mut used_x25519_keys = state
282                .used_x25519_keys
283                .iter()
284                .map(bincode::serialize)
285                .collect::<Result<Vec<_>, _>>()
286                .context("serializing used_x25519_keys")?;
287            used_x25519_keys.sort();
288
289            Ok(Self {
290                validators,
291                validator_exits,
292                used_bls_keys,
293                used_schnorr_keys,
294                used_x25519_keys,
295            })
296        }
297    }
298
299    impl TryFrom<StakeTableStateRlp> for StakeTableState {
300        type Error = anyhow::Error;
301
302        fn try_from(state: StakeTableStateRlp) -> Result<Self, Self::Error> {
303            Ok(Self {
304                validators: state
305                    .validators
306                    .into_iter()
307                    .map(|v| (v.account, v))
308                    .collect(),
309                validator_exits: state.validator_exits.into_iter().collect(),
310                used_bls_keys: state
311                    .used_bls_keys
312                    .iter()
313                    .map(|bytes| bincode::deserialize(bytes))
314                    .collect::<Result<HashSet<_>, _>>()
315                    .context("deserializing used_bls_keys")?,
316                used_schnorr_keys: state
317                    .used_schnorr_keys
318                    .iter()
319                    .map(|bytes| bincode::deserialize(bytes))
320                    .collect::<Result<HashSet<_>, _>>()
321                    .context("deserializing used_schnorr_keys")?,
322                used_x25519_keys: state
323                    .used_x25519_keys
324                    .iter()
325                    .map(|bytes| bincode::deserialize(bytes))
326                    .collect::<Result<HashSet<_>, _>>()
327                    .context("deserializing used_x25519_keys")?,
328            })
329        }
330    }
331
332    impl Encodable for StakeTableState {
333        fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
334            StakeTableStateRlp::try_from(self)
335                .expect("failed to canonicalize StakeTableState")
336                .encode(out);
337        }
338
339        fn length(&self) -> usize {
340            StakeTableStateRlp::try_from(self)
341                .expect("failed to canonicalize StakeTableState")
342                .length()
343        }
344    }
345
346    impl Decodable for StakeTableState {
347        fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
348            let rlp = StakeTableStateRlp::decode(buf)?;
349            rlp.try_into().map_err(|err| {
350                tracing::error!("failed to parse canonical StakeTableState: {err:#}");
351                alloy_rlp::Error::Custom("failed to parse canonical StakeTableState")
352            })
353        }
354    }
355}
356
357impl Committable for StakeTableState {
358    fn commit(&self) -> committable::Commitment<Self> {
359        let mut builder = RawCommitmentBuilder::new(&Self::tag());
360
361        for (_, validator) in self.validators.iter().sorted_by_key(|(a, _)| *a) {
362            builder = builder.field("validator", validator.commit());
363        }
364
365        builder = builder.constant_str("used_bls_keys");
366        for key in self.used_bls_keys.iter().sorted() {
367            builder = builder.var_size_bytes(&key.to_bytes());
368        }
369
370        builder = builder.constant_str("used_schnorr_keys");
371        for key in self
372            .used_schnorr_keys
373            .iter()
374            .sorted_by(|a, b| a.to_affine().xy().cmp(&b.to_affine().xy()))
375        {
376            let mut schnorr_key_bytes = vec![];
377            key.serialize_with_mode(&mut schnorr_key_bytes, ark_serialize::Compress::Yes)
378                .unwrap();
379            builder = builder.var_size_bytes(&schnorr_key_bytes);
380        }
381
382        // Only include used_x25519_keys when non-empty to preserve the pre-fast-finality commitment
383        // value for stake tables that have no x25519 keys yet. Same pattern as x25519_key /
384        // p2p_addr on RegisteredValidator. Backward compatibility is cross-checked by
385        // REFERENCE_V4_HEADER_COMMITMENT in reference_tests.rs.
386        if !self.used_x25519_keys.is_empty() {
387            builder = builder.constant_str("used_x25519_keys");
388            for key in self.used_x25519_keys.iter().sorted() {
389                builder = builder.var_size_bytes(key.as_slice());
390            }
391        }
392
393        builder = builder.constant_str("validator_exits");
394
395        for key in self.validator_exits.iter().sorted() {
396            builder = builder.fixed_size_bytes(&key.into_array());
397        }
398
399        builder.finalize()
400    }
401
402    fn tag() -> String {
403        "STAKE_TABLE".to_string()
404    }
405}
406
407impl StakeTableState {
408    pub fn new(
409        validators: RegisteredValidatorMap,
410        validator_exits: HashSet<Address>,
411        used_bls_keys: HashSet<BLSPubKey>,
412        used_schnorr_keys: HashSet<SchnorrPubKey>,
413        used_x25519_keys: HashSet<x25519::PublicKey>,
414    ) -> Self {
415        Self {
416            validators,
417            validator_exits,
418            used_bls_keys,
419            used_schnorr_keys,
420            used_x25519_keys,
421        }
422    }
423
424    pub fn validators(&self) -> &RegisteredValidatorMap {
425        &self.validators
426    }
427
428    pub fn into_validators(self) -> RegisteredValidatorMap {
429        self.validators
430    }
431
432    pub fn used_bls_keys(&self) -> &HashSet<BLSPubKey> {
433        &self.used_bls_keys
434    }
435
436    pub fn used_schnorr_keys(&self) -> &HashSet<SchnorrPubKey> {
437        &self.used_schnorr_keys
438    }
439
440    pub fn used_x25519_keys(&self) -> &HashSet<x25519::PublicKey> {
441        &self.used_x25519_keys
442    }
443
444    pub fn validator_exits(&self) -> &HashSet<Address> {
445        &self.validator_exits
446    }
447
448    /// Applies a stake table event to this state.
449    ///
450    ///
451    /// This function MUST NOT modify `self` if the event is invalid. All validation
452    /// checks must be performed before any state modifications occur.
453    pub fn apply_event(&mut self, event: StakeTableEvent) -> ApplyEventResult<()> {
454        match event {
455            StakeTableEvent::Register(ValidatorRegistered {
456                account,
457                blsVk,
458                schnorrVk,
459                commission,
460            }) => {
461                let stake_table_key = BLSPubKey::try_from(blsVk).ok();
462                if stake_table_key.is_none() {
463                    tracing::warn!(
464                        ?account,
465                        "registering unauthenticated validator (invalid BLS key)"
466                    );
467                }
468                let state_ver_key = SchnorrPubKey::try_from(schnorrVk).ok();
469                if state_ver_key.is_none() {
470                    tracing::warn!(
471                        ?account,
472                        "registering unauthenticated validator (invalid Schnorr key)"
473                    );
474                }
475                let authenticated = stake_table_key.is_some() && state_ver_key.is_some();
476
477                if self.validator_exits.contains(&account) {
478                    return Err(StakeTableError::ValidatorAlreadyExited(account));
479                }
480
481                let entry = self.validators.entry(account);
482                if let indexmap::map::Entry::Occupied(_) = entry {
483                    return Err(StakeTableError::AlreadyRegistered(account));
484                }
485
486                // Unparsable keys (`None`) are never deduped; the L1 contract
487                // dedups by raw G2 bytes so distinct registrations are unique
488                // on-chain regardless.
489                if let Some(k) = stake_table_key.as_ref()
490                    && self.used_bls_keys.contains(k)
491                {
492                    return Err(StakeTableError::BlsKeyAlreadyUsed(k.to_string()));
493                }
494
495                // The stake table v1 contract does *not* enforce that each schnorr key is only used once.
496                // Unparsable schnorr keys (`None`) are not deduped.
497                if let Some(k) = state_ver_key.as_ref()
498                    && self.used_schnorr_keys.contains(k)
499                {
500                    return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
501                        k.to_string(),
502                    )));
503                }
504
505                // All checks ok, applying changes
506                if let Some(k) = stake_table_key.as_ref() {
507                    self.used_bls_keys.insert(*k);
508                }
509                if let Some(k) = state_ver_key.as_ref() {
510                    self.used_schnorr_keys.insert(k.clone());
511                }
512
513                entry.or_insert(RegisteredValidator {
514                    account,
515                    stake_table_key,
516                    state_ver_key,
517                    stake: U256::ZERO,
518                    commission,
519                    delegators: HashMap::new(),
520                    authenticated,
521                    x25519_key: None,
522                    p2p_addr: None,
523                });
524            },
525
526            StakeTableEvent::RegisterV2(ref reg) => {
527                let (stake_table_key, state_ver_key, authenticated) = match reg.authenticate() {
528                    Ok((bls, schnorr)) => (Some(bls), Some(schnorr), true),
529                    Err(e) => {
530                        tracing::warn!(
531                            account = ?reg.account,
532                            %e,
533                            "registering unauthenticated validator",
534                        );
535                        (
536                            BLSPubKey::try_from(reg.blsVK).ok(),
537                            SchnorrPubKey::try_from(reg.schnorrVK).ok(),
538                            false,
539                        )
540                    },
541                };
542
543                let ValidatorRegisteredV2 {
544                    account,
545                    commission,
546                    ..
547                } = reg;
548
549                // Reject if validator already exited
550                if self.validator_exits.contains(account) {
551                    return Err(StakeTableError::ValidatorAlreadyExited(*account));
552                }
553
554                let entry = self.validators.entry(*account);
555                if let indexmap::map::Entry::Occupied(_) = entry {
556                    return Err(StakeTableError::AlreadyRegistered(*account));
557                }
558
559                // Unparsable keys aren't deduped, parsable ones are whether or not the signature verified.
560                if let Some(k) = stake_table_key.as_ref()
561                    && self.used_bls_keys.contains(k)
562                {
563                    return Err(StakeTableError::BlsKeyAlreadyUsed(k.to_string()));
564                }
565
566                // The contract only remembers schnorr keys registered since its v2 upgrade, so a
567                // key from a v1 registration can be accepted on chain a second time. Decaf started
568                // on the v1 contract and mainnet on v2, so only decaf has v1 registrations.
569                let (state_ver_key, authenticated) = match state_ver_key {
570                    Some(k) if self.used_schnorr_keys.contains(&k) => {
571                        tracing::warn!(
572                            ?account,
573                            schnorr_key = %k,
574                            "registering unauthenticated validator (schnorr key already used)"
575                        );
576                        (None, false)
577                    },
578                    key => (key, authenticated),
579                };
580
581                // All checks ok, applying changes
582                if let Some(k) = stake_table_key.as_ref() {
583                    self.used_bls_keys.insert(*k);
584                }
585                if let Some(k) = state_ver_key.as_ref() {
586                    self.used_schnorr_keys.insert(k.clone());
587                }
588
589                entry.or_insert(RegisteredValidator {
590                    account: *account,
591                    stake_table_key,
592                    state_ver_key,
593                    stake: U256::ZERO,
594                    commission: *commission,
595                    delegators: HashMap::new(),
596                    authenticated,
597                    x25519_key: None,
598                    p2p_addr: None,
599                });
600            },
601
602            StakeTableEvent::Deregister(ValidatorExit { validator })
603            | StakeTableEvent::DeregisterV2(ValidatorExitV2 { validator, .. }) => {
604                if !self.validators.contains_key(&validator) {
605                    return Err(StakeTableError::ValidatorNotFound(validator));
606                }
607
608                // All checks ok, applying changes
609                self.validator_exits.insert(validator);
610                self.validators.shift_remove(&validator);
611            },
612
613            StakeTableEvent::Delegate(delegated) => {
614                let Delegated {
615                    delegator,
616                    validator,
617                    amount,
618                } = delegated;
619
620                // Check amount is not zero first
621                if amount.is_zero() {
622                    return Err(StakeTableError::ZeroDelegatorStake(delegator));
623                }
624
625                let val = self
626                    .validators
627                    .get_mut(&validator)
628                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
629
630                // All checks ok, applying changes
631                // This cannot overflow in practice
632                val.stake = val.stake.checked_add(amount).unwrap_or_else(|| {
633                    panic!(
634                        "validator stake overflow: validator={validator}, stake={}, \
635                         amount={amount}",
636                        val.stake
637                    )
638                });
639                // Insert the delegator with the given stake
640                // or increase the stake if already present
641                val.delegators
642                    .entry(delegator)
643                    .and_modify(|stake| {
644                        *stake = stake.checked_add(amount).unwrap_or_else(|| {
645                            panic!(
646                                "delegator stake overflow: delegator={delegator}, stake={stake}, \
647                                 amount={amount}"
648                            )
649                        });
650                    })
651                    .or_insert(amount);
652            },
653
654            StakeTableEvent::Undelegate(Undelegated {
655                delegator,
656                validator,
657                amount,
658            })
659            | StakeTableEvent::UndelegateV2(UndelegatedV2 {
660                delegator,
661                validator,
662                amount,
663                ..
664            }) => {
665                let val = self
666                    .validators
667                    .get_mut(&validator)
668                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
669
670                if val.stake < amount {
671                    tracing::warn!("validator_stake={}, amount={amount}", val.stake);
672                    return Err(StakeTableError::InsufficientStake);
673                }
674
675                let delegator_stake = val
676                    .delegators
677                    .get_mut(&delegator)
678                    .ok_or(StakeTableError::DelegatorNotFound(delegator))?;
679
680                if *delegator_stake < amount {
681                    tracing::warn!("delegator_stake={delegator_stake}, amount={amount}");
682                    return Err(StakeTableError::InsufficientStake);
683                }
684
685                // Can unwrap because check above passed
686                let new_delegator_stake = delegator_stake.checked_sub(amount).unwrap();
687
688                // Can unwrap because check above passed
689                // All checks ok, applying changes
690                val.stake = val.stake.checked_sub(amount).unwrap();
691
692                if new_delegator_stake.is_zero() {
693                    val.delegators.remove(&delegator);
694                } else {
695                    *delegator_stake = new_delegator_stake;
696                }
697            },
698
699            StakeTableEvent::KeyUpdate(update) => {
700                let ConsensusKeysUpdated {
701                    account,
702                    blsVK,
703                    schnorrVK,
704                } = update;
705
706                let Ok(stake_table_key) = BLSPubKey::try_from(blsVK) else {
707                    return Ok(Err(ExpectedStakeTableError::InvalidBlsKey));
708                };
709                let Ok(state_ver_key) = SchnorrPubKey::try_from(schnorrVK) else {
710                    return Ok(Err(ExpectedStakeTableError::InvalidSchnorrKey));
711                };
712
713                if !self.validators.contains_key(&account) {
714                    return Err(StakeTableError::ValidatorNotFound(account));
715                }
716
717                if self.used_bls_keys.contains(&stake_table_key) {
718                    return Err(StakeTableError::BlsKeyAlreadyUsed(
719                        stake_table_key.to_string(),
720                    ));
721                }
722
723                // The stake table v1 contract does *not* enforce that each schnorr key is only used once,
724                // therefore it's possible to have multiple validators with the same schnorr key.
725                if self.used_schnorr_keys.contains(&state_ver_key) {
726                    return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
727                        state_ver_key.to_string(),
728                    )));
729                }
730
731                // All checks ok, applying changes
732                self.used_bls_keys.insert(stake_table_key);
733                self.used_schnorr_keys.insert(state_ver_key.clone());
734                // Can unwrap because check above passed
735                let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
736                    panic!("validator {account} must exist after contains_key check")
737                });
738                validator.stake_table_key = Some(stake_table_key);
739                validator.state_ver_key = Some(state_ver_key);
740                // V1 contract verified BLS POP at submission; no off-chain sig to re-check.
741                validator.authenticated = true;
742            },
743
744            StakeTableEvent::KeyUpdateV2(update) => {
745                // `validate_event` pre-filters auth failures during live fetch;
746                // on replay an unparsable key is a soft skip, other failures are fatal.
747                let (stake_table_key, state_ver_key) = match update.authenticate() {
748                    Ok(parsed) => parsed,
749                    Err(StakeTableSolError::InvalidBlsKey) => {
750                        return Ok(Err(ExpectedStakeTableError::InvalidBlsKey));
751                    },
752                    Err(StakeTableSolError::InvalidSchnorrKey) => {
753                        return Ok(Err(ExpectedStakeTableError::InvalidSchnorrKey));
754                    },
755                    Err(e) => {
756                        return Err(StakeTableError::AuthenticationFailed(e.to_string()));
757                    },
758                };
759
760                let ConsensusKeysUpdatedV2 { account, .. } = update;
761
762                if !self.validators.contains_key(&account) {
763                    return Err(StakeTableError::ValidatorNotFound(account));
764                }
765
766                // The stake table contract enforces that each bls key is only used once.
767                if self.used_bls_keys.contains(&stake_table_key) {
768                    return Err(StakeTableError::BlsKeyAlreadyUsed(
769                        stake_table_key.to_string(),
770                    ));
771                }
772
773                if self.used_schnorr_keys.contains(&state_ver_key) {
774                    return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
775                        state_ver_key.to_string(),
776                    )));
777                }
778
779                // All checks ok, applying changes
780                self.used_bls_keys.insert(stake_table_key);
781                self.used_schnorr_keys.insert(state_ver_key.clone());
782
783                // Can unwrap because check above passed
784                let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
785                    panic!("validator {account} must exist after contains_key check")
786                });
787                validator.stake_table_key = Some(stake_table_key);
788                validator.state_ver_key = Some(state_ver_key);
789                validator.authenticated = true;
790            },
791
792            StakeTableEvent::CommissionUpdate(CommissionUpdated {
793                validator,
794                newCommission,
795                ..
796            }) => {
797                // NOTE: Commission update events are supported only in protocol
798                // version V4 and stake table contract V2.
799                if newCommission > COMMISSION_BASIS_POINTS {
800                    return Err(StakeTableError::InvalidCommission(validator, newCommission));
801                }
802
803                // NOTE: currently we are not enforcing changes to the
804                // commission increase rates and leave this enforcement to the
805                // stake table contract.
806                let val = self
807                    .validators
808                    .get_mut(&validator)
809                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
810                val.commission = newCommission;
811            },
812
813            StakeTableEvent::RegisterV3(ref reg) => {
814                let (stake_table_key, state_ver_key, authenticated) = match reg.authenticate() {
815                    Ok((bls, schnorr)) => (Some(bls), Some(schnorr), true),
816                    Err(e) => {
817                        tracing::warn!(
818                            account = ?reg.account,
819                            %e,
820                            "registering unauthenticated validator",
821                        );
822                        (
823                            BLSPubKey::try_from(reg.blsVK).ok(),
824                            SchnorrPubKey::try_from(reg.schnorrVK).ok(),
825                            false,
826                        )
827                    },
828                };
829
830                let ValidatorRegisteredV3 {
831                    account,
832                    commission,
833                    x25519Key,
834                    p2pAddr,
835                    ..
836                } = reg;
837
838                let x25519_key = parse_x25519_key(x25519Key.0)
839                    .inspect_err(|e| tracing::warn!(%e, ?account, "Invalid x25519 key"))
840                    .ok();
841
842                // Parse p2p addr
843                let p2p_addr = match p2pAddr.parse::<NetAddr>() {
844                    Ok(addr) => Some(addr),
845                    Err(e) => {
846                        if !p2pAddr.is_empty() {
847                            tracing::warn!(%e, account = ?account, "Failed to parse p2p addr");
848                        }
849                        None
850                    },
851                };
852
853                if self.validator_exits.contains(account) {
854                    return Err(StakeTableError::ValidatorAlreadyExited(*account));
855                }
856
857                let entry = self.validators.entry(*account);
858                if let indexmap::map::Entry::Occupied(_) = entry {
859                    return Err(StakeTableError::AlreadyRegistered(*account));
860                }
861
862                // Unparsable keys aren't deduped; the L1 contract dedups by raw G2 bytes.
863                if let Some(k) = stake_table_key.as_ref()
864                    && self.used_bls_keys.contains(k)
865                {
866                    return Err(StakeTableError::BlsKeyAlreadyUsed(k.to_string()));
867                }
868
869                if let Some(k) = x25519_key.as_ref()
870                    && self.used_x25519_keys.contains(k)
871                {
872                    return Err(StakeTableError::X25519KeyAlreadyUsed(k.to_string()));
873                }
874
875                let (state_ver_key, authenticated) = match state_ver_key {
876                    Some(k) if self.used_schnorr_keys.contains(&k) => {
877                        tracing::warn!(
878                            ?account,
879                            schnorr_key = %k,
880                            "registering unauthenticated validator (schnorr key already used)"
881                        );
882                        (None, false)
883                    },
884                    key => (key, authenticated),
885                };
886
887                // All checks ok, applying changes
888                if let Some(k) = stake_table_key.as_ref() {
889                    self.used_bls_keys.insert(*k);
890                }
891                if let Some(k) = state_ver_key.as_ref() {
892                    self.used_schnorr_keys.insert(k.clone());
893                }
894                if let Some(k) = x25519_key.as_ref() {
895                    self.used_x25519_keys.insert(*k);
896                }
897
898                entry.or_insert(RegisteredValidator {
899                    account: *account,
900                    stake_table_key,
901                    state_ver_key,
902                    stake: U256::ZERO,
903                    commission: *commission,
904                    delegators: HashMap::new(),
905                    authenticated,
906                    x25519_key,
907                    p2p_addr,
908                });
909            },
910
911            StakeTableEvent::X25519KeyUpdate(X25519KeyUpdated {
912                validator,
913                x25519Key,
914            }) => {
915                let val = self
916                    .validators
917                    .get_mut(&validator)
918                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
919
920                let x25519_key = parse_x25519_key(x25519Key.0)
921                    .inspect_err(|e| tracing::warn!(%e, ?validator, "Invalid x25519 key"))
922                    .ok();
923
924                if let Some(key) = x25519_key {
925                    if self.used_x25519_keys.contains(&key) {
926                        return Err(StakeTableError::X25519KeyAlreadyUsed(key.to_string()));
927                    }
928                    self.used_x25519_keys.insert(key);
929                }
930                val.x25519_key = x25519_key;
931            },
932
933            StakeTableEvent::P2pAddrUpdate(P2pAddrUpdated {
934                validator,
935                ref p2pAddr,
936            }) => {
937                let val = self
938                    .validators
939                    .get_mut(&validator)
940                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
941
942                match p2pAddr.parse::<NetAddr>() {
943                    Ok(addr) => val.p2p_addr = Some(addr),
944                    Err(e) => {
945                        tracing::warn!(%e, validator = ?validator, "Failed to parse p2p addr");
946                        val.p2p_addr = None;
947                    },
948                }
949            },
950        }
951
952        Ok(Ok(()))
953    }
954}
955
956/// The contract enforces canonical nonzero encodings, so an error here means a
957/// contract bug. Callers degrade it to `None` instead of aborting (defense in
958/// depth): the key only affects p2p networking, not consensus safety, so this
959/// is preferred over halting stake table processing on every node. The zero
960/// check is explicit because the Rust parser accepts the zero encoding.
961fn parse_x25519_key(bytes: [u8; 32]) -> anyhow::Result<x25519::PublicKey> {
962    ensure!(bytes != [0u8; 32], "zero key");
963    Ok(x25519::PublicKey::try_from(bytes.as_slice())?)
964}
965
966pub fn validators_from_l1_events<I: Iterator<Item = StakeTableEvent>>(
967    events: I,
968) -> Result<(RegisteredValidatorMap, StakeTableHash), StakeTableError> {
969    let mut state = StakeTableState::default();
970    for event in events {
971        match state.apply_event(event.clone()) {
972            Ok(Ok(())) => {
973                // Event successfully applied
974            },
975            Ok(Err(expected_err)) => {
976                // Expected error, dont change the state
977                tracing::warn!("Expected error while applying event {event:?}: {expected_err}");
978            },
979            Err(err) => {
980                tracing::error!("Fatal error in applying event {event:?}: {err}");
981                return Err(err);
982            },
983        }
984    }
985    let commit = state.commit();
986    Ok((state.into_validators(), commit))
987}
988
989/// Select active validators
990///
991/// Filters out unauthenticated validator candidates, those without stake, and selects
992/// the top [`MAX_VALIDATORS`] staked validators.
993/// Returns a new AuthenticatedValidatorMap containing only the selected validators.
994pub(crate) fn select_active_validator_set(
995    candidates: &RegisteredValidatorMap,
996    protocol_version: Version,
997) -> Result<AuthenticatedValidatorMap, StakeTableError> {
998    let total_candidates = candidates.len();
999
1000    let valid_validators: AuthenticatedValidatorMap = candidates
1001        .iter()
1002        .filter_map(
1003            |(address, validator)| match AuthenticatedValidator::try_from(validator) {
1004                Err(e) => {
1005                    tracing::debug!("{e}");
1006                    None
1007                },
1008                Ok(cv) => {
1009                    if cv.delegators.is_empty() {
1010                        tracing::debug!("Validator {address:?} does not have any delegator");
1011                        return None;
1012                    }
1013                    if cv.stake.is_zero() {
1014                        tracing::debug!("Validator {address:?} does not have any stake");
1015                        return None;
1016                    }
1017                    if let Err(err) = cv.ensure_eligible(protocol_version) {
1018                        tracing::debug!(
1019                            ?address,
1020                            %err,
1021                            "Validator not eligible at protocol version {protocol_version}"
1022                        );
1023                        return None;
1024                    }
1025                    Some((*address, cv))
1026                },
1027            },
1028        )
1029        .collect();
1030
1031    tracing::debug!(
1032        total_candidates,
1033        filtered = valid_validators.len(),
1034        "Filtered out invalid validators"
1035    );
1036
1037    if valid_validators.is_empty() {
1038        tracing::warn!("Validator selection failed: no validators passed minimum criteria");
1039        return Err(StakeTableError::NoValidValidators);
1040    }
1041
1042    let maximum_stake = valid_validators.values().map(|v| v.stake).max().unwrap();
1043
1044    let minimum_stake = maximum_stake
1045        .checked_div(U256::from(VID_TARGET_TOTAL_STAKE))
1046        .ok_or_else(|| {
1047            tracing::error!("Overflow while calculating minimum stake threshold");
1048            StakeTableError::MinimumStakeOverflow
1049        })?;
1050
1051    let mut valid_stakers: Vec<_> = valid_validators
1052        .iter()
1053        .filter(|(_, v)| v.stake >= minimum_stake)
1054        .map(|(addr, v)| (*addr, v.stake))
1055        .collect();
1056
1057    tracing::info!(
1058        count = valid_stakers.len(),
1059        "Number of validators above minimum stake threshold"
1060    );
1061
1062    // Sort by stake (descending order)
1063    valid_stakers.sort_by_key(|(_, stake)| std::cmp::Reverse(*stake));
1064
1065    if valid_stakers.len() > MAX_VALIDATORS {
1066        valid_stakers.truncate(MAX_VALIDATORS);
1067    }
1068
1069    let selected_addresses: HashSet<_> = valid_stakers.iter().map(|(addr, _)| *addr).collect();
1070    let selected_validators: AuthenticatedValidatorMap = valid_validators
1071        .into_iter()
1072        .filter(|(address, _)| selected_addresses.contains(address))
1073        .collect();
1074
1075    tracing::info!(
1076        final_count = selected_validators.len(),
1077        "Selected active validator set"
1078    );
1079
1080    Ok(selected_validators)
1081}
1082
1083#[derive(Clone, Debug)]
1084pub struct ValidatorSet {
1085    pub(crate) all_validators: RegisteredValidatorMap,
1086    pub(crate) active_validators: AuthenticatedValidatorMap,
1087    pub(crate) stake_table_hash: Option<StakeTableHash>,
1088    /// The protocol version at which `active_validators` was selected.
1089    pub(crate) protocol_version: Version,
1090}
1091
1092impl ValidatorSet {
1093    /// Derive a validator set from a stake-table state at a given protocol version.
1094    pub fn from_state(
1095        state: &StakeTableState,
1096        protocol_version: Version,
1097    ) -> Result<Self, StakeTableError> {
1098        let active_validators = select_active_validator_set(state.validators(), protocol_version)?;
1099        Ok(Self {
1100            all_validators: state.validators().clone(),
1101            active_validators,
1102            stake_table_hash: Some(state.commit()),
1103            protocol_version,
1104        })
1105    }
1106
1107    /// Derive a validator set directly from L1 events.
1108    pub fn from_l1_events<I: Iterator<Item = StakeTableEvent>>(
1109        events: I,
1110        protocol_version: Version,
1111    ) -> Result<Self, StakeTableError> {
1112        let (all_validators, stake_table_hash) = validators_from_l1_events(events)?;
1113        let active_validators = select_active_validator_set(&all_validators, protocol_version)?;
1114        Ok(Self {
1115            all_validators,
1116            active_validators,
1117            stake_table_hash: Some(stake_table_hash),
1118            protocol_version,
1119        })
1120    }
1121
1122    /// All registered validators known when this set was derived.
1123    pub fn all_validators(&self) -> &RegisteredValidatorMap {
1124        &self.all_validators
1125    }
1126
1127    /// Validators selected to participate in consensus at `protocol_version`.
1128    pub fn active_validators(&self) -> &AuthenticatedValidatorMap {
1129        &self.active_validators
1130    }
1131
1132    /// Commitment of the underlying stake-table state, if known.
1133    pub fn stake_table_hash(&self) -> Option<StakeTableHash> {
1134        self.stake_table_hash
1135    }
1136
1137    /// Protocol version at which the active set was selected.
1138    pub fn protocol_version(&self) -> Version {
1139        self.protocol_version
1140    }
1141}
1142
1143impl std::fmt::Debug for StakeTableEvent {
1144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1145        match self {
1146            StakeTableEvent::Register(event) => write!(f, "Register({:?})", event.account),
1147            StakeTableEvent::RegisterV2(event) => write!(f, "RegisterV2({:?})", event.account),
1148            StakeTableEvent::Deregister(event) => write!(f, "Deregister({:?})", event.validator),
1149            StakeTableEvent::DeregisterV2(event) => {
1150                write!(f, "DeregisterV2({:?})", event.validator)
1151            },
1152            StakeTableEvent::Delegate(event) => write!(f, "Delegate({:?})", event.delegator),
1153            StakeTableEvent::Undelegate(event) => write!(f, "Undelegate({:?})", event.delegator),
1154            StakeTableEvent::UndelegateV2(event) => {
1155                write!(f, "UndelegateV2({:?})", event.delegator)
1156            },
1157            StakeTableEvent::KeyUpdate(event) => write!(f, "KeyUpdate({:?})", event.account),
1158            StakeTableEvent::KeyUpdateV2(event) => write!(f, "KeyUpdateV2({:?})", event.account),
1159            StakeTableEvent::CommissionUpdate(event) => {
1160                write!(f, "CommissionUpdate({:?})", event.validator)
1161            },
1162            StakeTableEvent::RegisterV3(event) => {
1163                write!(f, "RegisterV3({:?})", event.account)
1164            },
1165            StakeTableEvent::X25519KeyUpdate(event) => {
1166                write!(f, "X25519KeyUpdate({:?})", event.validator)
1167            },
1168            StakeTableEvent::P2pAddrUpdate(event) => {
1169                write!(f, "P2pAddrUpdate({:?})", event.validator)
1170            },
1171        }
1172    }
1173}
1174
1175/// ESP token initial supply on Espresso mainnet, in wei (18 decimals).
1176const MAINNET_INITIAL_SUPPLY_WEI: u128 = 3_590_000_000_000_000_000_000_000_000;
1177/// ESP token initial supply on the Decaf testnet, in wei (18 decimals).
1178const DECAF_INITIAL_SUPPLY_WEI: u128 = 10_000_000_000_000_000_000_000_000_000;
1179
1180/// Attempts to persist a batch of fetched L1 events before giving up.
1181#[cfg_attr(not(feature = "node"), allow(dead_code))]
1182const STORE_EVENTS_ATTEMPTS: usize = 5;
1183/// Delay between those attempts.
1184#[cfg_attr(not(feature = "node"), allow(dead_code))]
1185const STORE_EVENTS_RETRY_DELAY: Duration = Duration::from_secs(1);
1186
1187/// Elapsed time between progress reports during an L1 event scan, checked between chunks.
1188#[cfg_attr(not(feature = "node"), allow(dead_code))]
1189const PROGRESS_INTERVAL: Duration = Duration::from_secs(30);
1190
1191const MAINNET_STAKE_TABLE_CONTRACT: Address =
1192    address!("0xcef474d372b5b09defe2af187bf17338dc704451");
1193const DECAF_STAKE_TABLE_CONTRACT: Address = address!("0x40304fbe94d5e7d1492dd90c53a2d63e8506a037");
1194
1195/// Returns the ESP token's initial supply for the stake table contracts where it is already known.
1196///
1197/// The initial supply is fixed at token deployment: `fetch_and_update_initial_supply` locates
1198/// the one-time `Initialized` event and the matching mint `Transfer` from `address(0)`, which
1199/// can only occur once and can never change afterwards. Hardcoding it for known deployments
1200/// removes the last L1 dependency on the epoch-root reward-calculation path, so a node whose
1201/// L1 connection is down can still compute block rewards.
1202///
1203/// Keyed on the stake table contract as well as the chain id, so that a redeploy invalidates
1204/// the entry: the lookup misses and the node falls back to fetching from L1. The contract
1205/// address alone is not a unique key, since the same address can be deployed on more than one
1206/// L1.
1207///
1208/// A wrong constant here has no local symptom: the node keeps computing rewards, just the
1209/// wrong ones, diverging from the rest of the network and losing consensus.
1210/// `assert_known_initial_supply_matches_l1` (below, in tests) guards against this by fetching
1211/// the value from L1 and comparing it to the constant.
1212fn known_initial_supply(chain_id: ChainId, stake_table_contract: Address) -> Option<U256> {
1213    if chain_id == MAINNET_CHAIN_ID && stake_table_contract == MAINNET_STAKE_TABLE_CONTRACT {
1214        Some(U256::from(MAINNET_INITIAL_SUPPLY_WEI))
1215    } else if chain_id == DECAF_CHAIN_ID && stake_table_contract == DECAF_STAKE_TABLE_CONTRACT {
1216        Some(U256::from(DECAF_INITIAL_SUPPLY_WEI))
1217    } else {
1218        None
1219    }
1220}
1221
1222impl Fetcher {
1223    #[cfg(feature = "node")]
1224    pub fn new(
1225        peers: Arc<dyn StateCatchup>,
1226        persistence: Arc<AsyncMutex<dyn MembershipPersistence>>,
1227        l1_client: L1Client,
1228        chain_config: ChainConfig,
1229    ) -> Self {
1230        Self {
1231            peers,
1232            persistence,
1233            l1_client,
1234            chain_config: Arc::new(AsyncMutex::new(chain_config)),
1235            update_task: StakeTableUpdateTask(AsyncMutex::new(None)).into(),
1236            initial_supply: Arc::new(AsyncRwLock::new(None)),
1237        }
1238    }
1239
1240    #[cfg(feature = "node")]
1241    pub async fn spawn_update_loop(&self) {
1242        let mut update_task = self.update_task.0.lock().await;
1243        if update_task.is_none() {
1244            *update_task = Some(spawn(self.update_loop()));
1245        }
1246    }
1247
1248    /// Periodically updates the stake table from the L1 contract.
1249    /// This function polls the finalized block number from the L1 client at an interval
1250    /// and fetches stake table from contract
1251    /// and updates the persistence
1252    #[cfg(feature = "node")]
1253    fn update_loop(&self) -> impl Future<Output = ()> + use<> {
1254        let span = tracing::warn_span!("Stake table update loop");
1255        let self_clone = self.clone();
1256        let state = self.l1_client.state.clone();
1257        let l1_retry = self.l1_client.options().l1_retry_delay;
1258        let update_delay = self.l1_client.options().stake_table_update_interval;
1259        let chain_config = self.chain_config.clone();
1260
1261        async move {
1262            // Get the stake table contract address from the chain config.
1263            // This may not contain a stake table address if we are on a pre-epoch version.
1264            // It keeps retrying until the chain config is upgraded
1265            // after a successful upgrade to an epoch version.
1266            let stake_contract_address = loop {
1267                let contract = chain_config.lock().await.stake_table_contract;
1268                match contract {
1269                    Some(addr) => break addr,
1270                    None => {
1271                        tracing::debug!(
1272                            "Stake table contract address not found. Retrying in {l1_retry:?}...",
1273                        );
1274                    },
1275                }
1276                sleep(l1_retry).await;
1277            };
1278
1279            // Begin the main polling loop
1280            loop {
1281                let finalized_block = loop {
1282                    let last_finalized = state.lock().await.last_finalized;
1283                    if let Some(block) = last_finalized {
1284                        break block;
1285                    }
1286                    tracing::debug!("Finalized block not yet available. Retrying in {l1_retry:?}",);
1287                    sleep(l1_retry).await;
1288                };
1289
1290                tracing::debug!("Attempting to fetch stake table at L1 block {finalized_block:?}",);
1291
1292                loop {
1293                    match self_clone
1294                        .fetch_and_store_stake_table_events(stake_contract_address, finalized_block)
1295                        .await
1296                    {
1297                        Ok(events) => {
1298                            tracing::info!(
1299                                "Successfully fetched and stored stake table events at \
1300                                 block={finalized_block:?}"
1301                            );
1302                            tracing::debug!("events={events:?}");
1303                            break;
1304                        },
1305                        Err(e) => {
1306                            tracing::error!(
1307                                "Error fetching stake table at block {finalized_block:?}. err= \
1308                                 {e:#}",
1309                            );
1310                            sleep(l1_retry).await;
1311                        },
1312                    }
1313                }
1314
1315                tracing::debug!("Waiting {update_delay:?} before next stake table update...",);
1316                sleep(update_delay).await;
1317            }
1318        }
1319        .instrument(span)
1320    }
1321
1322    /// Get `StakeTable` at specific l1 block height.
1323    /// This function fetches and processes various events (ValidatorRegistered, ValidatorExit,
1324    /// Delegated, Undelegated, and ConsensusKeysUpdated) within the block range from the
1325    /// contract's initialization block to the provided `to_block` value.
1326    /// Events are fetched in chunks and retries are implemented for failed requests.
1327    /// Only new events fetched from L1 are stored in persistence.
1328    #[cfg(feature = "node")]
1329    pub async fn fetch_and_store_stake_table_events(
1330        &self,
1331        contract: Address,
1332        to_block: u64,
1333    ) -> anyhow::Result<Vec<(EventKey, StakeTableEvent)>> {
1334        let (read_l1_offset, persistence_events) = {
1335            let persistence_lock = self.persistence.lock().await;
1336            persistence_lock.load_events(0, to_block).await?
1337        };
1338
1339        tracing::info!("loaded events from storage to_block={to_block:?}");
1340
1341        // No need to fetch from contract
1342        // if persistence returns all the events that we need
1343        if let Some(EventsPersistenceRead::Complete) = read_l1_offset {
1344            return Ok(persistence_events);
1345        }
1346
1347        let from_block = read_l1_offset
1348            .map(|read| match read {
1349                EventsPersistenceRead::UntilL1Block(block) => Ok(block + 1),
1350                EventsPersistenceRead::Complete => Err(anyhow::anyhow!(
1351                    "Unexpected state. offset is complete after returning early"
1352                )),
1353            })
1354            .transpose()?;
1355
1356        ensure!(
1357            Some(to_block) >= from_block,
1358            "to_block {to_block:?} is less than from_block {from_block:?}"
1359        );
1360
1361        tracing::info!(%to_block, from_block = ?from_block, "Fetching events from contract");
1362
1363        let contract_events = Self::fetch_events_from_contract(
1364            self.l1_client.clone(),
1365            contract,
1366            from_block,
1367            to_block,
1368        )
1369        .await?;
1370
1371        // Store only the new events fetched from L1 contract
1372        tracing::info!(
1373            "storing {} new events in storage to_block={to_block:?}",
1374            contract_events.len()
1375        );
1376        // A failed write discards the whole scan, which on a cold start is hours of refetching.
1377        // The SQL backend only retries serialization conflicts, not transient write failures.
1378        for attempt in 1..=STORE_EVENTS_ATTEMPTS {
1379            let result = {
1380                let persistence_lock = self.persistence.lock().await;
1381                persistence_lock
1382                    .store_events(to_block, contract_events.clone())
1383                    .await
1384            };
1385            match result {
1386                Ok(()) => break,
1387                Err(e) if attempt < STORE_EVENTS_ATTEMPTS => {
1388                    tracing::warn!(attempt, %e, "failed to store stake table events, retrying");
1389                    sleep(STORE_EVENTS_RETRY_DELAY).await;
1390                },
1391                Err(e) => return Err(e.context("storing stake table events")),
1392            }
1393        }
1394
1395        let mut events = match from_block {
1396            Some(_) => persistence_events
1397                .into_iter()
1398                .chain(contract_events)
1399                .collect(),
1400            None => contract_events,
1401        };
1402
1403        // There are no duplicates because the RPC returns all events,
1404        // which are stored directly in persistence as is.
1405        // However, this step is taken as a precaution.
1406        // The vector is already sorted above, so this should be fast.
1407        let len_before_dedup = events.len();
1408        events.dedup();
1409        let len_after_dedup = events.len();
1410        if len_before_dedup != len_after_dedup {
1411            tracing::warn!("Duplicate events found and removed. This should not normally happen.")
1412        }
1413
1414        Ok(events)
1415    }
1416
1417    /// Validate a stake table event.
1418    ///
1419    /// Returns:
1420    /// - `Ok(true)` if the event is valid and should be processed
1421    /// - `Ok(false)` if the event should be skipped (non-fatal error)
1422    /// - `Err(StakeTableError)` if a fatal error occurs
1423    #[cfg_attr(not(feature = "node"), allow(dead_code))]
1424    fn validate_event(event: &StakeTableV3Events, log: &Log) -> Result<bool, StakeTableError> {
1425        match event {
1426            StakeTableV3Events::ConsensusKeysUpdatedV2(evt) => {
1427                if let Err(err) = evt.authenticate() {
1428                    tracing::warn!(
1429                        %err,
1430                        "Failed to authenticate ConsensusKeysUpdatedV2 event: {}",
1431                        log.display()
1432                    );
1433                    return Ok(false);
1434                }
1435            },
1436            StakeTableV3Events::CommissionUpdated(evt)
1437                if evt.newCommission > COMMISSION_BASIS_POINTS =>
1438            {
1439                return Err(StakeTableError::InvalidCommission(
1440                    evt.validator,
1441                    evt.newCommission,
1442                ));
1443            },
1444            _ => {},
1445        }
1446
1447        Ok(true)
1448    }
1449
1450    /// Break a block range into fixed-size chunks.
1451    #[cfg_attr(not(feature = "node"), allow(dead_code))]
1452    fn block_range_chunks(
1453        from_block: u64,
1454        to_block: u64,
1455        chunk_size: u64,
1456    ) -> impl Iterator<Item = (u64, u64)> {
1457        let mut start = from_block;
1458        let end = to_block;
1459        std::iter::from_fn(move || {
1460            let chunk_end = min(start + chunk_size - 1, end);
1461            if chunk_end < start {
1462                return None;
1463            }
1464            let chunk = (start, chunk_end);
1465            start = chunk_end + 1;
1466            Some(chunk)
1467        })
1468    }
1469
1470    /// Break a block range into fixed-size chunks, highest first.
1471    ///
1472    /// Both bounds of every chunk are inclusive, as `eth_getLogs` treats them, so a chunk spans
1473    /// `chunk_size` blocks rather than `chunk_size + 1`.
1474    #[cfg_attr(not(feature = "node"), allow(dead_code))]
1475    fn block_range_chunks_rev(
1476        from_block: u64,
1477        to_block: u64,
1478        chunk_size: u64,
1479    ) -> impl Iterator<Item = (u64, u64)> {
1480        let chunk_size = chunk_size.max(1);
1481        let mut end = to_block;
1482        let mut exhausted = to_block < from_block;
1483        std::iter::from_fn(move || {
1484            if exhausted {
1485                return None;
1486            }
1487            let chunk_end = end;
1488            let chunk_start = max(from_block, chunk_end.saturating_sub(chunk_size - 1));
1489            exhausted = chunk_start == from_block;
1490            end = chunk_start.saturating_sub(1);
1491            Some((chunk_start, chunk_end))
1492        })
1493    }
1494
1495    /// Fetch all stake table events from L1
1496    #[cfg(feature = "node")]
1497    pub async fn fetch_events_from_contract(
1498        l1_client: L1Client,
1499        contract: Address,
1500        from_block: Option<u64>,
1501        to_block: u64,
1502    ) -> Result<Vec<(EventKey, StakeTableEvent)>, StakeTableError> {
1503        let stake_table_contract = StakeTableV3::new(contract, l1_client.provider.clone());
1504        let max_retry_duration = l1_client.options().l1_events_max_retry_duration;
1505        let retry_delay = l1_client.options().l1_retry_delay;
1506        // get the block number when the contract was initialized
1507        // to avoid fetching events from block number 0
1508        let from_block = match from_block {
1509            Some(block) => block,
1510            None => {
1511                let start = Instant::now();
1512                loop {
1513                    match stake_table_contract.initializedAtBlock().call().await {
1514                        Ok(init_block) => break init_block.to::<u64>(),
1515                        Err(err) => {
1516                            if start.elapsed() >= max_retry_duration {
1517                                panic!(
1518                                    "Failed to retrieve initial block after `{}`: {err}",
1519                                    format_duration(max_retry_duration)
1520                                );
1521                            }
1522                            tracing::warn!(%err, "Failed to retrieve initial block, retrying...");
1523                            sleep(retry_delay).await;
1524                        },
1525                    }
1526                }
1527            },
1528        };
1529
1530        // To avoid making large RPC calls, divide the range into smaller chunks.
1531        // chunk size is from env "ESPRESSO_L1_EVENTS_MAX_BLOCK_RANGE
1532        // default value  is `10000` if env variable is not set
1533        let chunk_size = l1_client.options().l1_events_max_block_range;
1534        let chunks = Self::block_range_chunks(from_block, to_block, chunk_size);
1535        let scan_start = Instant::now();
1536        let mut last_report = Instant::now();
1537
1538        let mut events = vec![];
1539
1540        for (chunk, (from, to)) in chunks.enumerate() {
1541            let provider = l1_client.provider.clone();
1542
1543            tracing::debug!(from, to, "fetch all stake table events in range");
1544            // fetch events
1545            // retry if the call to the provider to fetch the events fails
1546            let logs: Vec<Log> = retry(
1547                retry_delay,
1548                max_retry_duration,
1549                "stake table events fetch",
1550                move || {
1551                    let provider = provider.clone();
1552
1553                    Box::pin(async move {
1554                        provider
1555                            .get_logs(&Self::events_filter(contract, from, to))
1556                            .await
1557                    })
1558                },
1559            )
1560            .await;
1561
1562            events.extend(Self::decode_events(logs)?);
1563
1564            // An up to date node fetches one chunk and finishes before the first report.
1565            if last_report.elapsed() >= PROGRESS_INTERVAL {
1566                last_report = Instant::now();
1567                tracing::info!(
1568                    target: "announce",
1569                    chunks = chunk + 1,
1570                    at_block = to,
1571                    to_block,
1572                    events = events.len(),
1573                    elapsed_secs = scan_start.elapsed().as_secs(),
1574                    "scanning stake table event history"
1575                );
1576            }
1577        }
1578
1579        sort_stake_table_events(events).map_err(Into::into)
1580    }
1581
1582    /// The filter matching every stake table event the fetcher understands.
1583    #[cfg(feature = "node")]
1584    fn events_filter(contract: Address, from_block: u64, to_block: u64) -> Filter {
1585        Filter::new()
1586            .events([
1587                ValidatorRegistered::SIGNATURE,
1588                ValidatorRegisteredV2::SIGNATURE,
1589                ValidatorRegisteredV3::SIGNATURE,
1590                ValidatorExit::SIGNATURE,
1591                ValidatorExitV2::SIGNATURE,
1592                Delegated::SIGNATURE,
1593                Undelegated::SIGNATURE,
1594                UndelegatedV2::SIGNATURE,
1595                ConsensusKeysUpdated::SIGNATURE,
1596                ConsensusKeysUpdatedV2::SIGNATURE,
1597                CommissionUpdated::SIGNATURE,
1598                X25519KeyUpdated::SIGNATURE,
1599                P2pAddrUpdated::SIGNATURE,
1600            ])
1601            .address(contract)
1602            .from_block(from_block)
1603            .to_block(to_block)
1604    }
1605
1606    /// Decode logs, dropping the ones that fail authentication.
1607    #[cfg(feature = "node")]
1608    fn decode_events(logs: Vec<Log>) -> Result<Vec<(StakeTableV3Events, Log)>, StakeTableError> {
1609        logs.into_iter()
1610            .filter_map(|log| {
1611                let event =
1612                    StakeTableV3Events::decode_raw_log(log.topics(), &log.data().data).ok()?;
1613                match Self::validate_event(&event, &log) {
1614                    Ok(true) => Some(Ok((event, log))),
1615                    Ok(false) => None,
1616                    Err(e) => Some(Err(e)),
1617                }
1618            })
1619            .collect()
1620    }
1621
1622    /// Fetch all stake table events in a single request.
1623    ///
1624    /// Unlike [`Self::fetch_events_from_contract`] this neither splits the range into chunks nor
1625    /// retries, so a provider that caps the block range or the result size fails immediately
1626    /// instead of after the retry budget expires. Callers that can fall back to the chunked
1627    /// fetcher use this to avoid paying for chunks against a provider that does not need them.
1628    #[cfg(feature = "node")]
1629    pub async fn try_fetch_events_from_contract(
1630        l1_client: L1Client,
1631        contract: Address,
1632        from_block: u64,
1633        to_block: u64,
1634    ) -> anyhow::Result<Vec<(EventKey, StakeTableEvent)>> {
1635        let logs = l1_client
1636            .provider
1637            .get_logs(&Self::events_filter(contract, from_block, to_block))
1638            .await?;
1639        Ok(sort_stake_table_events(Self::decode_events(logs)?)?)
1640    }
1641
1642    // Only used by staking CLI which doesn't have persistence
1643    #[cfg(feature = "node")]
1644    pub async fn fetch_all_validators_from_contract(
1645        l1_client: L1Client,
1646        contract: Address,
1647        to_block: u64,
1648    ) -> anyhow::Result<(RegisteredValidatorMap, StakeTableHash)> {
1649        let events = Self::fetch_events_from_contract(l1_client, contract, None, to_block).await?;
1650
1651        // Process the sorted events and return the resulting stake table.
1652        validators_from_l1_events(events.into_iter().map(|(_, e)| e))
1653            .context("failed to construct validators set from l1 events")
1654    }
1655
1656    /// Returns the initial token supply, fetching it from L1 if not yet known.
1657    ///
1658    /// Known deployments resolve immediately via `known_initial_supply`, without touching the
1659    /// cache or L1. Unknown ones fall back to the cache, then an L1 fetch.
1660    #[cfg(feature = "node")]
1661    pub async fn initial_supply_or_fetch(&self) -> Result<U256, FetchRewardError> {
1662        if let Some(supply) = self.known_initial_supply().await {
1663            return Ok(supply);
1664        }
1665
1666        // `fetch_and_update_initial_supply` needs a write lock, create temporary to drop lock
1667        let supply = *self.initial_supply.read().await;
1668        match supply {
1669            Some(supply) => Ok(supply),
1670            None => self.fetch_and_update_initial_supply().await,
1671        }
1672    }
1673
1674    /// Returns the initial token supply, from the hardcoded table or the pre-populated cache.
1675    #[cfg(not(feature = "node"))]
1676    pub async fn initial_supply_or_fetch(&self) -> Result<U256, FetchRewardError> {
1677        if let Some(supply) = self.known_initial_supply().await {
1678            return Ok(supply);
1679        }
1680
1681        Ok((*self.initial_supply.read().await).expect("initial supply pre-populated"))
1682    }
1683
1684    /// Returns the hardcoded initial supply for the configured chain and stake table contract,
1685    /// if known.
1686    async fn known_initial_supply(&self) -> Option<U256> {
1687        let chain_config = *self.chain_config.lock().await;
1688        known_initial_supply(chain_config.chain_id, chain_config.stake_table_contract?)
1689    }
1690
1691    /// Calculates the fixed block reward based on the token's initial supply,
1692    /// obtained via `initial_supply_or_fetch`.
1693    #[cfg(feature = "node")]
1694    pub async fn fetch_fixed_block_reward(&self) -> Result<RewardAmount, FetchRewardError> {
1695        let initial_supply = self.initial_supply_or_fetch().await?;
1696
1697        let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
1698            .checked_div(U256::from(COMMISSION_BASIS_POINTS))
1699            .ok_or(FetchRewardError::DivisionByZero(
1700                "COMMISSION_BASIS_POINTS is zero",
1701            ))?;
1702
1703        Ok(RewardAmount(reward))
1704    }
1705
1706    /// Fetches and updates the initial token supply.
1707    ///
1708    /// - Locates the `Initialized` event of the token contract (emitted only once).
1709    /// - Queries `Transfer` events in the same block, matching by transaction hash and
1710    ///   `from == address(0)` to find the initial mint.
1711    /// - If either step fails, the function aborts to prevent incorrect reward calculations.
1712    ///
1713    /// This avoids fetching transaction receipts, which may be unavailable on pruned L1 nodes.
1714    ///
1715    /// The ESP token contract itself does not expose the initialization block
1716    /// but the stake table contract does.
1717    /// The stake table contract is deployed after the token contract as it holds the token
1718    /// contract address. We use the stake table contract initialization block as a safe upper bound
1719    /// when scanning backwards for the token contract initialization event.
1720    #[cfg(feature = "node")]
1721    pub async fn fetch_and_update_initial_supply(&self) -> Result<U256, FetchRewardError> {
1722        tracing::info!("Fetching token initial supply");
1723        let chain_config = *self.chain_config.lock().await;
1724
1725        let stake_table_contract = chain_config
1726            .stake_table_contract
1727            .ok_or(FetchRewardError::MissingStakeTableContract)?;
1728
1729        let provider = self.l1_client.provider.clone();
1730        let stake_table = StakeTableV3::new(stake_table_contract, provider.clone());
1731
1732        // Get the block number where the stake table was initialized
1733        // Stake table contract has the token contract address
1734        // so the token contract is deployed before the stake table contract
1735        let stake_table_init_block = stake_table
1736            .initializedAtBlock()
1737            .block(BlockId::finalized())
1738            .call()
1739            .await
1740            .map_err(FetchRewardError::ContractCall)?
1741            .to::<u64>();
1742
1743        tracing::info!("stake table init block ={stake_table_init_block}");
1744
1745        let token_address = stake_table
1746            .token()
1747            .block(BlockId::finalized())
1748            .call()
1749            .await
1750            .map_err(FetchRewardError::TokenAddressFetch)?;
1751
1752        let token = EspToken::new(token_address, provider.clone());
1753
1754        // Fetch the `Initialized` event (emitted once during token contract init).
1755        // Falls back to scanning over a fixed block range if the full-range query fails.
1756        let init_logs = token
1757            .Initialized_filter()
1758            .from_block(0u64)
1759            .to_block(BlockNumberOrTag::Finalized)
1760            .query()
1761            .await;
1762
1763        let init_log = match init_logs {
1764            Ok(init_logs) => {
1765                if init_logs.is_empty() {
1766                    tracing::error!(
1767                        "Token Initialized event logs are empty. This should never happen"
1768                    );
1769                    return Err(FetchRewardError::MissingInitializedEvent);
1770                }
1771
1772                let (_, init_log) = init_logs[0].clone();
1773
1774                tracing::debug!(tx_hash = ?init_log.transaction_hash, "Found token `Initialized` event");
1775                init_log
1776            },
1777            Err(err) => {
1778                tracing::warn!(
1779                    "RPC returned error {err:?}. will fallback to scanning over fixed block range"
1780                );
1781                self.scan_token_contract_initialized_event_log(
1782                    stake_table_init_block,
1783                    token.clone(),
1784                )
1785                .await?
1786            },
1787        };
1788
1789        let init_block = init_log
1790            .block_number
1791            .ok_or(FetchRewardError::MissingBlockNumber)?;
1792
1793        let init_tx_hash =
1794            init_log
1795                .transaction_hash
1796                .ok_or_else(|| FetchRewardError::MissingTransactionHash {
1797                    init_log: init_log.clone().into(),
1798                })?;
1799
1800        // Query Transfer events in the initialization block instead of fetching
1801        // the transaction receipt, which pruned L1 nodes may not have.
1802        // Match by transaction hash to scope to the exact initialization tx.
1803        let transfer_logs = token
1804            .Transfer_filter()
1805            .from_block(init_block)
1806            .to_block(init_block)
1807            .query()
1808            .await
1809            .map_err(FetchRewardError::TransferEventQuery)?;
1810
1811        let (mint_transfer, _) = transfer_logs
1812            .iter()
1813            .find(|(transfer, log)| {
1814                log.transaction_hash == Some(init_tx_hash) && transfer.from == Address::ZERO
1815            })
1816            .ok_or(FetchRewardError::MissingTransferEvent)?;
1817
1818        tracing::debug!("mint transfer event ={mint_transfer:?}");
1819
1820        let initial_supply = mint_transfer.value;
1821
1822        tracing::info!("Initial token amount: {} ESP", format_ether(initial_supply));
1823
1824        let mut writer = self.initial_supply.write().await;
1825        *writer = Some(initial_supply);
1826
1827        Ok(initial_supply)
1828    }
1829
1830    /// Scans backwards in fixed-size block ranges to locate the `Initialized` event of the token contract.
1831    ///
1832    /// This is a fallback method used when querying the full block range for the `Initialized` event fails
1833    ///
1834    /// Starting from the stake table contract’s initialization block (which comes after the token contract
1835    /// is deployed), it scans in chunks (defined by `l1_events_max_block_range`) until it finds the event
1836    /// or until a maximum number of blocks (`MAX_BLOCKS_SCANNED`) is reached.
1837    #[cfg(feature = "node")]
1838    pub async fn scan_token_contract_initialized_event_log(
1839        &self,
1840        stake_table_init_block: u64,
1841        token: EspTokenInstance<L1Provider>,
1842    ) -> Result<Log, FetchRewardError> {
1843        let max_events_range = self.l1_client.options().l1_events_max_block_range;
1844        const MAX_BLOCKS_SCANNED: u64 = 200_000;
1845
1846        let lowest_block = stake_table_init_block.saturating_sub(MAX_BLOCKS_SCANNED - 1);
1847        let chunks =
1848            Self::block_range_chunks_rev(lowest_block, stake_table_init_block, max_events_range);
1849
1850        for (from_block, to_block) in chunks {
1851            let init_logs = token
1852                .Initialized_filter()
1853                .from_block(from_block)
1854                .to_block(to_block)
1855                .query()
1856                .await
1857                .map_err(FetchRewardError::ScanQueryFailed)?;
1858
1859            if !init_logs.is_empty() {
1860                let (_, init_log) = init_logs[0].clone();
1861                tracing::info!(
1862                    from_block,
1863                    tx_hash = ?init_log.transaction_hash,
1864                    "Found token Initialized event during scan"
1865                );
1866                return Ok(init_log);
1867            }
1868        }
1869
1870        tracing::error!(
1871            lowest_block,
1872            stake_table_init_block,
1873            "Exceeded maximum scan range while searching for token Initialized event"
1874        );
1875        Err(FetchRewardError::ExceededMaxScanRange(MAX_BLOCKS_SCANNED))
1876    }
1877
1878    pub async fn update_chain_config(&self, header: &Header) -> anyhow::Result<()> {
1879        let chain_config = self.get_chain_config(header).await?;
1880        // update chain config
1881        *self.chain_config.lock().await = chain_config;
1882
1883        Ok(())
1884    }
1885
1886    #[cfg(feature = "node")]
1887    pub async fn fetch(&self, epoch: EpochNumber, header: &Header) -> anyhow::Result<ValidatorSet> {
1888        let chain_config = *self.chain_config.lock().await;
1889        let Some(address) = chain_config.stake_table_contract else {
1890            bail!("No stake table contract address found in Chain config");
1891        };
1892
1893        let Some(l1_finalized_block_info) = header.l1_finalized() else {
1894            bail!(
1895                "The epoch root for epoch {epoch} is missing the L1 finalized block info. This is \
1896                 a fatal error. Consensus is blocked and will not recover."
1897            );
1898        };
1899
1900        let events = match self
1901            .fetch_and_store_stake_table_events(address, l1_finalized_block_info.number())
1902            .await
1903            .map_err(GetStakeTablesError::L1ClientFetchError)
1904        {
1905            Ok(events) => events,
1906            Err(e) => {
1907                bail!("failed to fetch stake table events {e:?}");
1908            },
1909        };
1910
1911        // Selection runs at the epoch root header's protocol version: that header's
1912        // `next_stake_table_hash` was computed under the active-set rules in effect at the root,
1913        // so events spanning an upgrade are intentionally evaluated under the root's rules rather
1914        // than each event's wall-clock version.
1915        match ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), header.version()) {
1916            Ok(res) => Ok(res),
1917            Err(e) => {
1918                bail!("failed to construct stake table {e:?}");
1919            },
1920        }
1921    }
1922
1923    /// Retrieve and verify `ChainConfig`
1924    // TODO move to appropriate object (Header?)
1925    pub(crate) async fn get_chain_config(&self, header: &Header) -> anyhow::Result<ChainConfig> {
1926        let chain_config = self.chain_config.lock().await;
1927        let peers = self.peers.clone();
1928        let header_cf = header.chain_config();
1929        if chain_config.commit() == header_cf.commit() {
1930            return Ok(*chain_config);
1931        }
1932
1933        let cf = match header_cf.resolve() {
1934            Some(cf) => cf,
1935            None => peers
1936                .fetch_chain_config(header_cf.commit())
1937                .await
1938                .inspect_err(|err| {
1939                    tracing::error!("failed to get chain_config from peers. err: {err:?}");
1940                })?,
1941        };
1942
1943        Ok(cf)
1944    }
1945
1946    #[cfg(any(test, feature = "testing"))]
1947    pub fn mock() -> Self {
1948        use crate::{mock, v0_1::NoStorage};
1949        let chain_config = ChainConfig::default();
1950        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
1951            .expect("Failed to create L1 client");
1952
1953        let peers = Arc::new(mock::MockStateCatchup::default());
1954        let persistence = NoStorage;
1955
1956        Self::new(
1957            peers,
1958            Arc::new(AsyncMutex::new(persistence)),
1959            l1,
1960            chain_config,
1961        )
1962    }
1963}
1964
1965#[cfg_attr(not(feature = "node"), allow(dead_code))]
1966async fn retry<F, T, E>(
1967    retry_delay: Duration,
1968    max_duration: Duration,
1969    operation_name: &str,
1970    mut operation: F,
1971) -> T
1972where
1973    F: FnMut() -> BoxFuture<'static, Result<T, E>>,
1974    E: std::fmt::Display,
1975{
1976    let start = Instant::now();
1977    loop {
1978        match operation().await {
1979            Ok(result) => return result,
1980            Err(err) => {
1981                if start.elapsed() >= max_duration {
1982                    panic!(
1983                        r#"
1984                    Failed to complete operation `{operation_name}` after `{}`.
1985                    error: {err}
1986
1987
1988                    This might be caused by:
1989                    - The current block range being too large for your RPC provider.
1990                    - The event query returning more data than your RPC allows as
1991                      some RPC providers limit the number of events returned.
1992                    - RPC provider outage
1993
1994                    Suggested solution:
1995                    - Reduce the value of the environment variable
1996                      `ESPRESSO_L1_EVENTS_MAX_BLOCK_RANGE` to query smaller ranges.
1997                    - Add multiple RPC providers
1998                    - Use a different RPC provider with higher rate limits."#,
1999                        format_duration(max_duration)
2000                    );
2001                }
2002                tracing::warn!(%err, "Retrying `{operation_name}` after error");
2003                sleep(retry_delay).await;
2004            },
2005        }
2006    }
2007}
2008
2009/// Calculates the stake ratio `p` and reward rate `R(p)`.
2010///
2011/// The reward rate `R(p)` is defined as:
2012///
2013///     R(p) = {
2014///         0.03 / sqrt(2 * 0.01),         if 0 <= p <= 0.01
2015///         0.03 / sqrt(2 * p),            if 0.01 < p <= 1
2016///     }
2017///
2018pub fn calculate_proportion_staked_and_reward_rate(
2019    total_stake: &BigDecimal,
2020    total_supply: &BigDecimal,
2021) -> anyhow::Result<(BigDecimal, BigDecimal)> {
2022    if total_supply.is_zero() {
2023        return Err(anyhow::anyhow!("Total supply cannot be zero"));
2024    }
2025
2026    let proportion_staked = total_stake / total_supply;
2027
2028    if proportion_staked < BigDecimal::zero() || proportion_staked > BigDecimal::one() {
2029        return Err(anyhow::anyhow!("Stake ratio p must be in the range [0, 1]"));
2030    }
2031
2032    let two = BigDecimal::from_u32(2).unwrap();
2033    let min_stake_ratio = BigDecimal::from_str("0.01")?;
2034    let numerator = BigDecimal::from_str("0.03")?;
2035
2036    let denominator = (&two * (&proportion_staked).max(&min_stake_ratio))
2037        .sqrt()
2038        .context("Failed to compute sqrt in R(p)")?;
2039
2040    let reward_rate = numerator / denominator;
2041
2042    tracing::debug!("rp={reward_rate}");
2043
2044    Ok((proportion_staked, reward_rate))
2045}
2046
2047pub(crate) fn compute_block_reward(
2048    epoch: EpochNumber,
2049    total_supply: U256,
2050    total_stake: U256,
2051    avg_block_time_ms: u64,
2052) -> anyhow::Result<RewardAmount> {
2053    // Convert to BigDecimal for precision
2054    let total_stake_bd = BigDecimal::from_str(&total_stake.to_string())?;
2055    let total_supply_bd = BigDecimal::from_str(&(total_supply.to_string()))?;
2056
2057    tracing::debug!(?epoch, "total_stake={total_stake}");
2058    tracing::debug!(?epoch, "total_supply_bd={total_supply_bd}");
2059
2060    let (proportion, reward_rate) =
2061        calculate_proportion_staked_and_reward_rate(&total_stake_bd, &total_supply_bd)?;
2062    let inflation_rate = proportion * reward_rate;
2063
2064    tracing::debug!(?epoch, "inflation_rate={inflation_rate:?}");
2065
2066    let blocks_per_year = MILLISECONDS_PER_YEAR
2067        .checked_div(avg_block_time_ms.into())
2068        .context("avg_block_time_ms is zero")?;
2069
2070    tracing::debug!(?epoch, "blocks_per_year={blocks_per_year:?}");
2071
2072    ensure!(!blocks_per_year.is_zero(), "blocks per year is zero");
2073    let block_reward = (total_supply_bd * inflation_rate) / blocks_per_year;
2074
2075    let block_reward_u256 = U256::from_str(&block_reward.round(0).to_string())?;
2076
2077    Ok(block_reward_u256.into())
2078}
2079
2080#[derive(Error, Debug)]
2081/// Error representing fail cases for retrieving the stake table.
2082#[cfg_attr(not(feature = "node"), allow(dead_code))]
2083enum GetStakeTablesError {
2084    #[error("Error fetching from L1: {0}")]
2085    L1ClientFetchError(anyhow::Error),
2086}
2087
2088#[cfg(any(test, feature = "testing"))]
2089impl super::v0_3::StakeTable {
2090    /// Generate a `StakeTable` with `n` members.
2091    pub fn mock(n: u64) -> Self {
2092        use hotshot_types::PeerConfig;
2093
2094        use super::SeqTypes;
2095
2096        (0..n)
2097            .map(|_| PeerConfig::test_default())
2098            .collect::<Vec<PeerConfig<SeqTypes>>>()
2099            .into()
2100    }
2101}
2102
2103#[cfg(any(test, feature = "testing"))]
2104impl DAMembers {
2105    /// Generate a `DaMembers` (alias committee) with `n` members.
2106    pub fn mock(n: u64) -> Self {
2107        use hotshot_types::PeerConfig;
2108
2109        use super::SeqTypes;
2110
2111        (0..n)
2112            .map(|_| PeerConfig::test_default())
2113            .collect::<Vec<PeerConfig<SeqTypes>>>()
2114            .into()
2115    }
2116}
2117
2118#[cfg(any(test, feature = "testing"))]
2119pub mod testing {
2120    use alloy::primitives::Bytes;
2121    use hotshot_contract_adapter::{
2122        sol_types::{EdOnBN254PointSol, G1PointSol, G2PointSol},
2123        stake_table::{StateSignatureSol, sign_address_bls, sign_address_schnorr},
2124    };
2125    use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
2126    use rand::{Rng as _, RngCore as _};
2127
2128    use super::*;
2129
2130    // TODO: current tests are just sanity checks, we need more.
2131
2132    #[derive(Debug, Clone)]
2133    pub struct TestValidator {
2134        pub account: Address,
2135        pub bls_vk: G2PointSol,
2136        pub schnorr_vk: EdOnBN254PointSol,
2137        pub commission: u16,
2138        pub bls_sig: G1PointSol,
2139        pub schnorr_sig: Bytes,
2140        pub x25519_key: [u8; 32],
2141        pub p2p_addr: String,
2142    }
2143
2144    impl TestValidator {
2145        pub fn random() -> Self {
2146            let account = Address::random();
2147            let commission = rand::thread_rng().gen_range(0..10000);
2148            Self::random_update_keys(account, commission)
2149        }
2150
2151        pub fn randomize_keys(&self) -> Self {
2152            Self::random_update_keys(self.account, self.commission)
2153        }
2154
2155        pub fn random_update_keys(account: Address, commission: u16) -> Self {
2156            let mut rng = &mut rand::thread_rng();
2157            let mut seed = [0u8; 32];
2158            rng.fill_bytes(&mut seed);
2159            let bls_key_pair = BLSKeyPair::generate(&mut rng);
2160            let bls_sig = sign_address_bls(&bls_key_pair, account);
2161            let schnorr_key_pair = StateKeyPair::generate_from_seed_indexed(seed, 0);
2162            let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, account);
2163            let mut x25519_key = [0u8; 32];
2164            rng.fill_bytes(&mut x25519_key);
2165            Self {
2166                account,
2167                bls_vk: bls_key_pair.ver_key().to_affine().into(),
2168                schnorr_vk: schnorr_key_pair.ver_key().to_affine().into(),
2169                commission,
2170                bls_sig: bls_sig.into(),
2171                schnorr_sig: StateSignatureSol::from(schnorr_sig).into(),
2172                x25519_key,
2173                p2p_addr: "127.0.0.1:9000".to_string(),
2174            }
2175        }
2176
2177        pub fn with_x25519_key(mut self, x25519_key: [u8; 32]) -> Self {
2178            self.x25519_key = x25519_key;
2179            self
2180        }
2181
2182        pub fn with_p2p_addr(mut self, p2p_addr: impl Into<String>) -> Self {
2183            self.p2p_addr = p2p_addr.into();
2184            self
2185        }
2186
2187        pub fn x25519_update(&self, x25519_key: [u8; 32]) -> StakeTableEvent {
2188            StakeTableEvent::X25519KeyUpdate(X25519KeyUpdated {
2189                validator: self.account,
2190                x25519Key: alloy::primitives::FixedBytes(x25519_key),
2191            })
2192        }
2193
2194        pub fn p2p_update(&self, p2p_addr: impl Into<String>) -> StakeTableEvent {
2195            StakeTableEvent::P2pAddrUpdate(P2pAddrUpdated {
2196                validator: self.account,
2197                p2pAddr: p2p_addr.into(),
2198            })
2199        }
2200    }
2201
2202    impl From<&TestValidator> for ValidatorRegistered {
2203        fn from(value: &TestValidator) -> Self {
2204            Self {
2205                account: value.account,
2206                blsVk: value.bls_vk,
2207                schnorrVk: value.schnorr_vk,
2208                commission: value.commission,
2209            }
2210        }
2211    }
2212
2213    impl From<&TestValidator> for ValidatorRegisteredV2 {
2214        fn from(value: &TestValidator) -> Self {
2215            Self {
2216                account: value.account,
2217                blsVK: value.bls_vk,
2218                schnorrVK: value.schnorr_vk,
2219                commission: value.commission,
2220                blsSig: value.bls_sig.into(),
2221                schnorrSig: value.schnorr_sig.clone(),
2222                metadataUri: "dummy-meta".to_string(),
2223            }
2224        }
2225    }
2226
2227    impl From<&TestValidator> for ValidatorRegisteredV3 {
2228        fn from(value: &TestValidator) -> Self {
2229            Self {
2230                account: value.account,
2231                blsVK: value.bls_vk,
2232                schnorrVK: value.schnorr_vk,
2233                commission: value.commission,
2234                blsSig: value.bls_sig.into(),
2235                schnorrSig: value.schnorr_sig.clone(),
2236                metadataUri: "dummy-meta".to_string(),
2237                x25519Key: alloy::primitives::FixedBytes(value.x25519_key),
2238                p2pAddr: value.p2p_addr.clone(),
2239            }
2240        }
2241    }
2242
2243    impl From<&TestValidator> for ConsensusKeysUpdated {
2244        fn from(value: &TestValidator) -> Self {
2245            Self {
2246                account: value.account,
2247                blsVK: value.bls_vk,
2248                schnorrVK: value.schnorr_vk,
2249            }
2250        }
2251    }
2252
2253    impl From<&TestValidator> for ConsensusKeysUpdatedV2 {
2254        fn from(value: &TestValidator) -> Self {
2255            Self {
2256                account: value.account,
2257                blsVK: value.bls_vk,
2258                schnorrVK: value.schnorr_vk,
2259                blsSig: value.bls_sig.into(),
2260                schnorrSig: value.schnorr_sig.clone(),
2261            }
2262        }
2263    }
2264
2265    impl From<&TestValidator> for ValidatorExit {
2266        fn from(value: &TestValidator) -> Self {
2267            Self {
2268                validator: value.account,
2269            }
2270        }
2271    }
2272
2273    impl RegisteredValidator<BLSPubKey> {
2274        pub fn mock() -> RegisteredValidator<BLSPubKey> {
2275            let val = TestValidator::random();
2276            let rng = &mut rand::thread_rng();
2277            let mut seed = [1u8; 32];
2278            rng.fill_bytes(&mut seed);
2279            let mut validator_stake = alloy::primitives::U256::from(0);
2280            let mut delegators = HashMap::new();
2281            for _i in 0..=5000 {
2282                let stake: u64 = rng.gen_range(0..10000);
2283                delegators.insert(Address::random(), alloy::primitives::U256::from(stake));
2284                validator_stake += alloy::primitives::U256::from(stake);
2285            }
2286
2287            let stake_table_key = BLSPubKey::try_from(val.bls_vk).expect("valid test BLS key");
2288            let state_ver_key =
2289                SchnorrPubKey::try_from(val.schnorr_vk).expect("valid test Schnorr key");
2290
2291            RegisteredValidator {
2292                account: val.account,
2293                stake_table_key: Some(stake_table_key),
2294                state_ver_key: Some(state_ver_key),
2295                stake: validator_stake,
2296                commission: val.commission,
2297                delegators,
2298                authenticated: true,
2299                x25519_key: None,
2300                p2p_addr: None,
2301            }
2302        }
2303    }
2304
2305    impl AuthenticatedValidator<BLSPubKey> {
2306        pub fn mock() -> AuthenticatedValidator<BLSPubKey> {
2307            RegisteredValidator::mock()
2308                .try_into()
2309                .expect("mock validator is always authenticated")
2310        }
2311
2312        pub fn mock_with_commission(commission: u16) -> AuthenticatedValidator<BLSPubKey> {
2313            let mut inner = RegisteredValidator::mock();
2314            inner.commission = commission;
2315            inner
2316                .try_into()
2317                .expect("mock validator is always authenticated")
2318        }
2319    }
2320}
2321
2322#[cfg(test)]
2323mod tests {
2324    use alloy::{
2325        primitives::{Address, Bytes},
2326        rpc::types::Log,
2327    };
2328    use hotshot_contract_adapter::{
2329        sol_types::{G1PointSol, G2PointSol},
2330        stake_table::{StakeTableContractVersion, sign_address_bls},
2331    };
2332    use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
2333    use pretty_assertions::assert_matches;
2334    use rstest::rstest;
2335    use versions::{
2336        DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION,
2337    };
2338
2339    use super::*;
2340    use crate::{L1ClientOptions, v0::impls::testing::*};
2341
2342    fn zero_g2() -> G2PointSol {
2343        G2PointSol {
2344            x0: U256::ZERO,
2345            x1: U256::ZERO,
2346            y0: U256::ZERO,
2347            y1: U256::ZERO,
2348        }
2349    }
2350
2351    fn zero_g1() -> G1PointSol {
2352        G1PointSol {
2353            x: U256::ZERO,
2354            y: U256::ZERO,
2355        }
2356    }
2357
2358    /// `eth_getLogs` counts both endpoints, so a chunk of `chunk_size` must span `chunk_size`
2359    /// blocks. The backward scan used to ask for `chunk_size + 1`, which providers rejected with
2360    /// "Block range 10001 exceeds the maximum of 10000 blocks per logs request", so the fallback
2361    /// scan for the token `Initialized` event could never succeed at the default range.
2362    #[test_log::test]
2363    fn test_block_range_chunks_never_exceed_chunk_size() {
2364        const MAX_RANGE: u64 = 10_000;
2365        const INIT_BLOCK: u64 = 1_000_000;
2366        const LOWEST_BLOCK: u64 = INIT_BLOCK - 200_000 + 1;
2367
2368        let width = |(from, to): &(u64, u64)| to - from + 1;
2369
2370        let descending: Vec<_> =
2371            Fetcher::block_range_chunks_rev(LOWEST_BLOCK, INIT_BLOCK, MAX_RANGE).collect();
2372
2373        assert_eq!(descending[0], (INIT_BLOCK - MAX_RANGE + 1, INIT_BLOCK));
2374        assert_eq!(descending.last().unwrap().0, LOWEST_BLOCK);
2375        for chunk in &descending {
2376            assert_eq!(width(chunk), MAX_RANGE, "chunk {chunk:?} is missized");
2377        }
2378        for pair in descending.windows(2) {
2379            assert_eq!(pair[0].0, pair[1].1 + 1, "gap or overlap at {pair:?}");
2380        }
2381
2382        let ascending: Vec<_> =
2383            Fetcher::block_range_chunks(LOWEST_BLOCK, INIT_BLOCK, MAX_RANGE).collect();
2384        for chunk in &ascending {
2385            assert!(
2386                width(chunk) <= MAX_RANGE,
2387                "chunk {chunk:?} exceeds {MAX_RANGE}"
2388            );
2389        }
2390
2391        // A range shorter than one chunk, and one bottoming out at genesis, must both terminate.
2392        assert_eq!(
2393            Fetcher::block_range_chunks_rev(0, 5, MAX_RANGE).collect::<Vec<_>>(),
2394            vec![(0, 5)]
2395        );
2396        assert_eq!(
2397            Fetcher::block_range_chunks_rev(0, MAX_RANGE, MAX_RANGE).collect::<Vec<_>>(),
2398            vec![(1, MAX_RANGE), (0, 0)]
2399        );
2400    }
2401
2402    #[test_log::test]
2403    fn test_from_l1_events() -> anyhow::Result<()> {
2404        // Build a stake table with one DA node and one consensus node.
2405        let val_1 = TestValidator::random();
2406        let val_1_new_keys = val_1.randomize_keys();
2407        let val_2 = TestValidator::random();
2408        let val_2_new_keys = val_2.randomize_keys();
2409        let delegator = Address::random();
2410        let mut events: Vec<StakeTableEvent> = [
2411            ValidatorRegistered::from(&val_1).into(),
2412            ValidatorRegisteredV2::from(&val_2).into(),
2413            Delegated {
2414                delegator,
2415                validator: val_1.account,
2416                amount: U256::from(10),
2417            }
2418            .into(),
2419            ConsensusKeysUpdated::from(&val_1_new_keys).into(),
2420            ConsensusKeysUpdatedV2::from(&val_2_new_keys).into(),
2421            Undelegated {
2422                delegator,
2423                validator: val_1.account,
2424                amount: U256::from(7),
2425            }
2426            .into(),
2427            // delegate to the same validator again
2428            Delegated {
2429                delegator,
2430                validator: val_1.account,
2431                amount: U256::from(5),
2432            }
2433            .into(),
2434            // delegate to the second validator
2435            Delegated {
2436                delegator: Address::random(),
2437                validator: val_2.account,
2438                amount: U256::from(3),
2439            }
2440            .into(),
2441        ]
2442        .to_vec();
2443
2444        let validators_set = ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION)?;
2445        let st = validators_set.active_validators;
2446        let st_val_1 = st.get(&val_1.account).unwrap();
2447        // final staked amount should be 10 (delegated) - 7 (undelegated) + 5 (Delegated)
2448        assert_eq!(st_val_1.stake, U256::from(8));
2449        assert_eq!(st_val_1.commission, val_1.commission);
2450        assert_eq!(st_val_1.delegators.len(), 1);
2451        // final delegated amount should be 10 (delegated) - 7 (undelegated) + 5 (Delegated)
2452        assert_eq!(*st_val_1.delegators.get(&delegator).unwrap(), U256::from(8));
2453
2454        let st_val_2 = st.get(&val_2.account).unwrap();
2455        assert_eq!(st_val_2.stake, U256::from(3));
2456        assert_eq!(st_val_2.commission, val_2.commission);
2457        assert_eq!(st_val_2.delegators.len(), 1);
2458
2459        events.push(ValidatorExit::from(&val_1).into());
2460
2461        let validator_set = ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION)?;
2462        let st = validator_set.active_validators;
2463        // The first validator should have been removed
2464        assert_eq!(st.get(&val_1.account), None);
2465
2466        // The second validator should be unchanged
2467        let st_val_2 = st.get(&val_2.account).unwrap();
2468        assert_eq!(st_val_2.stake, U256::from(3));
2469        assert_eq!(st_val_2.commission, val_2.commission);
2470        assert_eq!(st_val_2.delegators.len(), 1);
2471
2472        // remove the 2nd validator
2473        events.push(ValidatorExit::from(&val_2).into());
2474
2475        // This should fail because the validator has exited and no longer exists in the stake table.
2476        assert!(ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION).is_err());
2477
2478        Ok(())
2479    }
2480
2481    #[test]
2482    fn test_from_l1_events_failures() -> anyhow::Result<()> {
2483        let val = TestValidator::random();
2484        let delegator = Address::random();
2485
2486        let register: StakeTableEvent = ValidatorRegistered::from(&val).into();
2487        let register_v2: StakeTableEvent = ValidatorRegisteredV2::from(&val).into();
2488        let delegate: StakeTableEvent = Delegated {
2489            delegator,
2490            validator: val.account,
2491            amount: U256::from(10),
2492        }
2493        .into();
2494        let key_update: StakeTableEvent = ConsensusKeysUpdated::from(&val).into();
2495        let key_update_v2: StakeTableEvent = ConsensusKeysUpdatedV2::from(&val).into();
2496        let undelegate: StakeTableEvent = Undelegated {
2497            delegator,
2498            validator: val.account,
2499            amount: U256::from(7),
2500        }
2501        .into();
2502
2503        let exit: StakeTableEvent = ValidatorExit::from(&val).into();
2504
2505        let cases = [
2506            vec![exit],
2507            vec![undelegate.clone()],
2508            vec![delegate.clone()],
2509            vec![key_update],
2510            vec![key_update_v2],
2511            vec![register.clone(), register.clone()],
2512            vec![register_v2.clone(), register_v2.clone()],
2513            vec![register.clone(), register_v2.clone()],
2514            vec![register_v2.clone(), register.clone()],
2515            vec![
2516                register,
2517                delegate.clone(),
2518                undelegate.clone(),
2519                undelegate.clone(),
2520            ],
2521            vec![register_v2, delegate, undelegate.clone(), undelegate],
2522        ];
2523
2524        for events in cases.iter() {
2525            // NOTE: not selecting the active validator set because we care about wrong sequences of
2526            // events being detected. If we compute the active set we will also get an error if the
2527            // set is empty but that's not what we want to test here.
2528            let res = validators_from_l1_events(events.iter().cloned());
2529            assert!(
2530                res.is_err(),
2531                "events {res:?}, not a valid sequence of events"
2532            );
2533        }
2534        Ok(())
2535    }
2536
2537    #[test]
2538    fn test_validators_selection() {
2539        let mut candidates = IndexMap::new();
2540        let mut highest_stake = alloy::primitives::U256::ZERO;
2541
2542        for _i in 0..3000 {
2543            let candidate = RegisteredValidator::mock();
2544            candidates.insert(candidate.account, candidate.clone());
2545
2546            if candidate.stake > highest_stake {
2547                highest_stake = candidate.stake;
2548            }
2549        }
2550
2551        let minimum_stake = highest_stake / U256::from(VID_TARGET_TOTAL_STAKE);
2552
2553        let selected_validators = select_active_validator_set(&candidates, EPOCH_VERSION)
2554            .expect("Failed to select validators");
2555        assert!(
2556            selected_validators.len() <= MAX_VALIDATORS,
2557            "validators len is {}, expected at most {MAX_VALIDATORS}",
2558            selected_validators.len()
2559        );
2560
2561        let mut selected_validators_highest_stake = alloy::primitives::U256::ZERO;
2562        // Ensure every validator in the final selection is above or equal to minimum stake
2563        for (address, validator) in &selected_validators {
2564            assert!(
2565                validator.stake >= minimum_stake,
2566                "Validator {:?} has stake below minimum: {}",
2567                address,
2568                validator.stake
2569            );
2570
2571            if validator.stake > selected_validators_highest_stake {
2572                selected_validators_highest_stake = validator.stake;
2573            }
2574        }
2575    }
2576
2577    // For a bug where the GCL did not match the stake table contract implementation and allowed
2578    // duplicated BLS keys via the update keys events.
2579    #[rstest::rstest]
2580    fn test_regression_non_unique_bls_keys_not_discarded(
2581        #[values(
2582            StakeTableContractVersion::V1,
2583            StakeTableContractVersion::V2,
2584            StakeTableContractVersion::V3
2585        )]
2586        version: StakeTableContractVersion,
2587    ) {
2588        let val = TestValidator::random();
2589        let register: StakeTableEvent = match version {
2590            StakeTableContractVersion::V1 => ValidatorRegistered::from(&val).into(),
2591            StakeTableContractVersion::V2 => ValidatorRegisteredV2::from(&val).into(),
2592            StakeTableContractVersion::V3 => StakeTableEvent::RegisterV3((&val).into()),
2593        };
2594        let delegate: StakeTableEvent = Delegated {
2595            delegator: Address::random(),
2596            validator: val.account,
2597            amount: U256::from(10),
2598        }
2599        .into();
2600
2601        // first ensure that wan build a valid stake table
2602        assert!(
2603            ValidatorSet::from_l1_events(
2604                vec![register.clone(), delegate.clone()].into_iter(),
2605                EPOCH_VERSION,
2606            )
2607            .is_ok()
2608        );
2609
2610        // add the invalid key update (re-using the same consensus keys)
2611        let key_update = ConsensusKeysUpdated::from(&val).into();
2612        let err = ValidatorSet::from_l1_events(
2613            vec![register, delegate, key_update].into_iter(),
2614            EPOCH_VERSION,
2615        )
2616        .unwrap_err();
2617
2618        let bls = BLSPubKey::try_from(val.bls_vk).expect("valid test BLS key");
2619        assert!(matches!(err, StakeTableError::BlsKeyAlreadyUsed(addr) if addr == bls.to_string()));
2620    }
2621
2622    // Test that the GCL does not
2623    // allow re-registration for the same Ethereum account.
2624    #[test]
2625    fn test_regression_reregister_eth_account() {
2626        let val1 = TestValidator::random();
2627        let val2 = val1.randomize_keys();
2628        let account = val1.account;
2629
2630        let register1 = ValidatorRegisteredV2::from(&val1).into();
2631        let deregister1 = ValidatorExit::from(&val1).into();
2632        let register2 = ValidatorRegisteredV2::from(&val2).into();
2633        let events = [register1, deregister1, register2];
2634        let error = validators_from_l1_events(events.iter().cloned()).unwrap_err();
2635        assert_matches!(error, StakeTableError::ValidatorAlreadyExited(addr) if addr == account);
2636    }
2637
2638    #[test]
2639    fn test_display_log() {
2640        let serialized = r#"{"address":"0x0000000000000000000000000000000000000069",
2641            "topics":["0x0000000000000000000000000000000000000000000000000000000000000069"],
2642            "data":"0x69",
2643            "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2644            "blockNumber":"0x69","blockTimestamp":"0x69",
2645            "transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2646            "transactionIndex":"0x69","logIndex":"0x70","removed":false}"#;
2647        let log: Log = serde_json::from_str(serialized).unwrap();
2648        assert_eq!(
2649            log.display(),
2650            "Log(block=105,index=112,\
2651             transaction_hash=0x0000000000000000000000000000000000000000000000000000000000000069)"
2652        )
2653    }
2654
2655    #[rstest]
2656    #[case::v1(StakeTableContractVersion::V1)]
2657    #[case::v2(StakeTableContractVersion::V2)]
2658    #[case::v3(StakeTableContractVersion::V3)]
2659    fn test_register_validator(#[case] version: StakeTableContractVersion) {
2660        let mut state = StakeTableState::default();
2661        let validator = TestValidator::random();
2662
2663        let event = match version {
2664            StakeTableContractVersion::V1 => StakeTableEvent::Register((&validator).into()),
2665            StakeTableContractVersion::V2 => StakeTableEvent::RegisterV2((&validator).into()),
2666            StakeTableContractVersion::V3 => StakeTableEvent::RegisterV3((&validator).into()),
2667        };
2668
2669        state.apply_event(event).unwrap().unwrap();
2670
2671        let stored = state.validators.get(&validator.account).unwrap();
2672        assert_eq!(stored.account, validator.account);
2673    }
2674
2675    #[rstest]
2676    #[case::v1(StakeTableContractVersion::V1)]
2677    #[case::v2(StakeTableContractVersion::V2)]
2678    #[case::v3(StakeTableContractVersion::V3)]
2679    fn test_validator_already_registered(#[case] version: StakeTableContractVersion) {
2680        let mut stake_table_state = StakeTableState::default();
2681
2682        let test_validator = TestValidator::random();
2683
2684        // First registration attempt using the specified contract version
2685        match version {
2686            StakeTableContractVersion::V1 => {
2687                stake_table_state.apply_event(StakeTableEvent::Register((&test_validator).into()))
2688            },
2689            StakeTableContractVersion::V2 => {
2690                stake_table_state.apply_event(StakeTableEvent::RegisterV2((&test_validator).into()))
2691            },
2692            StakeTableContractVersion::V3 => {
2693                stake_table_state.apply_event(StakeTableEvent::RegisterV3((&test_validator).into()))
2694            },
2695        }
2696        .unwrap()
2697        .unwrap(); // Expect the first registration to succeed
2698
2699        // attempt using V1 registration (should fail with AlreadyRegistered)
2700        let v1_already_registered_result = stake_table_state
2701            .clone()
2702            .apply_event(StakeTableEvent::Register((&test_validator).into()));
2703
2704        pretty_assertions::assert_matches!(
2705           v1_already_registered_result,  Err(StakeTableError::AlreadyRegistered(account))
2706                if account == test_validator.account,
2707           "Expected AlreadyRegistered error. version ={version:?} result={v1_already_registered_result:?}",
2708        );
2709
2710        // attempt using V2 registration (should also fail with AlreadyRegistered)
2711        let v2_already_registered_result = stake_table_state
2712            .clone()
2713            .apply_event(StakeTableEvent::RegisterV2((&test_validator).into()));
2714
2715        pretty_assertions::assert_matches!(
2716            v2_already_registered_result,
2717            Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
2718            "Expected AlreadyRegistered error. version ={version:?} result={v2_already_registered_result:?}",
2719
2720        );
2721
2722        // attempt using V3 registration with a different x25519 key (should also fail
2723        // with AlreadyRegistered because the validator address is already registered)
2724        let v3_already_registered_result =
2725            stake_table_state
2726                .clone()
2727                .apply_event(StakeTableEvent::RegisterV3(
2728                    (&test_validator
2729                        .clone()
2730                        .with_x25519_key([43u8; 32])
2731                        .with_p2p_addr("127.0.0.1:9001"))
2732                        .into(),
2733                ));
2734
2735        pretty_assertions::assert_matches!(
2736            v3_already_registered_result,
2737            Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
2738            "Expected AlreadyRegistered error. version ={version:?} result={v3_already_registered_result:?}",
2739        );
2740    }
2741
2742    #[test]
2743    fn test_register_validator_v2_auth_fails_marks_as_unauthenticated() {
2744        let mut state = StakeTableState::default();
2745        let mut val = TestValidator::random();
2746        val.bls_sig = Default::default();
2747        let event = StakeTableEvent::RegisterV2((&val).into());
2748
2749        let result = state.apply_event(event);
2750        assert!(
2751            result.is_ok(),
2752            "Validator with invalid auth should still be accepted"
2753        );
2754
2755        let validator = state
2756            .validators()
2757            .get(&val.account)
2758            .expect("validator should exist");
2759        assert!(
2760            !validator.authenticated,
2761            "Validator should be marked as not authenticated"
2762        );
2763
2764        let event = StakeTableEvent::Delegate(Delegated {
2765            delegator: Address::random(),
2766            validator: val.account,
2767            amount: U256::from(100),
2768        });
2769        state.apply_event(event).unwrap().unwrap();
2770
2771        let active = select_active_validator_set(state.validators(), EPOCH_VERSION);
2772        match active {
2773            Err(_) => {}, // No validators is valid - means the unauthenticated one was filtered
2774            Ok(map) => {
2775                assert!(
2776                    map.get(&val.account).is_none(),
2777                    "Unauthenticated validator should not be in active set"
2778                );
2779            },
2780        }
2781    }
2782
2783    #[test]
2784    fn test_register_v2_bad_sig_preserves_parsed_bls_key() {
2785        let val = TestValidator::random();
2786        let other = TestValidator::random();
2787        let bad_val = TestValidator {
2788            bls_sig: other.bls_sig,
2789            ..val.clone()
2790        };
2791
2792        let mut state = StakeTableState::default();
2793        state
2794            .apply_event(StakeTableEvent::RegisterV2((&bad_val).into()))
2795            .expect("no fatal error")
2796            .expect("registered unauthenticated");
2797
2798        let registered = state.validators().get(&val.account).expect("present");
2799        assert!(!registered.authenticated);
2800        let expected_bls = BLSPubKey::try_from(val.bls_vk).expect("valid bls key");
2801        assert_eq!(registered.stake_table_key.as_ref(), Some(&expected_bls));
2802    }
2803
2804    #[test]
2805    fn test_register_v3_bad_sig_preserves_parsed_bls_key() {
2806        let val = TestValidator::random();
2807        let other = TestValidator::random();
2808        let bad_val = TestValidator {
2809            bls_sig: other.bls_sig,
2810            ..val.clone()
2811        };
2812
2813        let mut state = StakeTableState::default();
2814        state
2815            .apply_event(StakeTableEvent::RegisterV3((&bad_val).into()))
2816            .expect("no fatal error")
2817            .expect("registered unauthenticated");
2818
2819        let registered = state.validators().get(&val.account).expect("present");
2820        assert!(!registered.authenticated);
2821        let expected_bls = BLSPubKey::try_from(val.bls_vk).expect("valid bls key");
2822        assert_eq!(registered.stake_table_key.as_ref(), Some(&expected_bls));
2823    }
2824
2825    #[test]
2826    fn test_authenticated_validator_deserialize_rejects_unauthenticated() {
2827        let mut validator = RegisteredValidator::<BLSPubKey>::mock();
2828        validator.authenticated = false;
2829
2830        let json = serde_json::to_string(&validator).unwrap();
2831        let result: Result<AuthenticatedValidator<BLSPubKey>, _> = serde_json::from_str(&json);
2832
2833        assert!(result.is_err());
2834        let err = result.unwrap_err().to_string();
2835        assert!(
2836            err.contains("cannot deserialize unauthenticated validator"),
2837            "unexpected error: {err}"
2838        );
2839    }
2840
2841    #[rstest]
2842    #[case::v1(StakeTableContractVersion::V1)]
2843    #[case::v2(StakeTableContractVersion::V2)]
2844    #[case::v3(StakeTableContractVersion::V3)]
2845    fn test_deregister_validator(#[case] version: StakeTableContractVersion) {
2846        let mut state = StakeTableState::default();
2847        let val = TestValidator::random();
2848
2849        let reg = StakeTableEvent::Register((&val).into());
2850        state.apply_event(reg).unwrap().unwrap();
2851
2852        let dereg = match version {
2853            StakeTableContractVersion::V1 => StakeTableEvent::Deregister((&val).into()),
2854            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2855                StakeTableEvent::DeregisterV2(ValidatorExitV2 {
2856                    validator: val.account,
2857                    unlocksAt: U256::from(1000u64),
2858                })
2859            },
2860        };
2861        state.apply_event(dereg).unwrap().unwrap();
2862        assert!(!state.validators.contains_key(&val.account));
2863    }
2864
2865    #[rstest]
2866    #[case::v1(StakeTableContractVersion::V1)]
2867    #[case::v2(StakeTableContractVersion::V2)]
2868    #[case::v3(StakeTableContractVersion::V3)]
2869    fn test_delegate_and_undelegate(#[case] version: StakeTableContractVersion) {
2870        let mut state = StakeTableState::default();
2871        let val = TestValidator::random();
2872        state
2873            .apply_event(StakeTableEvent::Register((&val).into()))
2874            .unwrap()
2875            .unwrap();
2876
2877        let delegator = Address::random();
2878        let amount = U256::from(1000);
2879        let delegate_event = StakeTableEvent::Delegate(Delegated {
2880            delegator,
2881            validator: val.account,
2882            amount,
2883        });
2884        state.apply_event(delegate_event).unwrap().unwrap();
2885
2886        let validator = state.validators.get(&val.account).unwrap();
2887        assert_eq!(validator.delegators.get(&delegator).cloned(), Some(amount));
2888
2889        let undelegate_event = match version {
2890            StakeTableContractVersion::V1 => StakeTableEvent::Undelegate(Undelegated {
2891                delegator,
2892                validator: val.account,
2893                amount,
2894            }),
2895            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2896                StakeTableEvent::UndelegateV2(UndelegatedV2 {
2897                    delegator,
2898                    validator: val.account,
2899                    amount,
2900                    unlocksAt: U256::from(2000u64),
2901                    undelegationId: 1,
2902                })
2903            },
2904        };
2905        state.apply_event(undelegate_event).unwrap().unwrap();
2906        let validator = state.validators.get(&val.account).unwrap();
2907        assert!(!validator.delegators.contains_key(&delegator));
2908    }
2909
2910    #[rstest]
2911    #[case::v1(StakeTableContractVersion::V1)]
2912    #[case::v2(StakeTableContractVersion::V2)]
2913    #[case::v3(StakeTableContractVersion::V3)]
2914    fn test_key_update_event(#[case] version: StakeTableContractVersion) {
2915        let mut state = StakeTableState::default();
2916        let val = TestValidator::random();
2917
2918        // Always register first using V1 to simulate upgrade scenarios
2919        state
2920            .apply_event(StakeTableEvent::Register((&val).into()))
2921            .unwrap()
2922            .unwrap();
2923
2924        let new_keys = val.randomize_keys();
2925
2926        let event = match version {
2927            StakeTableContractVersion::V1 => StakeTableEvent::KeyUpdate((&new_keys).into()),
2928            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2929                StakeTableEvent::KeyUpdateV2((&new_keys).into())
2930            },
2931        };
2932
2933        state.apply_event(event).unwrap().unwrap();
2934
2935        let updated = state.validators.get(&val.account).unwrap();
2936        let expected_bls = BLSPubKey::try_from(new_keys.bls_vk).expect("valid test BLS key");
2937        assert_eq!(updated.stake_table_key.as_ref(), Some(&expected_bls));
2938        let expected_schnorr =
2939            SchnorrPubKey::try_from(new_keys.schnorr_vk).expect("valid test Schnorr key");
2940        assert_eq!(updated.state_ver_key.as_ref(), Some(&expected_schnorr));
2941    }
2942
2943    #[test]
2944    fn test_duplicate_bls_key() {
2945        let mut state = StakeTableState::default();
2946        let val = TestValidator::random();
2947        let event1 = StakeTableEvent::Register((&val).into());
2948        let mut val2 = TestValidator::random();
2949        val2.bls_vk = val.bls_vk;
2950        val2.account = Address::random();
2951
2952        let event2 = StakeTableEvent::Register((&val2).into());
2953        state.apply_event(event1).unwrap().unwrap();
2954        let result = state.apply_event(event2);
2955
2956        let expected_bls_key = BLSPubKey::try_from(val.bls_vk)
2957            .expect("valid test BLS key")
2958            .to_string();
2959
2960        assert_matches!(
2961            result,
2962            Err(StakeTableError::BlsKeyAlreadyUsed(key))
2963                if key == expected_bls_key,
2964            "Expected BlsKeyAlreadyUsed({expected_bls_key}), but got: {result:?}",
2965        );
2966    }
2967
2968    #[test]
2969    fn test_duplicate_schnorr_key() {
2970        let mut state = StakeTableState::default();
2971        let val = TestValidator::random();
2972        let event1 = StakeTableEvent::Register((&val).into());
2973        let mut val2 = TestValidator::random();
2974        val2.schnorr_vk = val.schnorr_vk;
2975        val2.account = Address::random();
2976        val2.bls_vk = val2.randomize_keys().bls_vk;
2977
2978        let event2 = StakeTableEvent::Register((&val2).into());
2979        state.apply_event(event1).unwrap().unwrap();
2980        let result = state.apply_event(event2);
2981
2982        let schnorr = SchnorrPubKey::try_from(val.schnorr_vk).expect("valid test Schnorr key");
2983        assert_matches!(
2984            result,
2985            Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(key)))
2986                if key == schnorr.to_string(),
2987            "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
2988
2989        );
2990    }
2991
2992    #[test]
2993    fn test_duplicate_schnorr_key_v2_during_update() {
2994        let mut state = StakeTableState::default();
2995
2996        let val1 = TestValidator::random();
2997
2998        let mut rng = &mut rand::thread_rng();
2999        let bls_key_pair = BLSKeyPair::generate(&mut rng);
3000
3001        let val2 = TestValidator {
3002            bls_vk: bls_key_pair.ver_key().to_affine().into(),
3003            bls_sig: sign_address_bls(&bls_key_pair, val1.account).into(),
3004            ..val1.clone()
3005        };
3006        let event1 = StakeTableEvent::RegisterV2((&val1).into());
3007        let event2 = StakeTableEvent::KeyUpdateV2((&val2).into());
3008
3009        state.apply_event(event1).unwrap().unwrap();
3010        let result = state.apply_event(event2);
3011
3012        let schnorr = SchnorrPubKey::try_from(val1.schnorr_vk).expect("valid test Schnorr key");
3013        assert_matches!(
3014            result,
3015            Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(key)))
3016                if key == schnorr.to_string(),
3017            "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
3018        );
3019    }
3020
3021    #[test]
3022    fn test_register_and_deregister_validator() {
3023        let mut state = StakeTableState::default();
3024        let validator = TestValidator::random();
3025        let event = StakeTableEvent::Register((&validator).into());
3026        state.apply_event(event).unwrap().unwrap();
3027
3028        let deregister_event = StakeTableEvent::Deregister((&validator).into());
3029        assert!(state.apply_event(deregister_event).unwrap().is_ok());
3030    }
3031
3032    #[test]
3033    fn test_commission_validation_exceeds_basis_points() {
3034        // Create a simple stake table with one validator
3035        let validator = TestValidator::random();
3036        let mut stake_table = StakeTableState::default();
3037
3038        // Register the validator first
3039        let registration_event = ValidatorRegistered::from(&validator).into();
3040        stake_table
3041            .apply_event(registration_event)
3042            .unwrap()
3043            .unwrap();
3044
3045        // Test that a commission exactly at the limit is allowed
3046        let valid_commission_event = CommissionUpdated {
3047            validator: validator.account,
3048            timestamp: Default::default(),
3049            oldCommission: 0,
3050            newCommission: COMMISSION_BASIS_POINTS, // Exactly at the limit
3051        }
3052        .into();
3053        stake_table
3054            .apply_event(valid_commission_event)
3055            .unwrap()
3056            .unwrap();
3057
3058        let invalid_commission = COMMISSION_BASIS_POINTS + 1;
3059        let invalid_commission_event = CommissionUpdated {
3060            validator: validator.account,
3061            timestamp: Default::default(),
3062            oldCommission: 0,
3063            newCommission: invalid_commission,
3064        }
3065        .into();
3066
3067        let err = stake_table
3068            .apply_event(invalid_commission_event)
3069            .unwrap_err();
3070
3071        assert_matches!(
3072            err,
3073            StakeTableError::InvalidCommission(addr, invalid_commission)
3074                if addr == addr && invalid_commission == invalid_commission);
3075    }
3076
3077    #[test]
3078    fn test_delegate_zero_amount_is_rejected() {
3079        let mut state = StakeTableState::default();
3080        let validator = TestValidator::random();
3081        let account = validator.account;
3082        state
3083            .apply_event(StakeTableEvent::Register((&validator).into()))
3084            .unwrap()
3085            .unwrap();
3086
3087        let delegator = Address::random();
3088        let amount = U256::ZERO;
3089        let event = StakeTableEvent::Delegate(Delegated {
3090            delegator,
3091            validator: account,
3092            amount,
3093        });
3094        let result = state.apply_event(event);
3095
3096        assert_matches!(
3097            result,
3098            Err(StakeTableError::ZeroDelegatorStake(addr))
3099                if addr == delegator,
3100            "delegator stake is zero"
3101
3102        );
3103    }
3104
3105    #[test]
3106    fn test_undelegate_more_than_stake_fails() {
3107        let mut state = StakeTableState::default();
3108        let validator = TestValidator::random();
3109        let account = validator.account;
3110        state
3111            .apply_event(StakeTableEvent::Register((&validator).into()))
3112            .unwrap()
3113            .unwrap();
3114
3115        let delegator = Address::random();
3116        let event = StakeTableEvent::Delegate(Delegated {
3117            delegator,
3118            validator: account,
3119            amount: U256::from(10u64),
3120        });
3121        state.apply_event(event).unwrap().unwrap();
3122
3123        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3124            delegator,
3125            validator: account,
3126            amount: U256::from(20u64),
3127        }));
3128        assert_matches!(
3129            result,
3130            Err(StakeTableError::InsufficientStake),
3131            "Expected InsufficientStake error, got: {result:?}",
3132        );
3133    }
3134
3135    #[test]
3136    fn test_apply_event_does_not_modify_state_on_error() {
3137        let mut state = StakeTableState::default();
3138        let validator = TestValidator::random();
3139        let delegator = Address::random();
3140
3141        state
3142            .apply_event(StakeTableEvent::Register((&validator).into()))
3143            .unwrap()
3144            .unwrap();
3145
3146        // AlreadyRegistered error
3147        let state_before = state.clone();
3148        let result = state.apply_event(StakeTableEvent::Register((&validator).into()));
3149        assert_matches!(result, Err(StakeTableError::AlreadyRegistered(_)));
3150        assert_eq!(
3151            state, state_before,
3152            "State should not change on AlreadyRegistered error"
3153        );
3154
3155        // Duplicate BLS key error
3156        let state_before = state.clone();
3157        let mut validator2 = TestValidator::random();
3158        validator2.bls_vk = validator.bls_vk; // Reuse BLS key
3159        let result = state.apply_event(StakeTableEvent::Register((&validator2).into()));
3160        assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
3161        assert_eq!(
3162            state, state_before,
3163            "State should not change on BlsKeyAlreadyUsed error"
3164        );
3165
3166        // ValidatorNotFound error on deregister
3167        let state_before = state.clone();
3168        let nonexistent_validator = TestValidator::random();
3169        let result =
3170            state.apply_event(StakeTableEvent::Deregister((&nonexistent_validator).into()));
3171        assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
3172        assert_eq!(
3173            state, state_before,
3174            "State should not change on ValidatorNotFound error"
3175        );
3176
3177        // ValidatorNotFound error on undelegate
3178        let state_before = state.clone();
3179        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3180            delegator: Address::random(),
3181            validator: Address::random(),
3182            amount: U256::from(100u64),
3183        }));
3184        assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
3185        assert_eq!(
3186            state, state_before,
3187            "State should not change on ValidatorNotFound error for Undelegate"
3188        );
3189
3190        state
3191            .apply_event(StakeTableEvent::Delegate(Delegated {
3192                delegator,
3193                validator: validator.account,
3194                amount: U256::from(100u64),
3195            }))
3196            .unwrap()
3197            .unwrap();
3198
3199        // DelegatorNotFound error on undelegate
3200        let state_before = state.clone();
3201        let non_existent_delegator = Address::random();
3202        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3203            delegator: non_existent_delegator,
3204            validator: validator.account,
3205            amount: U256::from(50u64),
3206        }));
3207        assert_matches!(result, Err(StakeTableError::DelegatorNotFound(_)));
3208        assert_eq!(
3209            state, state_before,
3210            "State should not change on DelegatorNotFound error"
3211        );
3212
3213        // InsufficientStake error on undelegate
3214        let state_before = state.clone();
3215        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3216            delegator,
3217            validator: validator.account,
3218            amount: U256::from(200u64),
3219        }));
3220        assert_matches!(result, Err(StakeTableError::InsufficientStake));
3221        assert_eq!(
3222            state, state_before,
3223            "State should not change on InsufficientStake error"
3224        );
3225
3226        // InsufficientStake when validator total stake would be less than amount
3227        let validator2 = TestValidator::random();
3228        let delegator2 = Address::random();
3229
3230        state
3231            .apply_event(StakeTableEvent::Register((&validator2).into()))
3232            .unwrap()
3233            .unwrap();
3234
3235        state
3236            .apply_event(StakeTableEvent::Delegate(Delegated {
3237                delegator: delegator2,
3238                validator: validator2.account,
3239                amount: U256::from(50u64),
3240            }))
3241            .unwrap()
3242            .unwrap();
3243        let state_before = state.clone();
3244        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3245            delegator: delegator2,
3246            validator: validator2.account,
3247            amount: U256::from(100u64),
3248        }));
3249        assert_matches!(result, Err(StakeTableError::InsufficientStake));
3250        assert_eq!(state, state_before,);
3251
3252        // ZeroDelegatorStake error
3253        let state_before = state.clone();
3254        let result = state.apply_event(StakeTableEvent::Delegate(Delegated {
3255            delegator: Address::random(),
3256            validator: validator.account,
3257            amount: U256::ZERO,
3258        }));
3259        assert_matches!(result, Err(StakeTableError::ZeroDelegatorStake(_)));
3260        assert_eq!(
3261            state, state_before,
3262            "State should not change on ZeroDelegatorStake error"
3263        );
3264    }
3265
3266    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3267    async fn test_decaf_stake_table() {
3268        // The following commented-out block demonstrates how the `decaf_stake_table_events.json`
3269        // and `decaf_stake_table.json` files were originally generated.
3270
3271        // It generates decaf stake table data by fetching events from the contract,
3272        // writes events and the constructed stake table to JSON files.
3273
3274        /*
3275        let l1 = L1Client::new(vec!["https://ethereum-sepolia.publicnode.com"
3276            .parse()
3277            .unwrap()])
3278        .unwrap();
3279        let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
3280
3281        let events = Fetcher::fetch_events_from_contract(
3282            l1,
3283            contract_address.parse().unwrap(),
3284            None,
3285            8582328,
3286        )
3287        .await?;
3288
3289        // Serialize and write sorted events
3290        let json_events = serde_json::to_string_pretty(&sorted_events)?;
3291        let mut events_file = File::create("decaf_stake_table_events.json").await?;
3292        events_file.write_all(json_events.as_bytes()).await?;
3293
3294        // Process into stake table
3295        let stake_table = validators_from_l1_events(sorted_events.into_iter().map(|(_, e)| e))?;
3296
3297        // Serialize and write stake table
3298        let json_stake_table = serde_json::to_string_pretty(&stake_table)?;
3299        let mut stake_file = File::create("decaf_stake_table.json").await?;
3300        stake_file.write_all(json_stake_table.as_bytes()).await?;
3301        */
3302
3303        let events_json =
3304            std::fs::read_to_string("../../../data/v3/decaf_stake_table_events.json").unwrap();
3305        let events: Vec<(EventKey, StakeTableEvent)> = serde_json::from_str(&events_json).unwrap();
3306
3307        // Reconstruct stake table from events
3308        let reconstructed_stake_table =
3309            ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), EPOCH_VERSION)
3310                .unwrap()
3311                .active_validators;
3312
3313        let stake_table_json =
3314            std::fs::read_to_string("../../../data/v3/decaf_stake_table.json").unwrap();
3315        let expected: AuthenticatedValidatorMap = serde_json::from_str(&stake_table_json).unwrap();
3316
3317        assert_eq!(
3318            reconstructed_stake_table, expected,
3319            "Stake table reconstructed from events does not match the expected stake table "
3320        );
3321    }
3322
3323    async fn snapshot_stake_table_commit(
3324        network: &str,
3325        rpc_url: &str,
3326        contract: &str,
3327        to_block: u64,
3328    ) {
3329        let l1 = L1ClientOptions {
3330            l1_events_max_retry_duration: Duration::from_secs(120),
3331            l1_events_max_block_range: 10_000,
3332            l1_retry_delay: Duration::from_secs(2),
3333            ..Default::default()
3334        }
3335        .connect(vec![rpc_url.parse().unwrap()])
3336        .expect("unable to construct l1 client");
3337
3338        let events =
3339            Fetcher::fetch_events_from_contract(l1, contract.parse().unwrap(), None, to_block)
3340                .await
3341                .unwrap();
3342
3343        let validator_set =
3344            ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), EPOCH_VERSION)
3345                .expect("failed to build validator set");
3346
3347        let active_as_registered = to_registered_validator_map(validator_set.active_validators());
3348        let active_state = StakeTableState::new(
3349            active_as_registered,
3350            Default::default(),
3351            Default::default(),
3352            Default::default(),
3353            Default::default(),
3354        );
3355        let active_commit = active_state.commit();
3356
3357        let summary = format!(
3358            "network: {network}\nto_block: {to_block}\nstake_table_contract: \
3359             {contract}\nstake_table_hash: {}\nactive_validators_commit: {}\nall_validators: \
3360             {}\nactive_validators: {}\n",
3361            validator_set
3362                .stake_table_hash()
3363                .expect("stake_table_hash should be set"),
3364            active_commit,
3365            validator_set.all_validators().len(),
3366            validator_set.active_validators().len(),
3367        );
3368
3369        let mut settings = insta::Settings::clone_current();
3370        let data_dir = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
3371            .join("../../../data/insta_snapshots");
3372        settings.set_snapshot_path(data_dir);
3373        settings.set_prepend_module_to_snapshot(false);
3374        settings.bind(|| {
3375            insta::assert_snapshot!(format!("{network}_stake_table_snapshot"), summary);
3376        });
3377    }
3378
3379    #[ignore = "talks to public Sepolia RPC"]
3380    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3381    async fn snapshot_decaf_stake_table_commit() {
3382        snapshot_stake_table_commit(
3383            "decaf",
3384            "https://ethereum-sepolia.publicnode.com",
3385            "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037",
3386            10_935_000,
3387        )
3388        .await;
3389    }
3390
3391    #[ignore = "talks to public Ethereum mainnet RPC"]
3392    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3393    async fn snapshot_mainnet_stake_table_commit() {
3394        snapshot_stake_table_commit(
3395            "mainnet",
3396            "https://ethereum-rpc.publicnode.com",
3397            "0xcef474d372b5b09defe2af187bf17338dc704451",
3398            25_188_000,
3399        )
3400        .await;
3401    }
3402
3403    /// Fetches the initial supply from L1 for `stake_table_contract` and asserts it matches the
3404    /// constant hardcoded in `known_initial_supply`.
3405    ///
3406    /// Guards against a wrong hardcoded constant: since `known_initial_supply` is consulted
3407    /// before any L1 call, a wrong value would make this node compute different block rewards
3408    /// from the rest of the network, a consensus fault.
3409    async fn assert_known_initial_supply_matches_l1(
3410        rpc_url: &str,
3411        chain_id: ChainId,
3412        stake_table_contract: Address,
3413    ) {
3414        let expected = known_initial_supply(chain_id, stake_table_contract)
3415            .expect("stake table contract must have a hardcoded initial supply");
3416
3417        let l1 = L1ClientOptions {
3418            l1_events_max_retry_duration: Duration::from_secs(120),
3419            l1_events_max_block_range: 10_000,
3420            l1_retry_delay: Duration::from_secs(2),
3421            ..Default::default()
3422        }
3423        .connect(vec![rpc_url.parse().unwrap()])
3424        .expect("unable to construct l1 client");
3425
3426        let fetcher = Fetcher::new(
3427            Arc::new(crate::mock::MockStateCatchup::default()),
3428            Arc::new(AsyncMutex::new(crate::v0_1::NoStorage)),
3429            l1,
3430            ChainConfig {
3431                chain_id,
3432                stake_table_contract: Some(stake_table_contract),
3433                ..Default::default()
3434            },
3435        );
3436
3437        let fetched = fetcher
3438            .fetch_and_update_initial_supply()
3439            .await
3440            .expect("failed to fetch initial supply from L1");
3441
3442        assert_eq!(
3443            fetched, expected,
3444            "hardcoded initial supply for {stake_table_contract} does not match L1"
3445        );
3446    }
3447
3448    /// Needs an archival RPC: publicnode's free tier rejects the full-range `eth_getLogs` with
3449    /// "Archive requests require a personal token". Tenderly's public gateway serves it.
3450    ///
3451    /// Tenderly rejects `finalized` as a `toBlock`, so the full-range query fails there and
3452    /// `scan_token_contract_initialized_event_log` finds the event instead, in one chunk.
3453    #[ignore = "talks to public Ethereum mainnet RPC"]
3454    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3455    async fn fetch_initial_supply_matches_mainnet_constant() {
3456        assert_known_initial_supply_matches_l1(
3457            "https://mainnet.gateway.tenderly.co",
3458            MAINNET_CHAIN_ID,
3459            MAINNET_STAKE_TABLE_CONTRACT,
3460        )
3461        .await;
3462    }
3463
3464    /// Needs an archival RPC with working topic-indexed log queries. Confirmed by hand that
3465    /// publicnode's free-tier Sepolia endpoint is not one: a topic-filtered `eth_getLogs`
3466    /// silently returns no results for this block range, even though an unfiltered query over
3467    /// the same range and address returns the `Initialized` log. That makes
3468    /// `EspToken::Initialized_filter` unusable there, and the scan fallback exhausts
3469    /// `MAX_BLOCKS_SCANNED` without ever seeing the event.
3470    #[ignore = "talks to public Sepolia RPC"]
3471    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3472    async fn fetch_initial_supply_matches_decaf_constant() {
3473        assert_known_initial_supply_matches_l1(
3474            "https://sepolia.gateway.tenderly.co",
3475            DECAF_CHAIN_ID,
3476            DECAF_STAKE_TABLE_CONTRACT,
3477        )
3478        .await;
3479    }
3480
3481    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3482    #[should_panic]
3483    async fn test_large_max_events_range_panic() {
3484        // decaf stake table contract address
3485        let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
3486
3487        let l1 = L1ClientOptions {
3488            l1_events_max_retry_duration: Duration::from_secs(30),
3489            // max block range for public node rpc is 50000 so this should result in a panic
3490            l1_events_max_block_range: 10_u64.pow(9),
3491            l1_retry_delay: Duration::from_secs(1),
3492            ..Default::default()
3493        }
3494        .connect(vec![
3495            "https://ethereum-sepolia.publicnode.com".parse().unwrap(),
3496        ])
3497        .expect("unable to construct l1 client");
3498
3499        let latest_block = l1.provider.get_block_number().await.unwrap();
3500        let _events = Fetcher::fetch_events_from_contract(
3501            l1,
3502            contract_address.parse().unwrap(),
3503            None,
3504            latest_block,
3505        )
3506        .await
3507        .unwrap();
3508    }
3509
3510    /// Pins the block reward each hardcoded initial supply produces.
3511    ///
3512    /// `known_initial_supply` has no local symptom when wrong: the node keeps computing
3513    /// rewards, just different ones from the rest of the network. The live-chain tests above
3514    /// need an archival RPC and do not run in CI, so a digit slip has to fail here.
3515    #[test]
3516    fn block_reward_for_known_initial_supplies() {
3517        let reward = |supply: U256| {
3518            ((supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
3519                .checked_div(U256::from(COMMISSION_BASIS_POINTS))
3520                .unwrap()
3521        };
3522
3523        assert_eq!(
3524            reward(U256::from(MAINNET_INITIAL_SUPPLY_WEI)),
3525            U256::from(6_830_289_193_302_891_933u128)
3526        );
3527        assert_eq!(
3528            reward(U256::from(DECAF_INITIAL_SUPPLY_WEI)),
3529            U256::from(19_025_875_190_258_751_902u128)
3530        );
3531    }
3532
3533    /// Pins the constants themselves, so a digit slip fails here as well as in the reward test.
3534    #[test]
3535    fn known_initial_supply_values() {
3536        assert_eq!(
3537            known_initial_supply(MAINNET_CHAIN_ID, MAINNET_STAKE_TABLE_CONTRACT),
3538            Some(U256::from_str("3590000000000000000000000000").unwrap())
3539        );
3540        assert_eq!(
3541            known_initial_supply(DECAF_CHAIN_ID, DECAF_STAKE_TABLE_CONTRACT),
3542            Some(U256::from_str("10000000000000000000000000000").unwrap())
3543        );
3544        assert_eq!(
3545            known_initial_supply(MAINNET_CHAIN_ID, Address::repeat_byte(9)),
3546            None
3547        );
3548        assert_eq!(
3549            known_initial_supply(DECAF_CHAIN_ID, MAINNET_STAKE_TABLE_CONTRACT),
3550            None
3551        );
3552        assert_eq!(
3553            known_initial_supply(ChainId(U256::from(9)), MAINNET_STAKE_TABLE_CONTRACT),
3554            None
3555        );
3556    }
3557
3558    /// The hardcoded chain ids and addresses are the ones the networks actually run, per their
3559    /// genesis files.
3560    #[test]
3561    fn known_chains_match_genesis() {
3562        let genesis = |network: &str| {
3563            let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
3564                .join("../../../data/genesis")
3565                .join(format!("{network}.toml"));
3566            let genesis: toml::Value =
3567                toml::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
3568            let chain_config = &genesis["chain_config"];
3569            (
3570                ChainId(U256::from(chain_config["chain_id"].as_integer().unwrap())),
3571                chain_config["stake_table_contract"]
3572                    .as_str()
3573                    .unwrap()
3574                    .parse::<Address>()
3575                    .unwrap(),
3576            )
3577        };
3578
3579        assert_eq!(
3580            genesis("mainnet"),
3581            (MAINNET_CHAIN_ID, MAINNET_STAKE_TABLE_CONTRACT)
3582        );
3583        assert_eq!(
3584            genesis("decaf"),
3585            (DECAF_CHAIN_ID, DECAF_STAKE_TABLE_CONTRACT)
3586        );
3587    }
3588
3589    /// `initial_supply_or_fetch` short-circuits on a known chain without touching the L1.
3590    /// `Fetcher::mock` points at an unroutable L1, so the fetch path would hang; the timeout
3591    /// turns that regression into a named failure instead of a CI job timeout.
3592    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3593    async fn initial_supply_or_fetch_skips_l1_for_known_chains() {
3594        for (chain_id, contract, expected) in [
3595            (
3596                MAINNET_CHAIN_ID,
3597                MAINNET_STAKE_TABLE_CONTRACT,
3598                MAINNET_INITIAL_SUPPLY_WEI,
3599            ),
3600            (
3601                DECAF_CHAIN_ID,
3602                DECAF_STAKE_TABLE_CONTRACT,
3603                DECAF_INITIAL_SUPPLY_WEI,
3604            ),
3605        ] {
3606            let fetcher = Fetcher::mock();
3607            {
3608                let mut chain_config = fetcher.chain_config.lock().await;
3609                chain_config.chain_id = chain_id;
3610                chain_config.stake_table_contract = Some(contract);
3611            }
3612
3613            let supply =
3614                tokio::time::timeout(Duration::from_secs(5), fetcher.initial_supply_or_fetch())
3615                    .await
3616                    .expect(
3617                        "initial_supply_or_fetch reached the L1 fetch path for a known contract",
3618                    )
3619                    .unwrap();
3620
3621            assert_eq!(supply, U256::from(expected));
3622        }
3623    }
3624
3625    #[test]
3626    fn sanity_check_p_and_rp() {
3627        let total_stake = BigDecimal::from_str("1000").unwrap();
3628        let total_supply = BigDecimal::from_str("10000").unwrap(); // p = 0.1
3629
3630        let (p, rp) =
3631            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3632
3633        assert!(p > BigDecimal::zero());
3634        assert!(p < BigDecimal::one());
3635        assert!(rp > BigDecimal::zero());
3636    }
3637
3638    #[test]
3639    fn test_p_out_of_range() {
3640        let total_stake = BigDecimal::from_str("1000").unwrap();
3641        let total_supply = BigDecimal::from_str("500").unwrap(); // p = 2.0
3642
3643        let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3644        assert!(result.is_err());
3645    }
3646
3647    #[test]
3648    fn test_zero_total_supply() {
3649        let total_stake = BigDecimal::from_str("1000").unwrap();
3650        let total_supply = BigDecimal::from(0);
3651
3652        let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3653        assert!(result.is_err());
3654    }
3655
3656    #[test]
3657    fn test_valid_p_and_rp() {
3658        let total_stake = BigDecimal::from_str("5000").unwrap();
3659        let total_supply = BigDecimal::from_str("10000").unwrap();
3660
3661        let (p, rp) =
3662            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3663
3664        assert_eq!(p, BigDecimal::from_str("0.5").unwrap());
3665        assert!(rp > BigDecimal::from_str("0.0").unwrap());
3666    }
3667
3668    #[test]
3669    fn test_very_small_p() {
3670        let total_stake = BigDecimal::from_str("1").unwrap(); // 1 wei
3671        let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap(); // 10B * 1e18
3672
3673        let (p, rp) =
3674            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3675
3676        assert!(p > BigDecimal::from_str("0").unwrap());
3677        assert!(p < BigDecimal::from_str("1e-18").unwrap()); // p should be extremely small
3678        assert!(rp > BigDecimal::zero());
3679    }
3680
3681    #[test]
3682    fn test_p_very_close_to_one() {
3683        let total_stake = BigDecimal::from_str("9999999999999999999999999999").unwrap();
3684        let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap();
3685
3686        let (p, rp) =
3687            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3688
3689        assert!(p < BigDecimal::one());
3690        assert!(p > BigDecimal::from_str("0.999999999999999999999999999").unwrap());
3691        assert!(rp > BigDecimal::zero());
3692    }
3693
3694    /// tests `calculate_proportion_staked_and_reward_rate` produces correct p and R(p) values
3695    /// across a range of stake proportions within a small numerical tolerance.
3696    ///
3697    #[test]
3698    fn test_reward_rate_rp() {
3699        let test_cases = [
3700            // p , R(p)
3701            ("0.0000", "0.2121"), // 0%
3702            ("0.0050", "0.2121"), // 0.5%
3703            ("0.0100", "0.2121"), // 1%
3704            ("0.0250", "0.1342"), // 2.5%
3705            ("0.0500", "0.0949"), // 5%
3706            ("0.1000", "0.0671"), // 10%
3707            ("0.2500", "0.0424"), // 25%
3708            ("0.5000", "0.0300"), // 50%
3709            ("0.7500", "0.0245"), // 75%
3710            ("1.0000", "0.0212"), // 100%
3711        ];
3712
3713        let tolerance = BigDecimal::from_str("0.0001").unwrap();
3714
3715        let total_supply = BigDecimal::from_u32(10_000).unwrap();
3716
3717        for (p, rp) in test_cases {
3718            let p = BigDecimal::from_str(p).unwrap();
3719            let expected_rp = BigDecimal::from_str(rp).unwrap();
3720
3721            let total_stake = &p * &total_supply;
3722
3723            let (computed_p, computed_rp) =
3724                calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3725
3726            assert!(
3727                (&computed_p - &p).abs() < tolerance,
3728                "p mismatch: got {computed_p}, expected {p}"
3729            );
3730
3731            assert!(
3732                (&computed_rp - &expected_rp).abs() < tolerance,
3733                "R(p) mismatch for p={p}: got {computed_rp}, expected {expected_rp}"
3734            );
3735        }
3736    }
3737
3738    #[tokio::test(flavor = "multi_thread")]
3739    async fn test_dynamic_block_reward_with_expected_values() {
3740        // 10B tokens = 10_000_000_000 * 10^18
3741        let total_supply = U256::from_str("10000000000000000000000000000").unwrap();
3742        let total_supply_bd = BigDecimal::from_str(&total_supply.to_string()).unwrap();
3743
3744        let test_cases = [
3745            // --- Block time: 1 ms ---
3746            ("0.0000", "0.2121", 1, "0"), // 0% staked → inflation = 0 → 0 tokens
3747            ("0.0050", "0.2121", 1, "3362823439878234"), // 0.105% inflation → ~0.00336 tokens
3748            ("0.0100", "0.2121", 1, "6725646879756468"), // 0.2121% inflation → ~0.00673 tokens
3749            ("0.0250", "0.1342", 1, "10638635210553018"), // 0.3355% inflation → ~0.01064 tokens
3750            ("0.0500", "0.0949", 1, "15046296296296296"), // 0.4745% inflation → ~0.01505 tokens
3751            ("0.1000", "0.0671", 1, "21277270421106037"), // 0.671% inflation → ~0.02128 tokens
3752            ("0.2500", "0.0424", 1, "33612379502790461"), // 1.06% inflation → ~0.03361 tokens
3753            ("0.5000", "0.0300", 1, "47564687975646879"), // 1.5% inflation → ~0.04756 tokens
3754            ("0.7500", "0.0245", 1, "58266742770167427"), // 1.8375% inflation → ~0.05827 tokens
3755            ("1.0000", "0.0212", 1, "67224759005580923"), // 2.12% inflation → ~0.06722 tokens
3756            // --- Block time: 2000 ms (2 seconds) ---
3757            ("0.0000", "0.2121", 2000, "0"), // 0% staked → inflation = 0 → 0 tokens
3758            ("0.0050", "0.2121", 2000, "672564687975646880"), // 0.105% inflation → ~0.67256 tokens
3759            ("0.0100", "0.2121", 2000, "1345129375951293760"), // 0.2121% inflation → ~1.34513 tokens
3760            ("0.0250", "0.1342", 2000, "2127727042110603754"), // 0.3355% inflation → ~2.12773 tokens
3761            ("0.0500", "0.0949", 2000, "3009259259259259259"), // 0.4745% inflation → ~3.00926 tokens
3762            ("0.1000", "0.0671", 2000, "4255454084221207509"), // 0.671% inflation → ~4.25545 tokens
3763            ("0.2500", "0.0424", 2000, "6722475900558092339"), // 1.06% inflation → ~6.72248 tokens
3764            ("0.5000", "0.0300", 2000, "9512937595129375951"), // 1.5% inflation → ~9.51294 tokens
3765            ("0.7500", "0.0245", 2000, "11653348554033485540"), // 1.8375% inflation → ~11.65335 tokens
3766            ("1.0000", "0.0212", 2000, "13444951801116184678"), // 2.12% inflation → ~13.44495 tokens
3767            // --- Block time: 10000 ms (10 seconds) ---
3768            ("0.0000", "0.2121", 10000, "0"), // 0% staked → inflation = 0 → 0 tokens
3769            ("0.0050", "0.2121", 10000, "3362823439878234400"), // 0.105% inflation → ~3.36 tokens
3770            ("0.0100", "0.2121", 10000, "6725646879756468800"), // 0.2121% inflation → ~6.73 tokens
3771            ("0.0250", "0.1342", 10000, "10638635210553018770"), // 0.3355% inflation → ~10.64 tokens
3772            ("0.0500", "0.0949", 10000, "15046296296296296295"), // 0.4745% inflation → ~15.05 tokens
3773            ("0.1000", "0.0671", 10000, "21277270421106037545"), // 0.671% inflation → ~21.28 tokens
3774            ("0.2500", "0.0424", 10000, "33612379502790461695"), // 1.06% inflation → ~33.61 tokens
3775            ("0.5000", "0.0300", 10000, "47564687975646879755"), // 1.5% inflation → ~47.56 tokens
3776            ("0.7500", "0.0245", 10000, "58266742770167427700"), // 1.8375% inflation → ~58.27 tokens
3777            ("1.0000", "0.0212", 10000, "67224759005580923390"), // 2.12% inflation → ~67.22 tokens
3778        ];
3779
3780        let tolerance = U256::from(100_000_000_000_000_000u128); // 0.1 token
3781
3782        for (p, rp, avg_block_time_ms, expected_reward) in test_cases {
3783            let p = BigDecimal::from_str(p).unwrap();
3784            let total_stake_bd = (&p * &total_supply_bd).round(0);
3785            println!("total_stake_bd={total_stake_bd}");
3786
3787            let total_stake = U256::from_str(&total_stake_bd.to_plain_string()).unwrap();
3788            let expected_reward = U256::from_str(expected_reward).unwrap();
3789
3790            let epoch = EpochNumber::new(0);
3791            let actual_reward =
3792                compute_block_reward(epoch, total_supply, total_stake, avg_block_time_ms)
3793                    .unwrap()
3794                    .0;
3795
3796            let diff = if actual_reward > expected_reward {
3797                actual_reward - expected_reward
3798            } else {
3799                expected_reward - actual_reward
3800            };
3801
3802            assert!(
3803                diff <= tolerance,
3804                "Reward mismatch for p = {p}, R(p) = {rp}, block_time = {avg_block_time_ms}: \
3805                 expected = {expected_reward}, actual = {actual_reward}, diff = {diff}"
3806            );
3807        }
3808    }
3809
3810    // Uses V2 events where available. Delegate and CommissionUpdate don't have V2 versions.
3811    #[derive(Debug, Clone, Copy)]
3812    enum EventType {
3813        Delegate,
3814        Undelegate,
3815        KeyUpdate,
3816        CommissionUpdate,
3817        Exit,
3818        X25519KeyUpdate,
3819        P2pAddrUpdate,
3820    }
3821
3822    // Regression for PR #3903: validators with invalid signatures used to be dropped during
3823    // registration, so any later event targeting them failed with ValidatorNotFound and broke
3824    // stake table reconstruction. Now they're stored with authenticated=false and subsequent
3825    // events must succeed against them (though they stay out of the active consensus set).
3826    #[rstest]
3827    #[case::delegate(EventType::Delegate)]
3828    #[case::undelegate(EventType::Undelegate)]
3829    #[case::key_update(EventType::KeyUpdate)]
3830    #[case::commission_update(EventType::CommissionUpdate)]
3831    #[case::exit(EventType::Exit)]
3832    #[case::x25519_key_update(EventType::X25519KeyUpdate)]
3833    #[case::p2p_addr_update(EventType::P2pAddrUpdate)]
3834    fn test_events_targeting_unauthenticated_validator(
3835        #[case] event_type: EventType,
3836    ) -> anyhow::Result<()> {
3837        let mut state = StakeTableState::default();
3838        let mut val = TestValidator::random();
3839        val.bls_sig = Default::default();
3840        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3841
3842        let validator = state.validators().get(&val.account).context("validator")?;
3843        assert!(!validator.authenticated);
3844
3845        let delegator = Address::random();
3846        let initial_amount = U256::from(1000);
3847        state.apply_event(StakeTableEvent::Delegate(Delegated {
3848            delegator,
3849            validator: val.account,
3850            amount: initial_amount,
3851        }))??;
3852
3853        match event_type {
3854            EventType::Delegate => {
3855                let new_delegator = Address::random();
3856                let amount = U256::from(500);
3857                state.apply_event(StakeTableEvent::Delegate(Delegated {
3858                    delegator: new_delegator,
3859                    validator: val.account,
3860                    amount,
3861                }))??;
3862
3863                let validator = state.validators().get(&val.account).context("validator")?;
3864                assert_eq!(validator.stake, initial_amount + amount);
3865                assert_eq!(
3866                    validator.delegators.get(&new_delegator).cloned(),
3867                    Some(amount)
3868                );
3869            },
3870            EventType::Undelegate => {
3871                let undelegate_amount = U256::from(300);
3872                state.apply_event(StakeTableEvent::UndelegateV2(UndelegatedV2 {
3873                    delegator,
3874                    validator: val.account,
3875                    undelegationId: 1,
3876                    amount: undelegate_amount,
3877                    unlocksAt: U256::from(1000u64),
3878                }))??;
3879
3880                let validator = state.validators().get(&val.account).context("validator")?;
3881                assert_eq!(validator.stake, initial_amount - undelegate_amount);
3882                assert_eq!(
3883                    validator.delegators.get(&delegator).cloned(),
3884                    Some(initial_amount - undelegate_amount)
3885                );
3886            },
3887            EventType::KeyUpdate => {
3888                let new_keys = val.randomize_keys();
3889                state.apply_event(StakeTableEvent::KeyUpdateV2((&new_keys).into()))??;
3890
3891                let validator = state.validators().get(&val.account).context("validator")?;
3892                let expected_bls =
3893                    BLSPubKey::try_from(new_keys.bls_vk).expect("valid test BLS key");
3894                let expected_schnorr =
3895                    SchnorrPubKey::try_from(new_keys.schnorr_vk).expect("valid test Schnorr key");
3896                assert_eq!(validator.stake_table_key.as_ref(), Some(&expected_bls));
3897                assert_eq!(validator.state_ver_key.as_ref(), Some(&expected_schnorr));
3898                // KeyUpdateV2 authenticates signatures, so on success the validator
3899                // is promoted to `authenticated=true`, even if a prior registration
3900                // left it unauthenticated.
3901                assert!(validator.authenticated);
3902            },
3903            EventType::CommissionUpdate => {
3904                let new_commission: u16 = 5000;
3905                state.apply_event(StakeTableEvent::CommissionUpdate(CommissionUpdated {
3906                    validator: val.account,
3907                    timestamp: Default::default(),
3908                    oldCommission: val.commission,
3909                    newCommission: new_commission,
3910                }))??;
3911
3912                let validator = state.validators().get(&val.account).context("validator")?;
3913                assert_eq!(validator.commission, new_commission);
3914            },
3915            EventType::Exit => {
3916                state.apply_event(StakeTableEvent::DeregisterV2(ValidatorExitV2 {
3917                    validator: val.account,
3918                    unlocksAt: U256::from(1000u64),
3919                }))??;
3920
3921                assert!(!state.validators().contains_key(&val.account));
3922                return Ok(());
3923            },
3924            EventType::X25519KeyUpdate => {
3925                let new_x25519_key = [99u8; 32];
3926                state.apply_event(val.x25519_update(new_x25519_key))??;
3927
3928                let validator = state.validators().get(&val.account).context("validator")?;
3929                let expected = x25519::PublicKey::try_from(new_x25519_key.as_slice())
3930                    .expect("valid x25519 key");
3931                assert_eq!(validator.x25519_key, Some(expected));
3932                assert!(!validator.authenticated);
3933            },
3934            EventType::P2pAddrUpdate => {
3935                state.apply_event(val.p2p_update("10.0.0.1:9000"))??;
3936
3937                let validator = state.validators().get(&val.account).context("validator")?;
3938                let expected: NetAddr = "10.0.0.1:9000".parse().expect("valid p2p addr");
3939                assert_eq!(validator.p2p_addr, Some(expected));
3940                assert!(!validator.authenticated);
3941            },
3942        }
3943
3944        // For most event types the validator is still unauthenticated and is
3945        // filtered out of the active set. The KeyUpdate case is the exception:
3946        // KeyUpdateV2 verifies signatures and promotes the validator to
3947        // `authenticated=true`, so it becomes eligible for selection.
3948        let active = select_active_validator_set(state.validators(), EPOCH_VERSION);
3949        match event_type {
3950            EventType::KeyUpdate => match active {
3951                Ok(map) => assert!(map.contains_key(&val.account)),
3952                Err(e) => bail!("expected validator to be selected after KeyUpdate, got {e}"),
3953            },
3954            _ => match active {
3955                Err(StakeTableError::NoValidValidators) => {},
3956                Err(e) => bail!("Unexpected error: {e}"),
3957                Ok(map) => assert!(!map.contains_key(&val.account)),
3958            },
3959        }
3960        Ok(())
3961    }
3962
3963    #[test]
3964    fn test_register_v3_sets_x25519_and_p2p() -> anyhow::Result<()> {
3965        let val = TestValidator::random();
3966
3967        let mut state = StakeTableState::default();
3968        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3969
3970        let registered = state.validators().get(&val.account).unwrap();
3971        assert!(registered.authenticated);
3972
3973        let expected_x25519 =
3974            x25519::PublicKey::try_from(val.x25519_key.as_slice()).expect("valid x25519 key");
3975        assert_eq!(registered.x25519_key, Some(expected_x25519));
3976
3977        let expected_p2p: NetAddr = val.p2p_addr.parse().expect("valid p2p addr");
3978        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3979
3980        Ok(())
3981    }
3982
3983    #[test]
3984    fn test_register_v3_invalid_sig() -> anyhow::Result<()> {
3985        let val = TestValidator::random();
3986        let other = TestValidator::random();
3987
3988        // Build a V3 registration with val's keys but other's BLS sig (mismatched)
3989        let bad_val = TestValidator {
3990            bls_sig: other.bls_sig,
3991            ..val.clone()
3992        };
3993        let event = StakeTableEvent::RegisterV3((&bad_val).into());
3994
3995        let mut state = StakeTableState::default();
3996        state.apply_event(event)??;
3997
3998        let registered = state.validators().get(&val.account).unwrap();
3999        assert!(!registered.authenticated);
4000
4001        Ok(())
4002    }
4003
4004    #[test]
4005    fn test_register_v3_hostname_p2p() -> anyhow::Result<()> {
4006        let val = TestValidator::random();
4007
4008        let mut state = StakeTableState::default();
4009        // Empty p2p addr: NetAddr parses it as Name("", 0) which is still Some.
4010        // Non-IP strings become NetAddr::Name variant.
4011        let val = val.with_p2p_addr("my-host:9000");
4012        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4013
4014        let registered = state.validators().get(&val.account).unwrap();
4015        let expected_p2p: NetAddr = "my-host:9000".parse().unwrap();
4016        assert_eq!(registered.p2p_addr, Some(expected_p2p));
4017
4018        Ok(())
4019    }
4020
4021    #[test]
4022    fn test_register_v3_invalid_p2p_addr_degrades_to_none() -> anyhow::Result<()> {
4023        let val = TestValidator::random();
4024
4025        let mut state = StakeTableState::default();
4026        let val = val.with_p2p_addr("host:notaport");
4027        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4028
4029        let registered = state.validators().get(&val.account).unwrap();
4030        assert_eq!(registered.p2p_addr, None);
4031
4032        Ok(())
4033    }
4034
4035    #[test]
4036    fn test_x25519_key_update_sets_value() -> anyhow::Result<()> {
4037        let val = TestValidator::random();
4038
4039        let mut state = StakeTableState::default();
4040        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
4041
4042        assert_eq!(
4043            state.validators().get(&val.account).unwrap().x25519_key,
4044            None
4045        );
4046
4047        let x25519_key = [99u8; 32];
4048        state.apply_event(val.x25519_update(x25519_key))??;
4049
4050        let registered = state.validators().get(&val.account).unwrap();
4051        let expected_x25519 =
4052            x25519::PublicKey::try_from(x25519_key.as_slice()).expect("valid x25519 key");
4053        assert_eq!(registered.x25519_key, Some(expected_x25519));
4054
4055        Ok(())
4056    }
4057
4058    #[test]
4059    fn test_p2p_addr_update_sets_value() -> anyhow::Result<()> {
4060        let val = TestValidator::random();
4061
4062        let mut state = StakeTableState::default();
4063        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
4064
4065        assert_eq!(state.validators().get(&val.account).unwrap().p2p_addr, None);
4066
4067        let p2p_addr = "10.0.0.1:8080";
4068        state.apply_event(val.p2p_update(p2p_addr))??;
4069
4070        let registered = state.validators().get(&val.account).unwrap();
4071        let expected_p2p: NetAddr = p2p_addr.parse().expect("valid p2p addr");
4072        assert_eq!(registered.p2p_addr, Some(expected_p2p));
4073
4074        Ok(())
4075    }
4076
4077    #[test]
4078    fn test_p2p_addr_unparsable_sets_none() -> anyhow::Result<()> {
4079        let val = TestValidator::random();
4080
4081        let mut state = StakeTableState::default();
4082        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
4083
4084        // Set a valid address first.
4085        state.apply_event(val.p2p_update("10.0.0.1:8080"))??;
4086        assert!(
4087            state
4088                .validators()
4089                .get(&val.account)
4090                .unwrap()
4091                .p2p_addr
4092                .is_some()
4093        );
4094
4095        // An address with an invalid port that Rust's NetAddr parser rejects degrades to None.
4096        state.apply_event(val.p2p_update("host:notaport"))??;
4097        assert_eq!(state.validators().get(&val.account).unwrap().p2p_addr, None);
4098
4099        Ok(())
4100    }
4101
4102    #[test]
4103    fn test_x25519_key_update_unknown_validator() {
4104        let mut state = StakeTableState::default();
4105        let unknown = TestValidator::random();
4106
4107        let result = state.apply_event(unknown.x25519_update([1u8; 32]));
4108        assert_matches!(
4109            result,
4110            Err(StakeTableError::ValidatorNotFound(addr)) if addr == unknown.account
4111        );
4112    }
4113
4114    #[test]
4115    fn test_p2p_addr_update_unknown_validator() {
4116        let mut state = StakeTableState::default();
4117        let unknown = TestValidator::random();
4118
4119        let result = state.apply_event(unknown.p2p_update("127.0.0.1:9000"));
4120        assert_matches!(
4121            result,
4122            Err(StakeTableError::ValidatorNotFound(addr)) if addr == unknown.account
4123        );
4124    }
4125
4126    #[test]
4127    fn test_x25519_key_update_duplicate() -> anyhow::Result<()> {
4128        let shared_key = [55u8; 32];
4129        let val1 = TestValidator::random().with_x25519_key(shared_key);
4130        let val2 = TestValidator::random().with_x25519_key([2u8; 32]);
4131
4132        let mut state = StakeTableState::default();
4133        // Register both validators via V3
4134        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
4135        state.apply_event(StakeTableEvent::RegisterV3((&val2).into()))??;
4136
4137        // Try to update val2's x25519 key to the same as val1's
4138        let result = state.apply_event(val2.x25519_update(shared_key));
4139        assert_matches!(result, Err(StakeTableError::X25519KeyAlreadyUsed(_)));
4140
4141        Ok(())
4142    }
4143
4144    #[test]
4145    fn test_p2p_addr_update_hostname() -> anyhow::Result<()> {
4146        let val = TestValidator::random();
4147
4148        let mut state = StakeTableState::default();
4149        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
4150
4151        state.apply_event(val.p2p_update("my-node.example.com:9000"))??;
4152
4153        let registered = state.validators().get(&val.account).unwrap();
4154        let expected_p2p: NetAddr = "my-node.example.com:9000".parse().unwrap();
4155        assert_eq!(registered.p2p_addr, Some(expected_p2p));
4156
4157        Ok(())
4158    }
4159
4160    #[test]
4161    fn test_register_v3_duplicate_bls_key() -> anyhow::Result<()> {
4162        let val1 = TestValidator::random();
4163        let mut val2 = TestValidator::random();
4164        val2.bls_vk = val1.bls_vk;
4165
4166        let mut state = StakeTableState::default();
4167        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
4168        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
4169        assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
4170
4171        Ok(())
4172    }
4173
4174    #[test]
4175    fn test_register_v2_duplicate_schnorr_key_is_unauthenticated() -> anyhow::Result<()> {
4176        let val1 = TestValidator::random();
4177        let mut val2 = TestValidator::random();
4178        val2.schnorr_vk = val1.schnorr_vk;
4179
4180        let mut state = StakeTableState::default();
4181        state.apply_event(StakeTableEvent::RegisterV2((&val1).into()))??;
4182        state.apply_event(StakeTableEvent::RegisterV2((&val2).into()))??;
4183
4184        let registered = state.validators().get(&val2.account).expect("present");
4185        assert!(!registered.authenticated);
4186        assert!(registered.state_ver_key.is_none());
4187        assert_excluded_from_active_set(&state, val2.account);
4188
4189        Ok(())
4190    }
4191
4192    #[test]
4193    fn test_register_v3_duplicate_schnorr_key_is_unauthenticated() -> anyhow::Result<()> {
4194        let val1 = TestValidator::random();
4195        let mut val2 = TestValidator::random();
4196        val2.schnorr_vk = val1.schnorr_vk;
4197
4198        let mut state = StakeTableState::default();
4199        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
4200        state.apply_event(StakeTableEvent::RegisterV3((&val2).into()))??;
4201
4202        let registered = state.validators().get(&val2.account).expect("present");
4203        assert!(!registered.authenticated);
4204        assert!(registered.state_ver_key.is_none());
4205        assert_excluded_from_active_set(&state, val2.account);
4206
4207        Ok(())
4208    }
4209
4210    #[test]
4211    fn test_register_v3_reusing_v1_schnorr_key_keeps_both_validators() -> anyhow::Result<()> {
4212        let v1 = TestValidator::random();
4213        let mut v3 = TestValidator::random();
4214        v3.schnorr_vk = v1.schnorr_vk;
4215
4216        let events = Vec::from([
4217            StakeTableEvent::Register((&v1).into()),
4218            StakeTableEvent::RegisterV3((&v3).into()),
4219        ]);
4220        let (validators, _) = validators_from_l1_events(events.into_iter())?;
4221
4222        let first = validators.get(&v1.account).expect("present");
4223        assert!(first.authenticated);
4224        assert!(first.state_ver_key.is_some());
4225
4226        let second = validators.get(&v3.account).expect("present");
4227        assert!(!second.authenticated);
4228        assert!(second.state_ver_key.is_none());
4229
4230        Ok(())
4231    }
4232
4233    #[test]
4234    fn test_register_v3_duplicate_x25519_key() -> anyhow::Result<()> {
4235        let shared_x25519 = [7u8; 32];
4236        let val1 = TestValidator::random().with_x25519_key(shared_x25519);
4237        let val2 = TestValidator::random().with_x25519_key(shared_x25519);
4238
4239        let mut state = StakeTableState::default();
4240        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
4241        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
4242        assert_matches!(result, Err(StakeTableError::X25519KeyAlreadyUsed(_)));
4243
4244        Ok(())
4245    }
4246
4247    #[test]
4248    fn test_register_v3_rejects_exited_validator() -> anyhow::Result<()> {
4249        let val = TestValidator::random();
4250
4251        let mut state = StakeTableState::default();
4252        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4253        state.apply_event(StakeTableEvent::DeregisterV2(ValidatorExitV2 {
4254            validator: val.account,
4255            unlocksAt: U256::from(1000u64),
4256        }))??;
4257
4258        let result = state.apply_event(StakeTableEvent::RegisterV3((&val).into()));
4259        assert_matches!(
4260            result,
4261            Err(StakeTableError::ValidatorAlreadyExited(addr)) if addr == val.account
4262        );
4263
4264        Ok(())
4265    }
4266
4267    #[test]
4268    fn test_register_v3_zero_x25519_soft_fails() -> anyhow::Result<()> {
4269        let val = TestValidator::random().with_x25519_key([0u8; 32]);
4270
4271        let mut state = StakeTableState::default();
4272        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4273
4274        let registered = state.validators().get(&val.account).unwrap();
4275        assert!(registered.x25519_key.is_none());
4276        assert!(state.used_x25519_keys().is_empty());
4277
4278        Ok(())
4279    }
4280
4281    #[test]
4282    fn test_x25519_key_update_zero_soft_fails() -> anyhow::Result<()> {
4283        let val = TestValidator::random();
4284
4285        let mut state = StakeTableState::default();
4286        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4287
4288        state.apply_event(val.x25519_update([0u8; 32]))??;
4289        let registered = state.validators().get(&val.account).unwrap();
4290        assert!(registered.x25519_key.is_none());
4291        assert_eq!(state.used_x25519_keys().len(), 1);
4292
4293        Ok(())
4294    }
4295
4296    /// LE encoding of the curve25519 field prime 2^255 - 19: nonzero but
4297    /// rejected by the Rust parser as non-canonical.
4298    fn noncanonical_x25519_key() -> [u8; 32] {
4299        let mut key = [0xffu8; 32];
4300        key[0] = 0xed;
4301        key[31] = 0x7f;
4302        key
4303    }
4304
4305    /// An invalid x25519 key must not halt event processing;
4306    /// the validator registers with `x25519_key: None`.
4307    #[test]
4308    fn test_register_v3_noncanonical_x25519_soft_fails() -> anyhow::Result<()> {
4309        let val = TestValidator::random().with_x25519_key(noncanonical_x25519_key());
4310
4311        let mut state = StakeTableState::default();
4312        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4313
4314        let registered = state.validators().get(&val.account).unwrap();
4315        assert!(registered.x25519_key.is_none());
4316        assert!(state.used_x25519_keys().is_empty());
4317
4318        Ok(())
4319    }
4320
4321    /// An invalid x25519 key in an update must not halt event
4322    /// processing; the validator's key is cleared.
4323    #[test]
4324    fn test_x25519_key_update_noncanonical_soft_fails() -> anyhow::Result<()> {
4325        let val = TestValidator::random();
4326
4327        let mut state = StakeTableState::default();
4328        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4329        assert!(
4330            state
4331                .validators()
4332                .get(&val.account)
4333                .unwrap()
4334                .x25519_key
4335                .is_some()
4336        );
4337
4338        state.apply_event(val.x25519_update(noncanonical_x25519_key()))??;
4339
4340        let registered = state.validators().get(&val.account).unwrap();
4341        assert!(registered.x25519_key.is_none());
4342        assert_eq!(state.used_x25519_keys().len(), 1);
4343
4344        state.apply_event(val.x25519_update([42u8; 32]))??;
4345        let registered = state.validators().get(&val.account).unwrap();
4346        assert!(registered.x25519_key.is_some());
4347        assert_eq!(state.used_x25519_keys().len(), 2);
4348
4349        Ok(())
4350    }
4351
4352    /// `used_x25519_keys` must be part of the state commitment. Without this,
4353    /// nodes diverging on x25519 uniqueness tracking would produce identical
4354    /// state hashes and the divergence would go undetected.
4355    #[test]
4356    fn test_state_commit_includes_used_x25519_keys() -> anyhow::Result<()> {
4357        let val = TestValidator::random();
4358
4359        let mut state_without = StakeTableState::default();
4360        state_without.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
4361
4362        let mut state_with = state_without.clone();
4363        state_with
4364            .used_x25519_keys
4365            .insert(x25519::PublicKey::try_from([7u8; 32].as_slice()).unwrap());
4366
4367        assert_ne!(state_without.commit(), state_with.commit());
4368        Ok(())
4369    }
4370
4371    // --- NEW_PROTOCOL_VERSION selection filter tests ---
4372
4373    /// Construct a `RegisteredValidator` with both x25519_key and p2p_addr populated.
4374    fn complete_mock_validator() -> RegisteredValidator<BLSPubKey> {
4375        let mut v = RegisteredValidator::<BLSPubKey>::mock();
4376        v.x25519_key = Some(x25519::PublicKey::try_from([42u8; 32].as_slice()).unwrap());
4377        v.p2p_addr = Some("127.0.0.1:9000".parse().unwrap());
4378        v
4379    }
4380
4381    /// Pre-upgrade: validators with missing x25519/p2p fields must still be selected.
4382    #[test]
4383    fn test_select_pre_upgrade_includes_validators_missing_network_info() {
4384        let mut map = RegisteredValidatorMap::new();
4385
4386        let incomplete = RegisteredValidator::<BLSPubKey>::mock();
4387        assert!(incomplete.x25519_key.is_none());
4388        assert!(incomplete.p2p_addr.is_none());
4389        map.insert(incomplete.account, incomplete.clone());
4390
4391        let complete = complete_mock_validator();
4392        map.insert(complete.account, complete.clone());
4393
4394        let active = select_active_validator_set(&map, EPOCH_VERSION).unwrap();
4395        assert!(active.contains_key(&incomplete.account));
4396        assert!(active.contains_key(&complete.account));
4397    }
4398
4399    /// Post-upgrade: validator missing x25519_key is filtered out.
4400    #[test]
4401    fn test_select_post_upgrade_excludes_missing_x25519() {
4402        let mut map = RegisteredValidatorMap::new();
4403
4404        let mut missing_x25519 = complete_mock_validator();
4405        missing_x25519.x25519_key = None;
4406        map.insert(missing_x25519.account, missing_x25519.clone());
4407
4408        let complete = complete_mock_validator();
4409        map.insert(complete.account, complete.clone());
4410
4411        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
4412        assert!(!active.contains_key(&missing_x25519.account));
4413        assert!(active.contains_key(&complete.account));
4414    }
4415
4416    /// Post-upgrade: validator missing p2p_addr is filtered out.
4417    #[test]
4418    fn test_select_post_upgrade_excludes_missing_p2p() {
4419        let mut map = RegisteredValidatorMap::new();
4420
4421        let mut missing_p2p = complete_mock_validator();
4422        missing_p2p.p2p_addr = None;
4423        map.insert(missing_p2p.account, missing_p2p.clone());
4424
4425        let complete = complete_mock_validator();
4426        map.insert(complete.account, complete.clone());
4427
4428        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
4429        assert!(!active.contains_key(&missing_p2p.account));
4430        assert!(active.contains_key(&complete.account));
4431    }
4432
4433    /// Post-upgrade: a p2p address nobody can dial counts as missing, before the upgrade it
4434    /// doesn't matter.
4435    #[test]
4436    fn test_select_post_upgrade_excludes_invalid_p2p() {
4437        let mut map = RegisteredValidatorMap::new();
4438
4439        let mut invalid_p2p = complete_mock_validator();
4440        invalid_p2p.p2p_addr = Some("[[a]:b]:80".parse().unwrap());
4441        map.insert(invalid_p2p.account, invalid_p2p.clone());
4442
4443        let complete = complete_mock_validator();
4444        map.insert(complete.account, complete.clone());
4445
4446        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
4447        assert!(!active.contains_key(&invalid_p2p.account));
4448        assert!(active.contains_key(&complete.account));
4449
4450        let active = select_active_validator_set(&map, EPOCH_VERSION).unwrap();
4451        assert!(active.contains_key(&invalid_p2p.account));
4452    }
4453
4454    /// Post-upgrade: a single complete validator is kept.
4455    #[test]
4456    fn test_select_post_upgrade_keeps_complete() {
4457        let mut map = RegisteredValidatorMap::new();
4458        let complete = complete_mock_validator();
4459        map.insert(complete.account, complete.clone());
4460
4461        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
4462        assert!(active.contains_key(&complete.account));
4463    }
4464
4465    /// Post-upgrade: if all validators are incomplete, selection returns
4466    /// `NoValidValidators`.
4467    #[test]
4468    fn test_select_post_upgrade_all_incomplete_fails() {
4469        let mut map = RegisteredValidatorMap::new();
4470        for _ in 0..5 {
4471            let v = RegisteredValidator::<BLSPubKey>::mock();
4472            map.insert(v.account, v);
4473        }
4474
4475        let result = select_active_validator_set(&map, NEW_PROTOCOL_VERSION);
4476        assert_matches!(result, Err(StakeTableError::NoValidValidators));
4477    }
4478
4479    /// Post-upgrade: when more than MAX_VALIDATORS complete validators exist alongside
4480    /// incomplete ones, only complete validators appear in the result and the count is
4481    /// clamped to MAX_VALIDATORS.
4482    #[test]
4483    fn test_select_post_upgrade_top_n_only_complete() {
4484        let mut map = RegisteredValidatorMap::new();
4485        let mut complete_accounts = HashSet::new();
4486        let mut incomplete_accounts = HashSet::new();
4487
4488        // Build MAX_VALIDATORS + 50 validators, alternating complete/incomplete. Give
4489        // complete validators higher stake than incomplete ones so both the filter and
4490        // the stake-based top-N truncation cleanly pick complete ones.
4491        for i in 0..(MAX_VALIDATORS + 50) {
4492            if i % 2 == 0 {
4493                let mut v = complete_mock_validator();
4494                v.stake = U256::from(1_000_000_000_u64 + i as u64);
4495                complete_accounts.insert(v.account);
4496                map.insert(v.account, v);
4497            } else {
4498                let mut v = RegisteredValidator::<BLSPubKey>::mock();
4499                v.stake = U256::from(10_u64);
4500                incomplete_accounts.insert(v.account);
4501                map.insert(v.account, v);
4502            }
4503        }
4504
4505        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
4506
4507        assert!(
4508            active.len() <= MAX_VALIDATORS,
4509            "active has {} validators, expected at most {}",
4510            active.len(),
4511            MAX_VALIDATORS
4512        );
4513        for addr in active.keys() {
4514            assert!(
4515                complete_accounts.contains(addr),
4516                "incomplete validator {addr:?} ended up in active set"
4517            );
4518            assert!(!incomplete_accounts.contains(addr));
4519        }
4520    }
4521
4522    /// Verify the version boundary: the filter only kicks in at/after
4523    /// `NEW_PROTOCOL_VERSION`. A validator missing the x25519 key must be retained at
4524    /// every prior version and rejected at `NEW_PROTOCOL_VERSION`.
4525    #[test]
4526    fn test_select_version_boundary() {
4527        let mut v = complete_mock_validator();
4528        v.x25519_key = None;
4529        let mut map = RegisteredValidatorMap::new();
4530        map.insert(v.account, v.clone());
4531
4532        for protocol_version in [
4533            EPOCH_VERSION,
4534            DRB_AND_HEADER_UPGRADE_VERSION,
4535            EPOCH_REWARD_VERSION,
4536        ] {
4537            let active = select_active_validator_set(&map, protocol_version).unwrap();
4538            assert!(
4539                active.contains_key(&v.account),
4540                "missing x25519 validator should be included at protocol version \
4541                 {protocol_version}",
4542            );
4543        }
4544
4545        let result = select_active_validator_set(&map, NEW_PROTOCOL_VERSION);
4546        assert_matches!(result, Err(StakeTableError::NoValidValidators));
4547    }
4548
4549    /// Integration test: a RegisterV3 event with an unparsable p2p address results in
4550    /// `p2p_addr: None`, which is accepted pre-upgrade and rejected post-upgrade.
4551    #[test]
4552    fn test_p2p_parse_fail_pre_upgrade_included_post_upgrade_excluded() -> anyhow::Result<()> {
4553        let val = TestValidator::random().with_p2p_addr("host:notaport");
4554
4555        let mut state = StakeTableState::default();
4556        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
4557        // Delegate so the validator passes the stake/delegator checks.
4558        state.apply_event(StakeTableEvent::Delegate(Delegated {
4559            delegator: Address::random(),
4560            validator: val.account,
4561            amount: U256::from(100),
4562        }))??;
4563
4564        let registered = state.validators().get(&val.account).unwrap();
4565        assert_eq!(registered.p2p_addr, None);
4566
4567        // Pre-upgrade: the validator is selected.
4568        let pre = select_active_validator_set(state.validators(), EPOCH_VERSION).unwrap();
4569        assert!(pre.contains_key(&val.account));
4570
4571        // Post-upgrade: the sole validator is filtered out, so selection fails.
4572        let post = select_active_validator_set(state.validators(), NEW_PROTOCOL_VERSION);
4573        assert_matches!(post, Err(StakeTableError::NoValidValidators));
4574
4575        Ok(())
4576    }
4577
4578    #[test]
4579    fn test_register_v1_zero_g2_excluded_from_active_set() {
4580        let account = Address::random();
4581        let mut state = StakeTableState::default();
4582        state
4583            .apply_event(StakeTableEvent::Register(ValidatorRegistered {
4584                account,
4585                blsVk: zero_g2(),
4586                schnorrVk: StateKeyPair::generate().ver_key().into(),
4587                commission: 0,
4588            }))
4589            .unwrap()
4590            .unwrap();
4591
4592        let v = state.validators().get(&account).expect("present");
4593        assert!(v.stake_table_key.is_none());
4594        assert!(!v.authenticated);
4595
4596        state
4597            .apply_event(StakeTableEvent::Delegate(Delegated {
4598                delegator: Address::random(),
4599                validator: account,
4600                amount: U256::from(100),
4601            }))
4602            .unwrap()
4603            .unwrap();
4604
4605        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4606            Err(StakeTableError::NoValidValidators) => {},
4607            Ok(map) => assert!(map.get(&account).is_none()),
4608            Err(e) => panic!("unexpected error: {e}"),
4609        }
4610    }
4611
4612    #[test]
4613    fn test_register_v2_zero_g2_excluded_from_active_set() {
4614        let account = Address::random();
4615        let mut state = StakeTableState::default();
4616        state
4617            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
4618                account,
4619                blsVK: zero_g2(),
4620                schnorrVK: StateKeyPair::generate().ver_key().into(),
4621                commission: 0,
4622                blsSig: zero_g1().into(),
4623                schnorrSig: Bytes::default(),
4624                metadataUri: String::new(),
4625            }))
4626            .unwrap()
4627            .unwrap();
4628
4629        let v = state.validators().get(&account).expect("present");
4630        assert!(v.stake_table_key.is_none());
4631        assert!(!v.authenticated);
4632
4633        state
4634            .apply_event(StakeTableEvent::Delegate(Delegated {
4635                delegator: Address::random(),
4636                validator: account,
4637                amount: U256::from(100),
4638            }))
4639            .unwrap()
4640            .unwrap();
4641
4642        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4643            Err(StakeTableError::NoValidValidators) => {},
4644            Ok(map) => assert!(map.get(&account).is_none()),
4645            Err(e) => panic!("unexpected error: {e}"),
4646        }
4647    }
4648
4649    #[test]
4650    fn test_register_v3_zero_g2_excluded_from_active_set() {
4651        let val = TestValidator::random();
4652        let account = val.account;
4653        let mut state = StakeTableState::default();
4654        state
4655            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
4656                &val,
4657                zero_g2(),
4658            )))
4659            .unwrap()
4660            .unwrap();
4661
4662        let v = state.validators().get(&account).expect("present");
4663        assert!(v.stake_table_key.is_none());
4664        assert!(!v.authenticated);
4665
4666        state
4667            .apply_event(StakeTableEvent::Delegate(Delegated {
4668                delegator: Address::random(),
4669                validator: account,
4670                amount: U256::from(100),
4671            }))
4672            .unwrap()
4673            .unwrap();
4674
4675        assert_excluded_from_active_set(&state, account);
4676    }
4677
4678    fn zero_schnorr() -> hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4679        hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4680            x: U256::ZERO,
4681            y: U256::ZERO,
4682        }
4683    }
4684
4685    /// The twisted Edwards identity (0, 1): on-curve and in-subgroup but not a
4686    /// valid key.
4687    fn identity_schnorr() -> hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4688        hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4689            x: U256::ZERO,
4690            y: U256::from(1),
4691        }
4692    }
4693
4694    /// Build a valid V3 registration for `val` but overwrite `blsVK` with a poison value.
4695    /// The blsSig is for the original key so `authenticate()` will also fail, which is
4696    /// fine: the BLS key is unparsable regardless.
4697    fn register_v3_with_bls(val: &TestValidator, bls: G2PointSol) -> ValidatorRegisteredV3 {
4698        let mut reg = ValidatorRegisteredV3::from(val);
4699        reg.blsVK = bls;
4700        reg
4701    }
4702
4703    /// Check that `account` is excluded from the active validator set.
4704    fn assert_excluded_from_active_set(state: &StakeTableState, account: Address) {
4705        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4706            Err(StakeTableError::NoValidValidators) => {},
4707            Ok(map) => assert!(map.get(&account).is_none()),
4708            Err(e) => panic!("unexpected error: {e}"),
4709        }
4710    }
4711
4712    #[test]
4713    fn test_register_v1_identity_schnorr_excluded_from_active_set() {
4714        let account = Address::random();
4715        let bls: G2PointSol = BLSPubKey::generated_from_seed_indexed([8u8; 32], 0)
4716            .0
4717            .into();
4718        let mut state = StakeTableState::default();
4719        state
4720            .apply_event(StakeTableEvent::Register(ValidatorRegistered {
4721                account,
4722                blsVk: bls,
4723                schnorrVk: identity_schnorr(),
4724                commission: 0,
4725            }))
4726            .unwrap()
4727            .unwrap();
4728
4729        let v = state.validators().get(&account).expect("present");
4730        assert!(
4731            !v.authenticated,
4732            "validator with identity schnorr key must be unauthenticated"
4733        );
4734        assert!(v.state_ver_key.is_none());
4735
4736        state
4737            .apply_event(StakeTableEvent::Delegate(Delegated {
4738                delegator: Address::random(),
4739                validator: account,
4740                amount: U256::from(100),
4741            }))
4742            .unwrap()
4743            .unwrap();
4744
4745        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4746            Err(StakeTableError::NoValidValidators) => {},
4747            Ok(map) => assert!(map.get(&account).is_none()),
4748            Err(e) => panic!("unexpected error: {e}"),
4749        }
4750    }
4751
4752    #[test]
4753    fn test_register_v1_invalid_schnorr_excluded_from_active_set() {
4754        let account = Address::random();
4755        let bls: G2PointSol = BLSPubKey::generated_from_seed_indexed([9u8; 32], 0)
4756            .0
4757            .into();
4758        let mut state = StakeTableState::default();
4759        state
4760            .apply_event(StakeTableEvent::Register(ValidatorRegistered {
4761                account,
4762                blsVk: bls,
4763                schnorrVk: zero_schnorr(),
4764                commission: 0,
4765            }))
4766            .unwrap()
4767            .unwrap();
4768
4769        let v = state.validators().get(&account).expect("present");
4770        assert!(
4771            !v.authenticated,
4772            "validator with zero schnorr key must be unauthenticated"
4773        );
4774
4775        state
4776            .apply_event(StakeTableEvent::Delegate(Delegated {
4777                delegator: Address::random(),
4778                validator: account,
4779                amount: U256::from(100),
4780            }))
4781            .unwrap()
4782            .unwrap();
4783
4784        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4785            Err(StakeTableError::NoValidValidators) => {},
4786            Ok(map) => assert!(map.get(&account).is_none()),
4787            Err(e) => panic!("unexpected error: {e}"),
4788        }
4789    }
4790
4791    #[test]
4792    fn test_register_v3_identity_schnorr_excluded_from_active_set() {
4793        let val = TestValidator::random();
4794        let account = val.account;
4795        let mut reg = ValidatorRegisteredV3::from(&val);
4796        reg.schnorrVK = identity_schnorr();
4797        let mut state = StakeTableState::default();
4798        state
4799            .apply_event(StakeTableEvent::RegisterV3(reg))
4800            .unwrap()
4801            .unwrap();
4802
4803        let v = state.validators().get(&account).expect("present");
4804        assert!(
4805            !v.authenticated,
4806            "validator with identity schnorr key must be unauthenticated"
4807        );
4808        assert!(v.state_ver_key.is_none());
4809
4810        state
4811            .apply_event(StakeTableEvent::Delegate(Delegated {
4812                delegator: Address::random(),
4813                validator: account,
4814                amount: U256::from(100),
4815            }))
4816            .unwrap()
4817            .unwrap();
4818
4819        assert_excluded_from_active_set(&state, account);
4820    }
4821
4822    #[test]
4823    fn test_register_v3_invalid_schnorr_excluded_from_active_set() {
4824        let val = TestValidator::random();
4825        let account = val.account;
4826        let mut reg = ValidatorRegisteredV3::from(&val);
4827        reg.schnorrVK = zero_schnorr();
4828        let mut state = StakeTableState::default();
4829        state
4830            .apply_event(StakeTableEvent::RegisterV3(reg))
4831            .unwrap()
4832            .unwrap();
4833
4834        let v = state.validators().get(&account).expect("present");
4835        assert!(
4836            !v.authenticated,
4837            "validator with zero schnorr key must be unauthenticated"
4838        );
4839        assert!(v.state_ver_key.is_none());
4840
4841        state
4842            .apply_event(StakeTableEvent::Delegate(Delegated {
4843                delegator: Address::random(),
4844                validator: account,
4845                amount: U256::from(100),
4846            }))
4847            .unwrap()
4848            .unwrap();
4849
4850        assert_excluded_from_active_set(&state, account);
4851    }
4852
4853    #[test]
4854    fn test_zero_bls_key_register_unauthenticated() {
4855        let account = Address::random();
4856        let schnorr_key_pair = StateKeyPair::generate();
4857        let mut state = StakeTableState::default();
4858
4859        let reg_v2 = ValidatorRegisteredV2 {
4860            account,
4861            blsVK: zero_g2(),
4862            schnorrVK: schnorr_key_pair.ver_key().into(),
4863            commission: 0,
4864            blsSig: zero_g1().into(),
4865            schnorrSig: Bytes::default(),
4866            metadataUri: String::new(),
4867        };
4868        state
4869            .apply_event(StakeTableEvent::RegisterV2(reg_v2))
4870            .expect("no fatal error")
4871            .expect("registered as unauthenticated");
4872        let registered = state.validators().get(&account).expect("present");
4873        assert!(!registered.authenticated);
4874        assert!(registered.stake_table_key.is_none());
4875        assert!(AuthenticatedValidator::try_from(registered).is_err());
4876
4877        // V1 Register on a fresh state takes the same path.
4878        let account_v1 = Address::random();
4879        let schnorr_v1 = StateKeyPair::generate();
4880        let mut state_v1 = StakeTableState::default();
4881        let reg_v1 = ValidatorRegistered {
4882            account: account_v1,
4883            blsVk: zero_g2(),
4884            schnorrVk: schnorr_v1.ver_key().into(),
4885            commission: 0,
4886        };
4887        state_v1
4888            .apply_event(StakeTableEvent::Register(reg_v1))
4889            .expect("no fatal error")
4890            .expect("registered as unauthenticated");
4891        let registered_v1 = state_v1.validators().get(&account_v1).expect("present");
4892        assert!(!registered_v1.authenticated);
4893        assert!(registered_v1.stake_table_key.is_none());
4894
4895        // KeyUpdateV2 with unparsable key: soft skip, prior key preserved.
4896        let valid_val = TestValidator::random();
4897        state
4898            .apply_event(StakeTableEvent::RegisterV2((&valid_val).into()))
4899            .unwrap()
4900            .unwrap();
4901        let prior_key_v2 = state
4902            .validators()
4903            .get(&valid_val.account)
4904            .unwrap()
4905            .stake_table_key;
4906        let update = ConsensusKeysUpdatedV2 {
4907            account: valid_val.account,
4908            blsVK: zero_g2(),
4909            schnorrVK: schnorr_key_pair.ver_key().into(),
4910            blsSig: zero_g1().into(),
4911            schnorrSig: Bytes::default(),
4912        };
4913        assert_matches!(
4914            state.apply_event(StakeTableEvent::KeyUpdateV2(update)),
4915            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
4916        );
4917        assert_eq!(
4918            state
4919                .validators()
4920                .get(&valid_val.account)
4921                .unwrap()
4922                .stake_table_key,
4923            prior_key_v2
4924        );
4925
4926        // V1 KeyUpdate keeps the skip-event behavior so the prior valid key is preserved.
4927        let valid_v1 = TestValidator::random();
4928        state_v1
4929            .apply_event(StakeTableEvent::Register((&valid_v1).into()))
4930            .unwrap()
4931            .unwrap();
4932        let prior_key = state_v1
4933            .validators()
4934            .get(&valid_v1.account)
4935            .unwrap()
4936            .stake_table_key;
4937        let update_v1 = ConsensusKeysUpdated {
4938            account: valid_v1.account,
4939            blsVK: zero_g2(),
4940            schnorrVK: schnorr_key_pair.ver_key().into(),
4941        };
4942        assert_matches!(
4943            state_v1.apply_event(StakeTableEvent::KeyUpdate(update_v1)),
4944            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
4945        );
4946        assert_eq!(
4947            state_v1
4948                .validators()
4949                .get(&valid_v1.account)
4950                .unwrap()
4951                .stake_table_key,
4952            prior_key
4953        );
4954    }
4955
4956    /// Regression for the bug: a Register with an unparsable BLS key must
4957    /// leave the validator in the map so subsequent events to that account
4958    /// (delegations, exits) apply cleanly instead of hitting ValidatorNotFound.
4959    #[test]
4960    fn test_zero_bls_key_registered_unauthenticated_in_from_l1_events() {
4961        let valid_val = TestValidator::random();
4962        let poison_account = Address::random();
4963        let schnorr_key_pair = StateKeyPair::generate();
4964
4965        let events = vec![
4966            StakeTableEvent::RegisterV2((&valid_val).into()),
4967            StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
4968                account: poison_account,
4969                blsVK: zero_g2(),
4970                schnorrVK: schnorr_key_pair.ver_key().into(),
4971                commission: 0,
4972                blsSig: zero_g1().into(),
4973                schnorrSig: Bytes::default(),
4974                metadataUri: String::new(),
4975            }),
4976            StakeTableEvent::Delegate(Delegated {
4977                delegator: Address::random(),
4978                validator: poison_account,
4979                amount: U256::from(10),
4980            }),
4981        ];
4982
4983        let (validators, _) = validators_from_l1_events(events.into_iter()).expect("must not fail");
4984        let valid = validators
4985            .get(&valid_val.account)
4986            .expect("valid validator present");
4987        assert!(valid.authenticated);
4988
4989        let poisoned = validators
4990            .get(&poison_account)
4991            .expect("poison validator present as unauthenticated");
4992        assert!(!poisoned.authenticated);
4993        assert!(poisoned.stake_table_key.is_none());
4994        assert_eq!(poisoned.stake, U256::from(10));
4995        assert!(AuthenticatedValidator::try_from(poisoned).is_err());
4996    }
4997
4998    #[test]
4999    fn test_register_v3_zero_bls_key_unauthenticated_in_from_l1_events() {
5000        let valid_val = TestValidator::random();
5001        let poison_val = TestValidator::random();
5002        let poison_account = poison_val.account;
5003
5004        let events = vec![
5005            StakeTableEvent::RegisterV3((&valid_val).into()),
5006            StakeTableEvent::RegisterV3(register_v3_with_bls(&poison_val, zero_g2())),
5007            StakeTableEvent::Delegate(Delegated {
5008                delegator: Address::random(),
5009                validator: poison_account,
5010                amount: U256::from(10),
5011            }),
5012        ];
5013
5014        let (validators, _) = validators_from_l1_events(events.into_iter()).expect("must not fail");
5015        let valid = validators
5016            .get(&valid_val.account)
5017            .expect("valid validator present");
5018        assert!(valid.authenticated);
5019
5020        let poisoned = validators
5021            .get(&poison_account)
5022            .expect("poison validator present as unauthenticated");
5023        assert!(!poisoned.authenticated);
5024        assert!(poisoned.stake_table_key.is_none());
5025        assert_eq!(poisoned.stake, U256::from(10));
5026        assert!(AuthenticatedValidator::try_from(poisoned).is_err());
5027    }
5028
5029    /// Two distinct off-curve registrations must both succeed; unparsable
5030    /// keys are never inserted into `used_bls_keys`.
5031    #[test]
5032    fn test_two_distinct_unparsable_g2_keys_both_register() {
5033        let zero = zero_g2();
5034        // x=(1, 0), y=(0, 0): violates y² = x³ + b' over Fp2.
5035        let nonzero = G2PointSol {
5036            x0: U256::from(1),
5037            x1: U256::ZERO,
5038            y0: U256::ZERO,
5039            y1: U256::ZERO,
5040        };
5041        assert!(BLSPubKey::try_from(zero).is_err());
5042        assert!(BLSPubKey::try_from(nonzero).is_err());
5043        assert_ne!(zero, nonzero);
5044
5045        let acc_a = Address::random();
5046        let acc_b = Address::random();
5047
5048        let mut state = StakeTableState::default();
5049        state
5050            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
5051                account: acc_a,
5052                blsVK: zero,
5053                schnorrVK: StateKeyPair::generate().ver_key().into(),
5054                commission: 0,
5055                blsSig: zero_g1().into(),
5056                schnorrSig: Bytes::default(),
5057                metadataUri: String::new(),
5058            }))
5059            .expect("first unparsable register must not fail")
5060            .expect("first unparsable register must apply");
5061        state
5062            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
5063                account: acc_b,
5064                blsVK: nonzero,
5065                schnorrVK: StateKeyPair::generate().ver_key().into(),
5066                commission: 0,
5067                blsSig: zero_g1().into(),
5068                schnorrSig: Bytes::default(),
5069                metadataUri: String::new(),
5070            }))
5071            .expect("second unparsable register must not fail")
5072            .expect("second unparsable register must apply");
5073
5074        for account in [acc_a, acc_b] {
5075            let v = state.validators().get(&account).expect("present");
5076            assert!(!v.authenticated);
5077            assert!(v.stake_table_key.is_none());
5078        }
5079        assert!(state.used_bls_keys().is_empty());
5080    }
5081
5082    /// Two distinct V3 off-curve registrations must both succeed; unparsable
5083    /// keys are never inserted into `used_bls_keys`. Each account uses a
5084    /// distinct nonzero x25519 key so the x25519 dedup check does not fire.
5085    #[test]
5086    fn test_register_v3_two_distinct_unparsable_g2_keys_both_register() {
5087        let zero = zero_g2();
5088        // x=(1, 0), y=(0, 0): violates y² = x³ + b' over Fp2.
5089        let nonzero = G2PointSol {
5090            x0: U256::from(1),
5091            x1: U256::ZERO,
5092            y0: U256::ZERO,
5093            y1: U256::ZERO,
5094        };
5095        assert!(BLSPubKey::try_from(zero).is_err());
5096        assert!(BLSPubKey::try_from(nonzero).is_err());
5097        assert_ne!(zero, nonzero);
5098
5099        let val_a = TestValidator::random();
5100        let val_b = TestValidator::random();
5101        let acc_a = val_a.account;
5102        let acc_b = val_b.account;
5103
5104        let mut state = StakeTableState::default();
5105        state
5106            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
5107                &val_a, zero,
5108            )))
5109            .expect("first unparsable register must not fail")
5110            .expect("first unparsable register must apply");
5111        state
5112            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
5113                &val_b, nonzero,
5114            )))
5115            .expect("second unparsable register must not fail")
5116            .expect("second unparsable register must apply");
5117
5118        for account in [acc_a, acc_b] {
5119            let v = state.validators().get(&account).expect("present");
5120            assert!(!v.authenticated);
5121            assert!(v.stake_table_key.is_none());
5122        }
5123        assert!(state.used_bls_keys().is_empty());
5124    }
5125
5126    /// After a poison Register, a subsequent valid KeyUpdateV2 must
5127    /// authenticate the validator and install the new keys.
5128    #[test]
5129    fn test_zero_g2_then_valid_key_update() {
5130        let mut state = StakeTableState::default();
5131        let valid_val = TestValidator::random();
5132        let account = valid_val.account;
5133
5134        state
5135            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
5136                account,
5137                blsVK: zero_g2(),
5138                schnorrVK: StateKeyPair::generate().ver_key().into(),
5139                commission: 0,
5140                blsSig: zero_g1().into(),
5141                schnorrSig: Bytes::default(),
5142                metadataUri: String::new(),
5143            }))
5144            .unwrap()
5145            .unwrap();
5146        assert!(!state.validators().get(&account).unwrap().authenticated);
5147
5148        let new_keys = TestValidator::random_update_keys(account, 0);
5149        let update: ConsensusKeysUpdatedV2 = (&new_keys).into();
5150        state
5151            .apply_event(StakeTableEvent::KeyUpdateV2(update))
5152            .unwrap()
5153            .unwrap();
5154
5155        let v = state.validators().get(&account).unwrap();
5156        let expected_bls = BLSPubKey::try_from(new_keys.bls_vk).unwrap();
5157        assert_eq!(v.stake_table_key.as_ref(), Some(&expected_bls));
5158        // KeyUpdateV2 authenticates signatures, so on success the validator
5159        // is promoted to `authenticated=true`.
5160        assert!(v.authenticated);
5161    }
5162
5163    #[test]
5164    fn test_register_v3_zero_g2_then_valid_key_update() {
5165        let val = TestValidator::random();
5166        let account = val.account;
5167        let mut state = StakeTableState::default();
5168
5169        state
5170            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
5171                &val,
5172                zero_g2(),
5173            )))
5174            .unwrap()
5175            .unwrap();
5176        assert!(!state.validators().get(&account).unwrap().authenticated);
5177
5178        let new_keys = TestValidator::random_update_keys(account, 0);
5179        let update: ConsensusKeysUpdatedV2 = (&new_keys).into();
5180        state
5181            .apply_event(StakeTableEvent::KeyUpdateV2(update))
5182            .unwrap()
5183            .unwrap();
5184
5185        let v = state.validators().get(&account).unwrap();
5186        let expected_bls = BLSPubKey::try_from(new_keys.bls_vk).unwrap();
5187        assert_eq!(v.stake_table_key.as_ref(), Some(&expected_bls));
5188        // KeyUpdateV2 authenticates signatures, so on success the validator
5189        // is promoted to `authenticated=true`.
5190        assert!(v.authenticated);
5191    }
5192
5193    #[test]
5194    fn test_key_update_bad_key_no_corruption() {
5195        // V1 path
5196        let mut state_v1 = StakeTableState::default();
5197        let val = TestValidator::random();
5198        state_v1
5199            .apply_event(StakeTableEvent::Register((&val).into()))
5200            .unwrap()
5201            .unwrap();
5202        let prior_v1 = state_v1
5203            .validators()
5204            .get(&val.account)
5205            .unwrap()
5206            .stake_table_key;
5207        assert_matches!(
5208            state_v1.apply_event(StakeTableEvent::KeyUpdate(ConsensusKeysUpdated {
5209                account: val.account,
5210                blsVK: zero_g2(),
5211                schnorrVK: StateKeyPair::generate().ver_key().into(),
5212            })),
5213            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
5214        );
5215        assert_eq!(
5216            state_v1
5217                .validators()
5218                .get(&val.account)
5219                .unwrap()
5220                .stake_table_key,
5221            prior_v1
5222        );
5223
5224        // V2 path: replay-from-persistence safety net; live fetch is pre-filtered.
5225        let mut state_v2 = StakeTableState::default();
5226        let val2 = TestValidator::random();
5227        state_v2
5228            .apply_event(StakeTableEvent::RegisterV2((&val2).into()))
5229            .unwrap()
5230            .unwrap();
5231        let prior_v2 = state_v2
5232            .validators()
5233            .get(&val2.account)
5234            .unwrap()
5235            .stake_table_key;
5236        assert_matches!(
5237            state_v2.apply_event(StakeTableEvent::KeyUpdateV2(ConsensusKeysUpdatedV2 {
5238                account: val2.account,
5239                blsVK: zero_g2(),
5240                schnorrVK: StateKeyPair::generate().ver_key().into(),
5241                blsSig: zero_g1().into(),
5242                schnorrSig: Bytes::default(),
5243            })),
5244            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
5245        );
5246        assert_eq!(
5247            state_v2
5248                .validators()
5249                .get(&val2.account)
5250                .unwrap()
5251                .stake_table_key,
5252            prior_v2
5253        );
5254    }
5255}
5256
5257// Deploys StakeTableV3 on anvil and fuzzes the property: Solidity accepts an x25519 key =>
5258// `apply_event` returns the outer `Ok(_)` and stores `parse_x25519_key(key).ok()`.
5259// See `parse_x25519_key` for why contract-rejected keys must still soft-fail.
5260#[cfg(test)]
5261mod proptest_x25519_key {
5262    use alloy::{
5263        primitives::FixedBytes,
5264        providers::{ProviderBuilder, WalletProvider},
5265    };
5266    use proptest::{
5267        prelude::*,
5268        test_runner::{Config as ProptestConfig, TestRunner},
5269    };
5270
5271    use super::{testing::TestValidator, *};
5272
5273    fn x25519_key_strategy() -> impl Strategy<Value = [u8; 32]> {
5274        // LE encoding of the curve25519 field prime p = 2^255 - 19.
5275        let mut p = [0xffu8; 32];
5276        p[0] = 0xed;
5277        p[31] = 0x7f;
5278        prop_oneof![
5279            // Uniform random bytes
5280            any::<[u8; 32]>(),
5281            // All zeros (contract rejects)
5282            Just([0u8; 32]),
5283            // Exactly p: nonzero but non-canonical
5284            Just(p),
5285            // The 19 non-canonical encodings p..2^255-1 (0xed + 18 = 0xff, so no overflow)
5286            (0..19u8).prop_map(move |k| {
5287                let mut key = p;
5288                key[0] += k;
5289                key
5290            }),
5291            // p - 1: largest canonical value
5292            Just({
5293                let mut key = p;
5294                key[0] = 0xec;
5295                key
5296            }),
5297            // 2^256 - 1: top bit set. Rust parses it (bit 255 is only masked during DH),
5298            // the contract rejects it as a non-canonical alias.
5299            Just([0xffu8; 32]),
5300            // Top bit only: same alias class as above
5301            Just({
5302                let mut key = [0u8; 32];
5303                key[31] = 0x80;
5304                key
5305            }),
5306            // Small integers, including low-order point encodings
5307            (0..=2u8).prop_map(|v| {
5308                let mut key = [0u8; 32];
5309                key[0] = v;
5310                key
5311            }),
5312        ]
5313    }
5314
5315    #[test]
5316    fn solidity_rust_x25519_validation_equivalence() {
5317        let rt = tokio::runtime::Runtime::new().unwrap();
5318
5319        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
5320        let sender = provider.default_signer_address();
5321        let contract_addr = rt.block_on(async {
5322            let contract = StakeTableV3::deploy(&provider).await.unwrap();
5323            *contract.address()
5324        });
5325        let contract = StakeTableV3::new(contract_addr, &provider);
5326
5327        // Committed registration for the update-path oracle: gives `updateX25519Key` an
5328        // active validator. Done before both passes so every case sees identical state.
5329        let registered = TestValidator::random_update_keys(sender, 100).with_x25519_key([7u8; 32]);
5330        rt.block_on(async {
5331            let receipt = contract
5332                .registerValidatorV3(
5333                    registered.bls_vk,
5334                    registered.schnorr_vk,
5335                    registered.bls_sig.into(),
5336                    registered.schnorr_sig.clone(),
5337                    registered.commission,
5338                    String::new(),
5339                    FixedBytes(registered.x25519_key),
5340                    registered.p2p_addr.clone(),
5341                )
5342                .send()
5343                .await
5344                .unwrap()
5345                .get_receipt()
5346                .await
5347                .unwrap();
5348            assert!(receipt.status());
5349        });
5350
5351        // Rust mirror of the committed registration, base state for the update pass.
5352        let mut registered_state = StakeTableState::default();
5353        registered_state
5354            .apply_event(StakeTableEvent::RegisterV3((&registered).into()))
5355            .unwrap()
5356            .unwrap();
5357
5358        let base = TestValidator::random();
5359
5360        let cases = std::env::var("PROPTEST_CASES")
5361            .ok()
5362            .and_then(|v| v.parse().ok())
5363            .unwrap_or(512);
5364        let mut runner = TestRunner::new(ProptestConfig {
5365            cases,
5366            ..ProptestConfig::default()
5367        });
5368
5369        // Register path: static call so no state is committed between cases.
5370        runner
5371            .run(&x25519_key_strategy(), |key| {
5372                let sol_valid = rt.block_on(async {
5373                    contract
5374                        .registerValidatorV3(
5375                            base.bls_vk,
5376                            base.schnorr_vk,
5377                            base.bls_sig.into(),
5378                            base.schnorr_sig.clone(),
5379                            base.commission,
5380                            String::new(),
5381                            FixedBytes(key),
5382                            base.p2p_addr.clone(),
5383                        )
5384                        .from(base.account)
5385                        .call()
5386                        .await
5387                        .is_ok()
5388                });
5389
5390                let val = base.clone().with_x25519_key(key);
5391                let mut state = StakeTableState::default();
5392                let result = state.apply_event(StakeTableEvent::RegisterV3((&val).into()));
5393
5394                if sol_valid {
5395                    prop_assert!(
5396                        result.is_ok(),
5397                        "Solidity accepted registration with x25519 key {key:?} but apply_event \
5398                         failed: {result:?}"
5399                    );
5400                    let stored = state.validators().get(&val.account).unwrap().x25519_key;
5401                    // Contract-valid keys are always Rust-parsable, so `stored` must
5402                    // not degrade to `None`; the eq then pins the exact key.
5403                    prop_assert!(stored.is_some());
5404                    prop_assert_eq!(stored, parse_x25519_key(key).ok());
5405                }
5406                Ok(())
5407            })
5408            .unwrap();
5409
5410        // Update path: static call against the committed registration.
5411        runner
5412            .run(&x25519_key_strategy(), |key| {
5413                let sol_valid = rt.block_on(async {
5414                    contract
5415                        .updateX25519Key(FixedBytes(key))
5416                        .from(sender)
5417                        .call()
5418                        .await
5419                        .is_ok()
5420                });
5421
5422                let mut state = registered_state.clone();
5423                let result = state.apply_event(registered.x25519_update(key));
5424
5425                if sol_valid {
5426                    prop_assert!(
5427                        result.is_ok(),
5428                        "Solidity accepted x25519 key update {key:?} but apply_event failed: \
5429                         {result:?}"
5430                    );
5431                    let stored = state.validators().get(&sender).unwrap().x25519_key;
5432                    prop_assert!(stored.is_some());
5433                    prop_assert_eq!(stored, parse_x25519_key(key).ok());
5434                }
5435                Ok(())
5436            })
5437            .unwrap();
5438    }
5439}
5440
5441#[cfg(all(test, feature = "rlp"))]
5442mod rlp_tests {
5443    use alloy_rlp::{Decodable, Encodable};
5444    use hotshot_types::{signature_key::BLSPubKey, traits::signature_key::StateSignatureKey};
5445
5446    use super::*;
5447
5448    #[test]
5449    fn test_registered_validator_round_canonical_order() {
5450        let seed = [0; 32];
5451        let validator1 = RegisteredValidator {
5452            account: Address::random(),
5453            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed(seed, 0).0),
5454            state_ver_key: Some(SchnorrPubKey::generated_from_seed_indexed(seed, 0).0),
5455            stake: U256::MAX,
5456            commission: u16::MAX,
5457            delegators: [
5458                (Address::random(), U256::ZERO),
5459                (Address::random(), U256::MAX),
5460            ]
5461            .into_iter()
5462            .collect(),
5463            authenticated: true,
5464            x25519_key: Some(x25519::Keypair::generate().unwrap().public_key()),
5465            p2p_addr: Some(NetAddr::named("localhost", 8080)),
5466        };
5467        let validator2 = RegisteredValidator {
5468            account: Address::random(),
5469            ..validator1.clone()
5470        };
5471
5472        // Two hashsets with different capacities are very likely to end up with a different
5473        // iteration order.
5474        let mut validator_exits1 = HashSet::with_capacity(100);
5475        let mut validator_exits2 = HashSet::with_capacity(10_000);
5476        let mut used_bls_keys1 = HashSet::with_capacity(100);
5477        let mut used_bls_keys2 = HashSet::with_capacity(10_000);
5478        let mut used_schnorr_keys1 = HashSet::with_capacity(100);
5479        let mut used_schnorr_keys2 = HashSet::with_capacity(10_000);
5480        let mut used_x25519_keys1 = HashSet::with_capacity(100);
5481        let mut used_x25519_keys2 = HashSet::with_capacity(10_000);
5482
5483        for i in 0..100 {
5484            let exit = Address::random();
5485            let bls_key = BLSPubKey::generated_from_seed_indexed([0; 32], i).0;
5486            let schnorr_key = SchnorrPubKey::generated_from_seed_indexed([0; 32], i).0;
5487            let x25519_key = x25519::Keypair::generate().unwrap().public_key();
5488
5489            validator_exits1.insert(exit);
5490            validator_exits2.insert(exit);
5491            used_bls_keys1.insert(bls_key);
5492            used_bls_keys2.insert(bls_key);
5493            used_schnorr_keys1.insert(schnorr_key.clone());
5494            used_schnorr_keys2.insert(schnorr_key);
5495            used_x25519_keys1.insert(x25519_key);
5496            used_x25519_keys2.insert(x25519_key);
5497        }
5498        assert_eq!(validator_exits1, validator_exits2);
5499        assert_eq!(used_bls_keys1, used_bls_keys2);
5500        assert_eq!(used_schnorr_keys1, used_schnorr_keys2);
5501        assert_eq!(used_x25519_keys1, used_x25519_keys2);
5502        assert_ne!(
5503            validator_exits1.iter().collect::<Vec<_>>(),
5504            validator_exits2.iter().collect::<Vec<_>>()
5505        );
5506        assert_ne!(
5507            used_bls_keys1.iter().collect::<Vec<_>>(),
5508            used_bls_keys2.iter().collect::<Vec<_>>()
5509        );
5510        assert_ne!(
5511            used_schnorr_keys1.iter().collect::<Vec<_>>(),
5512            used_schnorr_keys2.iter().collect::<Vec<_>>()
5513        );
5514        assert_ne!(
5515            used_x25519_keys1.iter().collect::<Vec<_>>(),
5516            used_x25519_keys2.iter().collect::<Vec<_>>()
5517        );
5518
5519        let state1 = StakeTableState {
5520            validators: [
5521                (validator1.account, validator1.clone()),
5522                (validator2.account, validator2.clone()),
5523            ]
5524            .into_iter()
5525            .collect(),
5526            validator_exits: validator_exits1,
5527            used_bls_keys: used_bls_keys1,
5528            used_schnorr_keys: used_schnorr_keys1,
5529            used_x25519_keys: used_x25519_keys1,
5530        };
5531        let state2 = StakeTableState {
5532            validators: [
5533                (validator2.account, validator2.clone()),
5534                (validator1.account, validator1.clone()),
5535            ]
5536            .into_iter()
5537            .collect(),
5538            validator_exits: validator_exits2,
5539            used_bls_keys: used_bls_keys2,
5540            used_schnorr_keys: used_schnorr_keys2,
5541            used_x25519_keys: used_x25519_keys2,
5542        };
5543        assert_eq!(state1, state2);
5544        assert_eq!(state1.commit(), state2.commit());
5545
5546        let mut bytes1 = vec![];
5547        state1.encode(&mut bytes1);
5548        assert_eq!(bytes1.len(), state1.length());
5549
5550        let mut bytes2 = vec![];
5551        state2.encode(&mut bytes2);
5552        assert_eq!(bytes2.len(), state2.length());
5553
5554        assert_eq!(bytes1, bytes2);
5555        assert_eq!(
5556            state1,
5557            StakeTableState::decode(&mut bytes2.as_slice()).unwrap()
5558        );
5559        assert_eq!(
5560            state2,
5561            StakeTableState::decode(&mut bytes1.as_slice()).unwrap()
5562        );
5563    }
5564}