Skip to main content

espresso_types/v0/impls/
stake_table.rs

1use std::{
2    cmp::min,
3    collections::{HashMap, HashSet},
4    future::Future,
5    str::FromStr,
6    sync::Arc,
7    time::{Duration, Instant},
8};
9
10use alloy::{
11    eips::{BlockId, BlockNumberOrTag},
12    primitives::{Address, U256, utils::format_ether},
13    providers::Provider,
14    rpc::types::{Filter, Log},
15    sol_types::{SolEvent, SolEventInterface},
16};
17use anyhow::{Context, bail, ensure};
18use ark_ec::AffineRepr;
19use ark_serialize::CanonicalSerialize;
20use ark_std::One;
21use async_lock::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
22use bigdecimal::BigDecimal;
23use committable::{Commitment, Committable, RawCommitmentBuilder};
24use futures::future::BoxFuture;
25use hotshot::types::{BLSPubKey, SchnorrPubKey, SignatureKey as _};
26use hotshot_contract_adapter::sol_types::{
27    EspToken::{self, EspTokenInstance},
28    StakeTableV3::{
29        self, CommissionUpdated, ConsensusKeysUpdated, ConsensusKeysUpdatedV2, Delegated,
30        P2pAddrUpdated, StakeTableV3Events, Undelegated, UndelegatedV2, ValidatorExit,
31        ValidatorExitV2, ValidatorRegistered, ValidatorRegisteredV2, ValidatorRegisteredV3,
32        X25519KeyUpdated,
33    },
34};
35use hotshot_types::{
36    PeerConfig,
37    addr::NetAddr,
38    data::{EpochNumber, vid_disperse::VID_TARGET_TOTAL_STAKE},
39    x25519,
40};
41use humantime::format_duration;
42use indexmap::IndexMap;
43use itertools::Itertools;
44use num_traits::{FromPrimitive, Zero};
45use thiserror::Error;
46use tokio::{spawn, time::sleep};
47use tracing::Instrument;
48use vbs::version::Version;
49
50#[cfg(any(test, feature = "testing"))]
51use super::v0_3::DAMembers;
52use super::{
53    Header, L1Client, SeqTypes,
54    traits::{MembershipPersistence, StateCatchup},
55    v0_3::{
56        AuthenticatedValidator, ChainConfig, EventKey, Fetcher, MAX_VALIDATORS,
57        RegisteredValidator, StakeTableEvent, StakeTableUpdateTask,
58    },
59};
60use crate::{
61    traits::EventsPersistenceRead,
62    v0_1::L1Provider,
63    v0_3::{
64        BLOCKS_PER_YEAR, COMMISSION_BASIS_POINTS, EventSortingError, ExpectedStakeTableError,
65        FetchRewardError, INFLATION_RATE, MILLISECONDS_PER_YEAR, RewardAmount, StakeTableError,
66    },
67};
68
69pub type RegisteredValidatorMap = IndexMap<Address, RegisteredValidator<BLSPubKey>>;
70pub type AuthenticatedValidatorMap = IndexMap<Address, AuthenticatedValidator<BLSPubKey>>;
71
72pub fn to_registered_validator_map(
73    validators: &AuthenticatedValidatorMap,
74) -> RegisteredValidatorMap {
75    validators
76        .iter()
77        .map(|(addr, v)| (*addr, v.clone().into()))
78        .collect()
79}
80
81pub type StakeTableHash = Commitment<StakeTableState>;
82
83/// The result of applying a stake table event:
84/// - `Ok(Ok(()))`: success
85/// - `Ok(Err(...))`: expected error
86/// - `Err(...)`: serious error
87type ApplyEventResult<T> = Result<Result<T, ExpectedStakeTableError>, StakeTableError>;
88
89/// Format the alloy Log RPC type in a way to make it easy to find the event in an explorer.
90trait DisplayLog {
91    fn display(&self) -> String;
92}
93
94impl DisplayLog for Log {
95    fn display(&self) -> String {
96        // These values are all unlikely to be missing because we only create Log variables by
97        // fetching them from the RPC, so for simplicity we use defaults if the any of the values
98        // are missing.
99        let block = self.block_number.unwrap_or_default();
100        let index = self.log_index.unwrap_or_default();
101        let hash = self.transaction_hash.unwrap_or_default();
102        format!("Log(block={block},index={index},transaction_hash={hash})")
103    }
104}
105
106impl TryFrom<StakeTableV3Events> for StakeTableEvent {
107    type Error = anyhow::Error;
108
109    fn try_from(value: StakeTableV3Events) -> anyhow::Result<Self> {
110        match value {
111            StakeTableV3Events::ValidatorRegistered(v) => Ok(StakeTableEvent::Register(v)),
112            StakeTableV3Events::ValidatorRegisteredV2(v) => Ok(StakeTableEvent::RegisterV2(v)),
113            StakeTableV3Events::ValidatorRegisteredV3(v) => Ok(StakeTableEvent::RegisterV3(v)),
114            StakeTableV3Events::ValidatorExit(v) => Ok(StakeTableEvent::Deregister(v)),
115            StakeTableV3Events::ValidatorExitV2(v) => Ok(StakeTableEvent::DeregisterV2(v)),
116            StakeTableV3Events::Delegated(v) => Ok(StakeTableEvent::Delegate(v)),
117            StakeTableV3Events::Undelegated(v) => Ok(StakeTableEvent::Undelegate(v)),
118            StakeTableV3Events::UndelegatedV2(v) => Ok(StakeTableEvent::UndelegateV2(v)),
119            StakeTableV3Events::ConsensusKeysUpdated(v) => Ok(StakeTableEvent::KeyUpdate(v)),
120            StakeTableV3Events::ConsensusKeysUpdatedV2(v) => Ok(StakeTableEvent::KeyUpdateV2(v)),
121            StakeTableV3Events::CommissionUpdated(v) => Ok(StakeTableEvent::CommissionUpdate(v)),
122            StakeTableV3Events::X25519KeyUpdated(v) => Ok(StakeTableEvent::X25519KeyUpdate(v)),
123            StakeTableV3Events::P2pAddrUpdated(v) => Ok(StakeTableEvent::P2pAddrUpdate(v)),
124            StakeTableV3Events::ExitEscrowPeriodUpdated(v) => Err(anyhow::anyhow!(
125                "Unsupported StakeTableV3Events::ExitEscrowPeriodUpdated({v:?})"
126            )),
127            StakeTableV3Events::Initialized(v) => Err(anyhow::anyhow!(
128                "Unsupported StakeTableV3Events::Initialized({v:?})"
129            )),
130            StakeTableV3Events::MaxCommissionIncreaseUpdated(v) => Err(anyhow::anyhow!(
131                "Unsupported StakeTableV3Events::MaxCommissionIncreaseUpdated({v:?})"
132            )),
133            StakeTableV3Events::MinCommissionUpdateIntervalUpdated(v) => Err(anyhow::anyhow!(
134                "Unsupported StakeTableV3Events::MinCommissionUpdateIntervalUpdated({v:?})"
135            )),
136            StakeTableV3Events::OwnershipTransferred(v) => Err(anyhow::anyhow!(
137                "Unsupported StakeTableV3Events::OwnershipTransferred({v:?})"
138            )),
139            StakeTableV3Events::Paused(v) => Err(anyhow::anyhow!(
140                "Unsupported StakeTableV3Events::Paused({v:?})"
141            )),
142            StakeTableV3Events::RoleAdminChanged(v) => Err(anyhow::anyhow!(
143                "Unsupported StakeTableV3Events::RoleAdminChanged({v:?})"
144            )),
145            StakeTableV3Events::RoleGranted(v) => Err(anyhow::anyhow!(
146                "Unsupported StakeTableV3Events::RoleGranted({v:?})"
147            )),
148            StakeTableV3Events::RoleRevoked(v) => Err(anyhow::anyhow!(
149                "Unsupported StakeTableV3Events::RoleRevoked({v:?})"
150            )),
151            StakeTableV3Events::Unpaused(v) => Err(anyhow::anyhow!(
152                "Unsupported StakeTableV3Events::Unpaused({v:?})"
153            )),
154            StakeTableV3Events::Upgraded(v) => Err(anyhow::anyhow!(
155                "Unsupported StakeTableV3Events::Upgraded({v:?})"
156            )),
157            StakeTableV3Events::WithdrawalClaimed(v) => Err(anyhow::anyhow!(
158                "Unsupported StakeTableV3Events::WithdrawalClaimed({v:?})"
159            )),
160            StakeTableV3Events::ValidatorExitClaimed(v) => Err(anyhow::anyhow!(
161                "Unsupported StakeTableV3Events::ValidatorExitClaimed({v:?})"
162            )),
163            StakeTableV3Events::Withdrawal(v) => Err(anyhow::anyhow!(
164                "Unsupported StakeTableV3Events::Withdrawal({v:?})"
165            )),
166            StakeTableV3Events::MetadataUriUpdated(v) => Err(anyhow::anyhow!(
167                "Unsupported StakeTableV3Events::MetadataUriUpdated({v:?})"
168            )),
169            StakeTableV3Events::MinDelegateAmountUpdated(v) => Err(anyhow::anyhow!(
170                "Unsupported StakeTableV3Events::MinDelegateAmountUpdated({v:?})"
171            )),
172        }
173    }
174}
175
176fn sort_stake_table_events(
177    event_logs: Vec<(StakeTableV3Events, Log)>,
178) -> Result<Vec<(EventKey, StakeTableEvent)>, EventSortingError> {
179    let mut events: Vec<(EventKey, StakeTableEvent)> = Vec::new();
180
181    let key = |log: &Log| -> Result<EventKey, EventSortingError> {
182        let block_number = log
183            .block_number
184            .ok_or(EventSortingError::MissingBlockNumber)?;
185        let log_index = log.log_index.ok_or(EventSortingError::MissingLogIndex)?;
186        Ok((block_number, log_index))
187    };
188
189    for (e, log) in event_logs {
190        let k = key(&log)?;
191        let evt: StakeTableEvent = e
192            .try_into()
193            .map_err(|_| EventSortingError::InvalidStakeTableEvent)?;
194        events.push((k, evt));
195    }
196
197    events.sort_by_key(|(key, _)| *key);
198    Ok(events)
199}
200
201#[derive(Clone, Debug, Default, PartialEq)]
202pub struct StakeTableState {
203    validators: RegisteredValidatorMap,
204    validator_exits: HashSet<Address>,
205    used_bls_keys: HashSet<BLSPubKey>,
206    used_schnorr_keys: HashSet<SchnorrPubKey>,
207    used_x25519_keys: HashSet<x25519::PublicKey>,
208}
209
210impl Committable for StakeTableState {
211    fn commit(&self) -> committable::Commitment<Self> {
212        let mut builder = RawCommitmentBuilder::new(&Self::tag());
213
214        for (_, validator) in self.validators.iter().sorted_by_key(|(a, _)| *a) {
215            builder = builder.field("validator", validator.commit());
216        }
217
218        builder = builder.constant_str("used_bls_keys");
219        for key in self.used_bls_keys.iter().sorted() {
220            builder = builder.var_size_bytes(&key.to_bytes());
221        }
222
223        builder = builder.constant_str("used_schnorr_keys");
224        for key in self
225            .used_schnorr_keys
226            .iter()
227            .sorted_by(|a, b| a.to_affine().xy().cmp(&b.to_affine().xy()))
228        {
229            let mut schnorr_key_bytes = vec![];
230            key.serialize_with_mode(&mut schnorr_key_bytes, ark_serialize::Compress::Yes)
231                .unwrap();
232            builder = builder.var_size_bytes(&schnorr_key_bytes);
233        }
234
235        // Only include used_x25519_keys when non-empty to preserve the pre-fast-finality commitment
236        // value for stake tables that have no x25519 keys yet. Same pattern as x25519_key /
237        // p2p_addr on RegisteredValidator. Backward compatibility is cross-checked by
238        // REFERENCE_V4_HEADER_COMMITMENT in reference_tests.rs.
239        if !self.used_x25519_keys.is_empty() {
240            builder = builder.constant_str("used_x25519_keys");
241            for key in self.used_x25519_keys.iter().sorted() {
242                builder = builder.var_size_bytes(key.as_slice());
243            }
244        }
245
246        builder = builder.constant_str("validator_exits");
247
248        for key in self.validator_exits.iter().sorted() {
249            builder = builder.fixed_size_bytes(&key.into_array());
250        }
251
252        builder.finalize()
253    }
254
255    fn tag() -> String {
256        "STAKE_TABLE".to_string()
257    }
258}
259
260impl StakeTableState {
261    pub fn new(
262        validators: RegisteredValidatorMap,
263        validator_exits: HashSet<Address>,
264        used_bls_keys: HashSet<BLSPubKey>,
265        used_schnorr_keys: HashSet<SchnorrPubKey>,
266        used_x25519_keys: HashSet<x25519::PublicKey>,
267    ) -> Self {
268        Self {
269            validators,
270            validator_exits,
271            used_bls_keys,
272            used_schnorr_keys,
273            used_x25519_keys,
274        }
275    }
276
277    pub fn validators(&self) -> &RegisteredValidatorMap {
278        &self.validators
279    }
280
281    pub fn into_validators(self) -> RegisteredValidatorMap {
282        self.validators
283    }
284
285    pub fn used_bls_keys(&self) -> &HashSet<BLSPubKey> {
286        &self.used_bls_keys
287    }
288
289    pub fn used_schnorr_keys(&self) -> &HashSet<SchnorrPubKey> {
290        &self.used_schnorr_keys
291    }
292
293    pub fn used_x25519_keys(&self) -> &HashSet<x25519::PublicKey> {
294        &self.used_x25519_keys
295    }
296
297    pub fn validator_exits(&self) -> &HashSet<Address> {
298        &self.validator_exits
299    }
300
301    /// Applies a stake table event to this state.
302    ///
303    ///
304    /// This function MUST NOT modify `self` if the event is invalid. All validation
305    /// checks must be performed before any state modifications occur.
306    pub fn apply_event(&mut self, event: StakeTableEvent) -> ApplyEventResult<()> {
307        match event {
308            StakeTableEvent::Register(ValidatorRegistered {
309                account,
310                blsVk,
311                schnorrVk,
312                commission,
313            }) => {
314                let stake_table_key: BLSPubKey = blsVk.into();
315                let state_ver_key: SchnorrPubKey = schnorrVk.into();
316
317                if self.validator_exits.contains(&account) {
318                    return Err(StakeTableError::ValidatorAlreadyExited(account));
319                }
320
321                let entry = self.validators.entry(account);
322                if let indexmap::map::Entry::Occupied(_) = entry {
323                    return Err(StakeTableError::AlreadyRegistered(account));
324                }
325
326                // The stake table contract enforces that each bls key is only used once.
327                if self.used_bls_keys.contains(&stake_table_key) {
328                    return Err(StakeTableError::BlsKeyAlreadyUsed(
329                        stake_table_key.to_string(),
330                    ));
331                }
332
333                // The stake table v1 contract does *not* enforce that each schnorr key is only used once.
334                if self.used_schnorr_keys.contains(&state_ver_key) {
335                    return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
336                        state_ver_key.to_string(),
337                    )));
338                }
339
340                // All checks ok, applying changes
341                self.used_bls_keys.insert(stake_table_key);
342                self.used_schnorr_keys.insert(state_ver_key.clone());
343
344                entry.or_insert(RegisteredValidator {
345                    account,
346                    stake_table_key,
347                    state_ver_key,
348                    stake: U256::ZERO,
349                    commission,
350                    delegators: HashMap::new(),
351                    authenticated: true,
352                    x25519_key: None,
353                    p2p_addr: None,
354                });
355            },
356
357            StakeTableEvent::RegisterV2(ref reg) => {
358                let authenticated = reg.authenticate().is_ok();
359                if !authenticated {
360                    tracing::warn!(
361                        account = ?reg.account,
362                        "Validator registered with invalid signature"
363                    );
364                }
365
366                let ValidatorRegisteredV2 {
367                    account,
368                    blsVK,
369                    schnorrVK,
370                    commission,
371                    ..
372                } = reg;
373
374                let stake_table_key: BLSPubKey = (*blsVK).into();
375                let state_ver_key: SchnorrPubKey = (*schnorrVK).into();
376
377                // Reject if validator already exited
378                if self.validator_exits.contains(account) {
379                    return Err(StakeTableError::ValidatorAlreadyExited(*account));
380                }
381
382                let entry = self.validators.entry(*account);
383                if let indexmap::map::Entry::Occupied(_) = entry {
384                    return Err(StakeTableError::AlreadyRegistered(*account));
385                }
386
387                // The stake table v2 contract enforces that each bls key is only used once.
388                if self.used_bls_keys.contains(&stake_table_key) {
389                    return Err(StakeTableError::BlsKeyAlreadyUsed(
390                        stake_table_key.to_string(),
391                    ));
392                }
393
394                // The stake table v2 contract enforces schnorr key is only used once.
395                if self.used_schnorr_keys.contains(&state_ver_key) {
396                    return Err(StakeTableError::SchnorrKeyAlreadyUsed(
397                        state_ver_key.to_string(),
398                    ));
399                }
400
401                // All checks ok, applying changes
402                self.used_bls_keys.insert(stake_table_key);
403                self.used_schnorr_keys.insert(state_ver_key.clone());
404
405                entry.or_insert(RegisteredValidator {
406                    account: *account,
407                    stake_table_key,
408                    state_ver_key,
409                    stake: U256::ZERO,
410                    commission: *commission,
411                    delegators: HashMap::new(),
412                    authenticated,
413                    x25519_key: None,
414                    p2p_addr: None,
415                });
416            },
417
418            StakeTableEvent::Deregister(ValidatorExit { validator })
419            | StakeTableEvent::DeregisterV2(ValidatorExitV2 { validator, .. }) => {
420                if !self.validators.contains_key(&validator) {
421                    return Err(StakeTableError::ValidatorNotFound(validator));
422                }
423
424                // All checks ok, applying changes
425                self.validator_exits.insert(validator);
426                self.validators.shift_remove(&validator);
427            },
428
429            StakeTableEvent::Delegate(delegated) => {
430                let Delegated {
431                    delegator,
432                    validator,
433                    amount,
434                } = delegated;
435
436                // Check amount is not zero first
437                if amount.is_zero() {
438                    return Err(StakeTableError::ZeroDelegatorStake(delegator));
439                }
440
441                let val = self
442                    .validators
443                    .get_mut(&validator)
444                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
445
446                // All checks ok, applying changes
447                // This cannot overflow in practice
448                val.stake = val.stake.checked_add(amount).unwrap_or_else(|| {
449                    panic!(
450                        "validator stake overflow: validator={validator}, stake={}, \
451                         amount={amount}",
452                        val.stake
453                    )
454                });
455                // Insert the delegator with the given stake
456                // or increase the stake if already present
457                val.delegators
458                    .entry(delegator)
459                    .and_modify(|stake| {
460                        *stake = stake.checked_add(amount).unwrap_or_else(|| {
461                            panic!(
462                                "delegator stake overflow: delegator={delegator}, stake={stake}, \
463                                 amount={amount}"
464                            )
465                        });
466                    })
467                    .or_insert(amount);
468            },
469
470            StakeTableEvent::Undelegate(Undelegated {
471                delegator,
472                validator,
473                amount,
474            })
475            | StakeTableEvent::UndelegateV2(UndelegatedV2 {
476                delegator,
477                validator,
478                amount,
479                ..
480            }) => {
481                let val = self
482                    .validators
483                    .get_mut(&validator)
484                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
485
486                if val.stake < amount {
487                    tracing::warn!("validator_stake={}, amount={amount}", val.stake);
488                    return Err(StakeTableError::InsufficientStake);
489                }
490
491                let delegator_stake = val
492                    .delegators
493                    .get_mut(&delegator)
494                    .ok_or(StakeTableError::DelegatorNotFound(delegator))?;
495
496                if *delegator_stake < amount {
497                    tracing::warn!("delegator_stake={delegator_stake}, amount={amount}");
498                    return Err(StakeTableError::InsufficientStake);
499                }
500
501                // Can unwrap because check above passed
502                let new_delegator_stake = delegator_stake.checked_sub(amount).unwrap();
503
504                // Can unwrap because check above passed
505                // All checks ok, applying changes
506                val.stake = val.stake.checked_sub(amount).unwrap();
507
508                if new_delegator_stake.is_zero() {
509                    val.delegators.remove(&delegator);
510                } else {
511                    *delegator_stake = new_delegator_stake;
512                }
513            },
514
515            StakeTableEvent::KeyUpdate(update) => {
516                let ConsensusKeysUpdated {
517                    account,
518                    blsVK,
519                    schnorrVK,
520                } = update;
521
522                let stake_table_key: BLSPubKey = blsVK.into();
523                let state_ver_key: SchnorrPubKey = schnorrVK.into();
524
525                if !self.validators.contains_key(&account) {
526                    return Err(StakeTableError::ValidatorNotFound(account));
527                }
528
529                if self.used_bls_keys.contains(&stake_table_key) {
530                    return Err(StakeTableError::BlsKeyAlreadyUsed(
531                        stake_table_key.to_string(),
532                    ));
533                }
534
535                // The stake table v1 contract does *not* enforce that each schnorr key is only used once,
536                // therefore it's possible to have multiple validators with the same schnorr key.
537                if self.used_schnorr_keys.contains(&state_ver_key) {
538                    return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
539                        state_ver_key.to_string(),
540                    )));
541                }
542
543                // All checks ok, applying changes
544                self.used_bls_keys.insert(stake_table_key);
545                self.used_schnorr_keys.insert(state_ver_key.clone());
546                // Can unwrap because check above passed
547                let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
548                    panic!("validator {account} must exist after contains_key check")
549                });
550                validator.stake_table_key = stake_table_key;
551                validator.state_ver_key = state_ver_key;
552            },
553
554            StakeTableEvent::KeyUpdateV2(update) => {
555                // Signature authentication is performed right after fetching, if we get an
556                // unauthenticated event here, something went wrong, we abort early.
557                update
558                    .authenticate()
559                    .map_err(|e| StakeTableError::AuthenticationFailed(e.to_string()))?;
560
561                let ConsensusKeysUpdatedV2 {
562                    account,
563                    blsVK,
564                    schnorrVK,
565                    ..
566                } = update;
567
568                let stake_table_key: BLSPubKey = blsVK.into();
569                let state_ver_key: SchnorrPubKey = schnorrVK.into();
570
571                if !self.validators.contains_key(&account) {
572                    return Err(StakeTableError::ValidatorNotFound(account));
573                }
574
575                // The stake table contract enforces that each bls key is only used once.
576                if self.used_bls_keys.contains(&stake_table_key) {
577                    return Err(StakeTableError::BlsKeyAlreadyUsed(
578                        stake_table_key.to_string(),
579                    ));
580                }
581
582                // The stake table v2 contract enforces that each schnorr key is only used once
583                if self.used_schnorr_keys.contains(&state_ver_key) {
584                    return Err(StakeTableError::SchnorrKeyAlreadyUsed(
585                        state_ver_key.to_string(),
586                    ));
587                }
588
589                // All checks ok, applying changes
590                self.used_bls_keys.insert(stake_table_key);
591                self.used_schnorr_keys.insert(state_ver_key.clone());
592
593                // Can unwrap because check above passed
594                let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
595                    panic!("validator {account} must exist after contains_key check")
596                });
597                validator.stake_table_key = stake_table_key;
598                validator.state_ver_key = state_ver_key;
599            },
600
601            StakeTableEvent::CommissionUpdate(CommissionUpdated {
602                validator,
603                newCommission,
604                ..
605            }) => {
606                // NOTE: Commission update events are supported only in protocol
607                // version V4 and stake table contract V2.
608                if newCommission > COMMISSION_BASIS_POINTS {
609                    return Err(StakeTableError::InvalidCommission(validator, newCommission));
610                }
611
612                // NOTE: currently we are not enforcing changes to the
613                // commission increase rates and leave this enforcement to the
614                // stake table contract.
615                let val = self
616                    .validators
617                    .get_mut(&validator)
618                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
619                val.commission = newCommission;
620            },
621
622            StakeTableEvent::RegisterV3(ref reg) => {
623                let authenticated = reg.authenticate().is_ok();
624                if !authenticated {
625                    tracing::warn!(
626                        account = ?reg.account,
627                        "Validator registered with invalid signature"
628                    );
629                }
630
631                let ValidatorRegisteredV3 {
632                    account,
633                    blsVK,
634                    schnorrVK,
635                    commission,
636                    x25519Key,
637                    p2pAddr,
638                    ..
639                } = reg;
640
641                let stake_table_key: BLSPubKey = (*blsVK).into();
642                let state_ver_key: SchnorrPubKey = (*schnorrVK).into();
643
644                if x25519Key.0 == [0u8; 32] {
645                    return Err(StakeTableError::InvalidX25519Key("zero key".into()));
646                }
647                let x25519_key = x25519::PublicKey::try_from(x25519Key.0.as_slice())
648                    .map_err(|e| StakeTableError::InvalidX25519Key(format!("{e}")))?;
649
650                // Parse p2p addr
651                let p2p_addr = match p2pAddr.parse::<NetAddr>() {
652                    Ok(addr) => Some(addr),
653                    Err(e) => {
654                        if !p2pAddr.is_empty() {
655                            tracing::warn!(%e, account = ?account, "Failed to parse p2p addr");
656                        }
657                        None
658                    },
659                };
660
661                if self.validator_exits.contains(account) {
662                    return Err(StakeTableError::ValidatorAlreadyExited(*account));
663                }
664
665                let entry = self.validators.entry(*account);
666                if let indexmap::map::Entry::Occupied(_) = entry {
667                    return Err(StakeTableError::AlreadyRegistered(*account));
668                }
669
670                if self.used_bls_keys.contains(&stake_table_key) {
671                    return Err(StakeTableError::BlsKeyAlreadyUsed(
672                        stake_table_key.to_string(),
673                    ));
674                }
675
676                if self.used_schnorr_keys.contains(&state_ver_key) {
677                    return Err(StakeTableError::SchnorrKeyAlreadyUsed(
678                        state_ver_key.to_string(),
679                    ));
680                }
681
682                if self.used_x25519_keys.contains(&x25519_key) {
683                    return Err(StakeTableError::X25519KeyAlreadyUsed(
684                        x25519_key.to_string(),
685                    ));
686                }
687
688                // All checks ok, applying changes
689                self.used_bls_keys.insert(stake_table_key);
690                self.used_schnorr_keys.insert(state_ver_key.clone());
691                self.used_x25519_keys.insert(x25519_key);
692
693                entry.or_insert(RegisteredValidator {
694                    account: *account,
695                    stake_table_key,
696                    state_ver_key,
697                    stake: U256::ZERO,
698                    commission: *commission,
699                    delegators: HashMap::new(),
700                    authenticated,
701                    x25519_key: Some(x25519_key),
702                    p2p_addr,
703                });
704            },
705
706            StakeTableEvent::X25519KeyUpdate(X25519KeyUpdated {
707                validator,
708                x25519Key,
709            }) => {
710                let val = self
711                    .validators
712                    .get_mut(&validator)
713                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
714
715                let key = x25519::PublicKey::try_from(x25519Key.0.as_slice())
716                    .map_err(|e| StakeTableError::InvalidX25519Key(format!("{e}")))?;
717                if self.used_x25519_keys.contains(&key) {
718                    return Err(StakeTableError::X25519KeyAlreadyUsed(key.to_string()));
719                }
720                self.used_x25519_keys.insert(key);
721                val.x25519_key = Some(key);
722            },
723
724            StakeTableEvent::P2pAddrUpdate(P2pAddrUpdated {
725                validator,
726                ref p2pAddr,
727            }) => {
728                let val = self
729                    .validators
730                    .get_mut(&validator)
731                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
732
733                match p2pAddr.parse::<NetAddr>() {
734                    Ok(addr) => val.p2p_addr = Some(addr),
735                    Err(e) => {
736                        tracing::warn!(%e, validator = ?validator, "Failed to parse p2p addr");
737                        val.p2p_addr = None;
738                    },
739                }
740            },
741        }
742
743        Ok(Ok(()))
744    }
745}
746
747pub fn validators_from_l1_events<I: Iterator<Item = StakeTableEvent>>(
748    events: I,
749) -> Result<(RegisteredValidatorMap, StakeTableHash), StakeTableError> {
750    let mut state = StakeTableState::default();
751    for event in events {
752        match state.apply_event(event.clone()) {
753            Ok(Ok(())) => {
754                // Event successfully applied
755            },
756            Ok(Err(expected_err)) => {
757                // Expected error, dont change the state
758                tracing::warn!("Expected error while applying event {event:?}: {expected_err}");
759            },
760            Err(err) => {
761                tracing::error!("Fatal error in applying event {event:?}: {err}");
762                return Err(err);
763            },
764        }
765    }
766    let commit = state.commit();
767    Ok((state.into_validators(), commit))
768}
769
770/// Select active validators
771///
772/// Filters out unauthenticated validator candidates, those without stake, and selects
773/// the top [`MAX_VALIDATORS`] staked validators.
774/// Returns a new AuthenticatedValidatorMap containing only the selected validators.
775pub(crate) fn select_active_validator_set(
776    candidates: &RegisteredValidatorMap,
777    protocol_version: Version,
778) -> Result<AuthenticatedValidatorMap, StakeTableError> {
779    let total_candidates = candidates.len();
780
781    let valid_validators: AuthenticatedValidatorMap = candidates
782        .iter()
783        .filter_map(
784            |(address, validator)| match AuthenticatedValidator::try_from(validator) {
785                Err(e) => {
786                    tracing::debug!("{e}");
787                    None
788                },
789                Ok(cv) => {
790                    if cv.delegators.is_empty() {
791                        tracing::info!("Validator {address:?} does not have any delegator");
792                        return None;
793                    }
794                    if cv.stake.is_zero() {
795                        tracing::info!("Validator {address:?} does not have any stake");
796                        return None;
797                    }
798                    if !cv.is_eligible(protocol_version) {
799                        tracing::debug!(
800                            ?address,
801                            has_x25519 = cv.x25519_key.is_some(),
802                            has_p2p = cv.p2p_addr.is_some(),
803                            "Validator not eligible at protocol version {protocol_version}"
804                        );
805                        return None;
806                    }
807                    Some((*address, cv))
808                },
809            },
810        )
811        .collect();
812
813    tracing::debug!(
814        total_candidates,
815        filtered = valid_validators.len(),
816        "Filtered out invalid validators"
817    );
818
819    if valid_validators.is_empty() {
820        tracing::warn!("Validator selection failed: no validators passed minimum criteria");
821        return Err(StakeTableError::NoValidValidators);
822    }
823
824    let maximum_stake = valid_validators.values().map(|v| v.stake).max().unwrap();
825
826    let minimum_stake = maximum_stake
827        .checked_div(U256::from(VID_TARGET_TOTAL_STAKE))
828        .ok_or_else(|| {
829            tracing::error!("Overflow while calculating minimum stake threshold");
830            StakeTableError::MinimumStakeOverflow
831        })?;
832
833    let mut valid_stakers: Vec<_> = valid_validators
834        .iter()
835        .filter(|(_, v)| v.stake >= minimum_stake)
836        .map(|(addr, v)| (*addr, v.stake))
837        .collect();
838
839    tracing::info!(
840        count = valid_stakers.len(),
841        "Number of validators above minimum stake threshold"
842    );
843
844    // Sort by stake (descending order)
845    valid_stakers.sort_by_key(|(_, stake)| std::cmp::Reverse(*stake));
846
847    if valid_stakers.len() > MAX_VALIDATORS {
848        valid_stakers.truncate(MAX_VALIDATORS);
849    }
850
851    let selected_addresses: HashSet<_> = valid_stakers.iter().map(|(addr, _)| *addr).collect();
852    let selected_validators: AuthenticatedValidatorMap = valid_validators
853        .into_iter()
854        .filter(|(address, _)| selected_addresses.contains(address))
855        .collect();
856
857    tracing::info!(
858        final_count = selected_validators.len(),
859        "Selected active validator set"
860    );
861
862    Ok(selected_validators)
863}
864
865#[derive(Clone, Debug)]
866pub struct ValidatorSet {
867    pub(crate) all_validators: RegisteredValidatorMap,
868    pub(crate) active_validators: AuthenticatedValidatorMap,
869    pub(crate) stake_table_hash: Option<StakeTableHash>,
870    /// The protocol version at which `active_validators` was selected.
871    pub(crate) protocol_version: Version,
872}
873
874impl ValidatorSet {
875    /// Derive a validator set from a stake-table state at a given protocol version.
876    pub fn from_state(
877        state: &StakeTableState,
878        protocol_version: Version,
879    ) -> Result<Self, StakeTableError> {
880        let active_validators = select_active_validator_set(state.validators(), protocol_version)?;
881        Ok(Self {
882            all_validators: state.validators().clone(),
883            active_validators,
884            stake_table_hash: Some(state.commit()),
885            protocol_version,
886        })
887    }
888
889    /// Derive a validator set directly from L1 events.
890    pub fn from_l1_events<I: Iterator<Item = StakeTableEvent>>(
891        events: I,
892        protocol_version: Version,
893    ) -> Result<Self, StakeTableError> {
894        let (all_validators, stake_table_hash) = validators_from_l1_events(events)?;
895        let active_validators = select_active_validator_set(&all_validators, protocol_version)?;
896        Ok(Self {
897            all_validators,
898            active_validators,
899            stake_table_hash: Some(stake_table_hash),
900            protocol_version,
901        })
902    }
903
904    /// All registered validators known when this set was derived.
905    pub fn all_validators(&self) -> &RegisteredValidatorMap {
906        &self.all_validators
907    }
908
909    /// Validators selected to participate in consensus at `protocol_version`.
910    pub fn active_validators(&self) -> &AuthenticatedValidatorMap {
911        &self.active_validators
912    }
913
914    /// Commitment of the underlying stake-table state, if known.
915    pub fn stake_table_hash(&self) -> Option<StakeTableHash> {
916        self.stake_table_hash
917    }
918
919    /// Protocol version at which the active set was selected.
920    pub fn protocol_version(&self) -> Version {
921        self.protocol_version
922    }
923}
924
925impl std::fmt::Debug for StakeTableEvent {
926    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
927        match self {
928            StakeTableEvent::Register(event) => write!(f, "Register({:?})", event.account),
929            StakeTableEvent::RegisterV2(event) => write!(f, "RegisterV2({:?})", event.account),
930            StakeTableEvent::Deregister(event) => write!(f, "Deregister({:?})", event.validator),
931            StakeTableEvent::DeregisterV2(event) => {
932                write!(f, "DeregisterV2({:?})", event.validator)
933            },
934            StakeTableEvent::Delegate(event) => write!(f, "Delegate({:?})", event.delegator),
935            StakeTableEvent::Undelegate(event) => write!(f, "Undelegate({:?})", event.delegator),
936            StakeTableEvent::UndelegateV2(event) => {
937                write!(f, "UndelegateV2({:?})", event.delegator)
938            },
939            StakeTableEvent::KeyUpdate(event) => write!(f, "KeyUpdate({:?})", event.account),
940            StakeTableEvent::KeyUpdateV2(event) => write!(f, "KeyUpdateV2({:?})", event.account),
941            StakeTableEvent::CommissionUpdate(event) => {
942                write!(f, "CommissionUpdate({:?})", event.validator)
943            },
944            StakeTableEvent::RegisterV3(event) => {
945                write!(f, "RegisterV3({:?})", event.account)
946            },
947            StakeTableEvent::X25519KeyUpdate(event) => {
948                write!(f, "X25519KeyUpdate({:?})", event.validator)
949            },
950            StakeTableEvent::P2pAddrUpdate(event) => {
951                write!(f, "P2pAddrUpdate({:?})", event.validator)
952            },
953        }
954    }
955}
956
957impl Fetcher {
958    pub fn new(
959        peers: Arc<dyn StateCatchup>,
960        persistence: Arc<AsyncMutex<dyn MembershipPersistence>>,
961        l1_client: L1Client,
962        chain_config: ChainConfig,
963    ) -> Self {
964        Self {
965            peers,
966            persistence,
967            l1_client,
968            chain_config: Arc::new(AsyncMutex::new(chain_config)),
969            update_task: StakeTableUpdateTask(AsyncMutex::new(None)).into(),
970            initial_supply: Arc::new(AsyncRwLock::new(None)),
971        }
972    }
973
974    pub async fn spawn_update_loop(&self) {
975        let mut update_task = self.update_task.0.lock().await;
976        if update_task.is_none() {
977            *update_task = Some(spawn(self.update_loop()));
978        }
979    }
980
981    /// Periodically updates the stake table from the L1 contract.
982    /// This function polls the finalized block number from the L1 client at an interval
983    /// and fetches stake table from contract
984    /// and updates the persistence
985    fn update_loop(&self) -> impl Future<Output = ()> + use<> {
986        let span = tracing::warn_span!("Stake table update loop");
987        let self_clone = self.clone();
988        let state = self.l1_client.state.clone();
989        let l1_retry = self.l1_client.options().l1_retry_delay;
990        let update_delay = self.l1_client.options().stake_table_update_interval;
991        let chain_config = self.chain_config.clone();
992
993        async move {
994            // Get the stake table contract address from the chain config.
995            // This may not contain a stake table address if we are on a pre-epoch version.
996            // It keeps retrying until the chain config is upgraded
997            // after a successful upgrade to an epoch version.
998            let stake_contract_address = loop {
999                let contract = chain_config.lock().await.stake_table_contract;
1000                match contract {
1001                    Some(addr) => break addr,
1002                    None => {
1003                        tracing::debug!(
1004                            "Stake table contract address not found. Retrying in {l1_retry:?}...",
1005                        );
1006                    },
1007                }
1008                sleep(l1_retry).await;
1009            };
1010
1011            // Begin the main polling loop
1012            loop {
1013                let finalized_block = loop {
1014                    let last_finalized = state.lock().await.last_finalized;
1015                    if let Some(block) = last_finalized {
1016                        break block;
1017                    }
1018                    tracing::debug!("Finalized block not yet available. Retrying in {l1_retry:?}",);
1019                    sleep(l1_retry).await;
1020                };
1021
1022                tracing::debug!("Attempting to fetch stake table at L1 block {finalized_block:?}",);
1023
1024                loop {
1025                    match self_clone
1026                        .fetch_and_store_stake_table_events(stake_contract_address, finalized_block)
1027                        .await
1028                    {
1029                        Ok(events) => {
1030                            tracing::info!(
1031                                "Successfully fetched and stored stake table events at \
1032                                 block={finalized_block:?}"
1033                            );
1034                            tracing::debug!("events={events:?}");
1035                            break;
1036                        },
1037                        Err(e) => {
1038                            tracing::error!(
1039                                "Error fetching stake table at block {finalized_block:?}. err= \
1040                                 {e:#}",
1041                            );
1042                            sleep(l1_retry).await;
1043                        },
1044                    }
1045                }
1046
1047                tracing::debug!("Waiting {update_delay:?} before next stake table update...",);
1048                sleep(update_delay).await;
1049            }
1050        }
1051        .instrument(span)
1052    }
1053
1054    /// Get `StakeTable` at specific l1 block height.
1055    /// This function fetches and processes various events (ValidatorRegistered, ValidatorExit,
1056    /// Delegated, Undelegated, and ConsensusKeysUpdated) within the block range from the
1057    /// contract's initialization block to the provided `to_block` value.
1058    /// Events are fetched in chunks and retries are implemented for failed requests.
1059    /// Only new events fetched from L1 are stored in persistence.
1060    pub async fn fetch_and_store_stake_table_events(
1061        &self,
1062        contract: Address,
1063        to_block: u64,
1064    ) -> anyhow::Result<Vec<(EventKey, StakeTableEvent)>> {
1065        let (read_l1_offset, persistence_events) = {
1066            let persistence_lock = self.persistence.lock().await;
1067            persistence_lock.load_events(0, to_block).await?
1068        };
1069
1070        tracing::info!("loaded events from storage to_block={to_block:?}");
1071
1072        // No need to fetch from contract
1073        // if persistence returns all the events that we need
1074        if let Some(EventsPersistenceRead::Complete) = read_l1_offset {
1075            return Ok(persistence_events);
1076        }
1077
1078        let from_block = read_l1_offset
1079            .map(|read| match read {
1080                EventsPersistenceRead::UntilL1Block(block) => Ok(block + 1),
1081                EventsPersistenceRead::Complete => Err(anyhow::anyhow!(
1082                    "Unexpected state. offset is complete after returning early"
1083                )),
1084            })
1085            .transpose()?;
1086
1087        ensure!(
1088            Some(to_block) >= from_block,
1089            "to_block {to_block:?} is less than from_block {from_block:?}"
1090        );
1091
1092        tracing::info!(%to_block, from_block = ?from_block, "Fetching events from contract");
1093
1094        let contract_events = Self::fetch_events_from_contract(
1095            self.l1_client.clone(),
1096            contract,
1097            from_block,
1098            to_block,
1099        )
1100        .await?;
1101
1102        // Store only the new events fetched from L1 contract
1103        tracing::info!(
1104            "storing {} new events in storage to_block={to_block:?}",
1105            contract_events.len()
1106        );
1107        {
1108            let persistence_lock = self.persistence.lock().await;
1109            persistence_lock
1110                .store_events(to_block, contract_events.clone())
1111                .await
1112                .inspect_err(|e| tracing::error!("failed to store events. err={e}"))?;
1113        }
1114
1115        let mut events = match from_block {
1116            Some(_) => persistence_events
1117                .into_iter()
1118                .chain(contract_events)
1119                .collect(),
1120            None => contract_events,
1121        };
1122
1123        // There are no duplicates because the RPC returns all events,
1124        // which are stored directly in persistence as is.
1125        // However, this step is taken as a precaution.
1126        // The vector is already sorted above, so this should be fast.
1127        let len_before_dedup = events.len();
1128        events.dedup();
1129        let len_after_dedup = events.len();
1130        if len_before_dedup != len_after_dedup {
1131            tracing::warn!("Duplicate events found and removed. This should not normally happen.")
1132        }
1133
1134        Ok(events)
1135    }
1136
1137    /// Validate a stake table event.
1138    ///
1139    /// Returns:
1140    /// - `Ok(true)` if the event is valid and should be processed
1141    /// - `Ok(false)` if the event should be skipped (non-fatal error)
1142    /// - `Err(StakeTableError)` if a fatal error occurs
1143    fn validate_event(event: &StakeTableV3Events, log: &Log) -> Result<bool, StakeTableError> {
1144        match event {
1145            StakeTableV3Events::ConsensusKeysUpdatedV2(evt) => {
1146                if let Err(err) = evt.authenticate() {
1147                    tracing::warn!(
1148                        %err,
1149                        "Failed to authenticate ConsensusKeysUpdatedV2 event: {}",
1150                        log.display()
1151                    );
1152                    return Ok(false);
1153                }
1154            },
1155            StakeTableV3Events::CommissionUpdated(evt)
1156                if evt.newCommission > COMMISSION_BASIS_POINTS =>
1157            {
1158                return Err(StakeTableError::InvalidCommission(
1159                    evt.validator,
1160                    evt.newCommission,
1161                ));
1162            },
1163            _ => {},
1164        }
1165
1166        Ok(true)
1167    }
1168
1169    /// Break a block range into fixed-size chunks.
1170    fn block_range_chunks(
1171        from_block: u64,
1172        to_block: u64,
1173        chunk_size: u64,
1174    ) -> impl Iterator<Item = (u64, u64)> {
1175        let mut start = from_block;
1176        let end = to_block;
1177        std::iter::from_fn(move || {
1178            let chunk_end = min(start + chunk_size - 1, end);
1179            if chunk_end < start {
1180                return None;
1181            }
1182            let chunk = (start, chunk_end);
1183            start = chunk_end + 1;
1184            Some(chunk)
1185        })
1186    }
1187
1188    /// Fetch all stake table events from L1
1189    pub async fn fetch_events_from_contract(
1190        l1_client: L1Client,
1191        contract: Address,
1192        from_block: Option<u64>,
1193        to_block: u64,
1194    ) -> Result<Vec<(EventKey, StakeTableEvent)>, StakeTableError> {
1195        let stake_table_contract = StakeTableV3::new(contract, l1_client.provider.clone());
1196        let max_retry_duration = l1_client.options().l1_events_max_retry_duration;
1197        let retry_delay = l1_client.options().l1_retry_delay;
1198        // get the block number when the contract was initialized
1199        // to avoid fetching events from block number 0
1200        let from_block = match from_block {
1201            Some(block) => block,
1202            None => {
1203                let start = Instant::now();
1204                loop {
1205                    match stake_table_contract.initializedAtBlock().call().await {
1206                        Ok(init_block) => break init_block.to::<u64>(),
1207                        Err(err) => {
1208                            if start.elapsed() >= max_retry_duration {
1209                                panic!(
1210                                    "Failed to retrieve initial block after `{}`: {err}",
1211                                    format_duration(max_retry_duration)
1212                                );
1213                            }
1214                            tracing::warn!(%err, "Failed to retrieve initial block, retrying...");
1215                            sleep(retry_delay).await;
1216                        },
1217                    }
1218                }
1219            },
1220        };
1221
1222        // To avoid making large RPC calls, divide the range into smaller chunks.
1223        // chunk size is from env "ESPRESSO_L1_EVENTS_MAX_BLOCK_RANGE
1224        // default value  is `10000` if env variable is not set
1225        let chunk_size = l1_client.options().l1_events_max_block_range;
1226        let chunks = Self::block_range_chunks(from_block, to_block, chunk_size);
1227
1228        let mut events = vec![];
1229
1230        for (from, to) in chunks {
1231            let provider = l1_client.provider.clone();
1232
1233            tracing::debug!(from, to, "fetch all stake table events in range");
1234            // fetch events
1235            // retry if the call to the provider to fetch the events fails
1236            let logs: Vec<Log> = retry(
1237                retry_delay,
1238                max_retry_duration,
1239                "stake table events fetch",
1240                move || {
1241                    let provider = provider.clone();
1242
1243                    Box::pin(async move {
1244                        let filter = Filter::new()
1245                            .events([
1246                                ValidatorRegistered::SIGNATURE,
1247                                ValidatorRegisteredV2::SIGNATURE,
1248                                ValidatorRegisteredV3::SIGNATURE,
1249                                ValidatorExit::SIGNATURE,
1250                                ValidatorExitV2::SIGNATURE,
1251                                Delegated::SIGNATURE,
1252                                Undelegated::SIGNATURE,
1253                                UndelegatedV2::SIGNATURE,
1254                                ConsensusKeysUpdated::SIGNATURE,
1255                                ConsensusKeysUpdatedV2::SIGNATURE,
1256                                CommissionUpdated::SIGNATURE,
1257                                X25519KeyUpdated::SIGNATURE,
1258                                P2pAddrUpdated::SIGNATURE,
1259                            ])
1260                            .address(contract)
1261                            .from_block(from)
1262                            .to_block(to);
1263                        provider.get_logs(&filter).await
1264                    })
1265                },
1266            )
1267            .await;
1268
1269            let chunk_events = logs
1270                .into_iter()
1271                .filter_map(|log| {
1272                    let event =
1273                        StakeTableV3Events::decode_raw_log(log.topics(), &log.data().data).ok()?;
1274                    match Self::validate_event(&event, &log) {
1275                        Ok(true) => Some(Ok((event, log))),
1276                        Ok(false) => None,
1277                        Err(e) => Some(Err(e)),
1278                    }
1279                })
1280                .collect::<Result<Vec<_>, _>>()?;
1281
1282            events.extend(chunk_events);
1283        }
1284
1285        sort_stake_table_events(events).map_err(Into::into)
1286    }
1287
1288    // Only used by staking CLI which doesn't have persistence
1289    pub async fn fetch_all_validators_from_contract(
1290        l1_client: L1Client,
1291        contract: Address,
1292        to_block: u64,
1293    ) -> anyhow::Result<(RegisteredValidatorMap, StakeTableHash)> {
1294        let events = Self::fetch_events_from_contract(l1_client, contract, None, to_block).await?;
1295
1296        // Process the sorted events and return the resulting stake table.
1297        validators_from_l1_events(events.into_iter().map(|(_, e)| e))
1298            .context("failed to construct validators set from l1 events")
1299    }
1300
1301    /// Calculates the fixed block reward based on the token's initial supply.
1302    /// - The initial supply is fetched from the token contract
1303    /// - If the supply is not present, it invokes `fetch_and_update_initial_supply` to retrieve it.
1304    pub async fn fetch_fixed_block_reward(&self) -> Result<RewardAmount, FetchRewardError> {
1305        // `fetch_and_update_initial_supply` needs a write lock, create temporary to drop lock
1306        let initial_supply = *self.initial_supply.read().await;
1307        let initial_supply = match initial_supply {
1308            Some(supply) => supply,
1309            None => self.fetch_and_update_initial_supply().await?,
1310        };
1311
1312        let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
1313            .checked_div(U256::from(COMMISSION_BASIS_POINTS))
1314            .ok_or(FetchRewardError::DivisionByZero(
1315                "COMMISSION_BASIS_POINTS is zero",
1316            ))?;
1317
1318        Ok(RewardAmount(reward))
1319    }
1320
1321    /// Fetches and updates the initial token supply.
1322    ///
1323    /// - Locates the `Initialized` event of the token contract (emitted only once).
1324    /// - Queries `Transfer` events in the same block, matching by transaction hash and
1325    ///   `from == address(0)` to find the initial mint.
1326    /// - If either step fails, the function aborts to prevent incorrect reward calculations.
1327    ///
1328    /// This avoids fetching transaction receipts, which may be unavailable on pruned L1 nodes.
1329    ///
1330    /// The ESP token contract itself does not expose the initialization block
1331    /// but the stake table contract does.
1332    /// The stake table contract is deployed after the token contract as it holds the token
1333    /// contract address. We use the stake table contract initialization block as a safe upper bound
1334    /// when scanning backwards for the token contract initialization event.
1335    pub async fn fetch_and_update_initial_supply(&self) -> Result<U256, FetchRewardError> {
1336        tracing::info!("Fetching token initial supply");
1337        let chain_config = *self.chain_config.lock().await;
1338
1339        let stake_table_contract = chain_config
1340            .stake_table_contract
1341            .ok_or(FetchRewardError::MissingStakeTableContract)?;
1342
1343        let provider = self.l1_client.provider.clone();
1344        let stake_table = StakeTableV3::new(stake_table_contract, provider.clone());
1345
1346        // Get the block number where the stake table was initialized
1347        // Stake table contract has the token contract address
1348        // so the token contract is deployed before the stake table contract
1349        let stake_table_init_block = stake_table
1350            .initializedAtBlock()
1351            .block(BlockId::finalized())
1352            .call()
1353            .await
1354            .map_err(FetchRewardError::ContractCall)?
1355            .to::<u64>();
1356
1357        tracing::info!("stake table init block ={stake_table_init_block}");
1358
1359        let token_address = stake_table
1360            .token()
1361            .block(BlockId::finalized())
1362            .call()
1363            .await
1364            .map_err(FetchRewardError::TokenAddressFetch)?;
1365
1366        let token = EspToken::new(token_address, provider.clone());
1367
1368        // Fetch the `Initialized` event (emitted once during token contract init).
1369        // Falls back to scanning over a fixed block range if the full-range query fails.
1370        let init_logs = token
1371            .Initialized_filter()
1372            .from_block(0u64)
1373            .to_block(BlockNumberOrTag::Finalized)
1374            .query()
1375            .await;
1376
1377        let init_log = match init_logs {
1378            Ok(init_logs) => {
1379                if init_logs.is_empty() {
1380                    tracing::error!(
1381                        "Token Initialized event logs are empty. This should never happen"
1382                    );
1383                    return Err(FetchRewardError::MissingInitializedEvent);
1384                }
1385
1386                let (_, init_log) = init_logs[0].clone();
1387
1388                tracing::debug!(tx_hash = ?init_log.transaction_hash, "Found token `Initialized` event");
1389                init_log
1390            },
1391            Err(err) => {
1392                tracing::warn!(
1393                    "RPC returned error {err:?}. will fallback to scanning over fixed block range"
1394                );
1395                self.scan_token_contract_initialized_event_log(
1396                    stake_table_init_block,
1397                    token.clone(),
1398                )
1399                .await?
1400            },
1401        };
1402
1403        let init_block = init_log
1404            .block_number
1405            .ok_or(FetchRewardError::MissingBlockNumber)?;
1406
1407        let init_tx_hash =
1408            init_log
1409                .transaction_hash
1410                .ok_or_else(|| FetchRewardError::MissingTransactionHash {
1411                    init_log: init_log.clone().into(),
1412                })?;
1413
1414        // Query Transfer events in the initialization block instead of fetching
1415        // the transaction receipt, which pruned L1 nodes may not have.
1416        // Match by transaction hash to scope to the exact initialization tx.
1417        let transfer_logs = token
1418            .Transfer_filter()
1419            .from_block(init_block)
1420            .to_block(init_block)
1421            .query()
1422            .await
1423            .map_err(FetchRewardError::TransferEventQuery)?;
1424
1425        let (mint_transfer, _) = transfer_logs
1426            .iter()
1427            .find(|(transfer, log)| {
1428                log.transaction_hash == Some(init_tx_hash) && transfer.from == Address::ZERO
1429            })
1430            .ok_or(FetchRewardError::MissingTransferEvent)?;
1431
1432        tracing::debug!("mint transfer event ={mint_transfer:?}");
1433
1434        let initial_supply = mint_transfer.value;
1435
1436        tracing::info!("Initial token amount: {} ESP", format_ether(initial_supply));
1437
1438        let mut writer = self.initial_supply.write().await;
1439        *writer = Some(initial_supply);
1440
1441        Ok(initial_supply)
1442    }
1443
1444    /// Scans backwards in fixed-size block ranges to locate the `Initialized` event of the token contract.
1445    ///
1446    /// This is a fallback method used when querying the full block range for the `Initialized` event fails
1447    ///
1448    /// Starting from the stake table contract’s initialization block (which comes after the token contract
1449    /// is deployed), it scans in chunks (defined by `l1_events_max_block_range`) until it finds the event
1450    /// or until a maximum number of blocks (`MAX_BLOCKS_SCANNED`) is reached.
1451    pub async fn scan_token_contract_initialized_event_log(
1452        &self,
1453        stake_table_init_block: u64,
1454        token: EspTokenInstance<L1Provider>,
1455    ) -> Result<Log, FetchRewardError> {
1456        let max_events_range = self.l1_client.options().l1_events_max_block_range;
1457        const MAX_BLOCKS_SCANNED: u64 = 200_000;
1458        let mut total_scanned = 0;
1459
1460        let mut from_block = stake_table_init_block.saturating_sub(max_events_range);
1461        let mut to_block = stake_table_init_block;
1462
1463        loop {
1464            if total_scanned >= MAX_BLOCKS_SCANNED {
1465                tracing::error!(
1466                    total_scanned,
1467                    "Exceeded maximum scan range while searching for token Initialized event"
1468                );
1469                return Err(FetchRewardError::ExceededMaxScanRange(MAX_BLOCKS_SCANNED));
1470            }
1471
1472            let init_logs = token
1473                .Initialized_filter()
1474                .from_block(from_block)
1475                .to_block(to_block)
1476                .query()
1477                .await
1478                .map_err(FetchRewardError::ScanQueryFailed)?;
1479
1480            if !init_logs.is_empty() {
1481                let (_, init_log) = init_logs[0].clone();
1482                tracing::info!(
1483                    from_block,
1484                    tx_hash = ?init_log.transaction_hash,
1485                    "Found token Initialized event during scan"
1486                );
1487                return Ok(init_log);
1488            }
1489
1490            total_scanned += max_events_range;
1491            from_block = from_block.saturating_sub(max_events_range);
1492            to_block = to_block.saturating_sub(max_events_range);
1493        }
1494    }
1495
1496    pub async fn update_chain_config(&self, header: &Header) -> anyhow::Result<()> {
1497        let chain_config = self.get_chain_config(header).await?;
1498        // update chain config
1499        *self.chain_config.lock().await = chain_config;
1500
1501        Ok(())
1502    }
1503
1504    pub async fn fetch(&self, epoch: EpochNumber, header: &Header) -> anyhow::Result<ValidatorSet> {
1505        let chain_config = *self.chain_config.lock().await;
1506        let Some(address) = chain_config.stake_table_contract else {
1507            bail!("No stake table contract address found in Chain config");
1508        };
1509
1510        let Some(l1_finalized_block_info) = header.l1_finalized() else {
1511            bail!(
1512                "The epoch root for epoch {epoch} is missing the L1 finalized block info. This is \
1513                 a fatal error. Consensus is blocked and will not recover."
1514            );
1515        };
1516
1517        let events = match self
1518            .fetch_and_store_stake_table_events(address, l1_finalized_block_info.number())
1519            .await
1520            .map_err(GetStakeTablesError::L1ClientFetchError)
1521        {
1522            Ok(events) => events,
1523            Err(e) => {
1524                bail!("failed to fetch stake table events {e:?}");
1525            },
1526        };
1527
1528        // Selection runs at the epoch root header's protocol version: that header's
1529        // `next_stake_table_hash` was computed under the active-set rules in effect at the root,
1530        // so events spanning an upgrade are intentionally evaluated under the root's rules rather
1531        // than each event's wall-clock version.
1532        match ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), header.version()) {
1533            Ok(res) => Ok(res),
1534            Err(e) => {
1535                bail!("failed to construct stake table {e:?}");
1536            },
1537        }
1538    }
1539
1540    /// Retrieve and verify `ChainConfig`
1541    // TODO move to appropriate object (Header?)
1542    pub(crate) async fn get_chain_config(&self, header: &Header) -> anyhow::Result<ChainConfig> {
1543        let chain_config = self.chain_config.lock().await;
1544        let peers = self.peers.clone();
1545        let header_cf = header.chain_config();
1546        if chain_config.commit() == header_cf.commit() {
1547            return Ok(*chain_config);
1548        }
1549
1550        let cf = match header_cf.resolve() {
1551            Some(cf) => cf,
1552            None => peers
1553                .fetch_chain_config(header_cf.commit())
1554                .await
1555                .inspect_err(|err| {
1556                    tracing::error!("failed to get chain_config from peers. err: {err:?}");
1557                })?,
1558        };
1559
1560        Ok(cf)
1561    }
1562
1563    #[cfg(any(test, feature = "testing"))]
1564    pub fn mock() -> Self {
1565        use crate::{mock, v0_1::NoStorage};
1566        let chain_config = ChainConfig::default();
1567        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
1568            .expect("Failed to create L1 client");
1569
1570        let peers = Arc::new(mock::MockStateCatchup::default());
1571        let persistence = NoStorage;
1572
1573        Self::new(
1574            peers,
1575            Arc::new(AsyncMutex::new(persistence)),
1576            l1,
1577            chain_config,
1578        )
1579    }
1580}
1581
1582async fn retry<F, T, E>(
1583    retry_delay: Duration,
1584    max_duration: Duration,
1585    operation_name: &str,
1586    mut operation: F,
1587) -> T
1588where
1589    F: FnMut() -> BoxFuture<'static, Result<T, E>>,
1590    E: std::fmt::Display,
1591{
1592    let start = Instant::now();
1593    loop {
1594        match operation().await {
1595            Ok(result) => return result,
1596            Err(err) => {
1597                if start.elapsed() >= max_duration {
1598                    panic!(
1599                        r#"
1600                    Failed to complete operation `{operation_name}` after `{}`.
1601                    error: {err}
1602
1603
1604                    This might be caused by:
1605                    - The current block range being too large for your RPC provider.
1606                    - The event query returning more data than your RPC allows as
1607                      some RPC providers limit the number of events returned.
1608                    - RPC provider outage
1609
1610                    Suggested solution:
1611                    - Reduce the value of the environment variable
1612                      `ESPRESSO_L1_EVENTS_MAX_BLOCK_RANGE` to query smaller ranges.
1613                    - Add multiple RPC providers
1614                    - Use a different RPC provider with higher rate limits."#,
1615                        format_duration(max_duration)
1616                    );
1617                }
1618                tracing::warn!(%err, "Retrying `{operation_name}` after error");
1619                sleep(retry_delay).await;
1620            },
1621        }
1622    }
1623}
1624
1625/// Calculates the stake ratio `p` and reward rate `R(p)`.
1626///
1627/// The reward rate `R(p)` is defined as:
1628///
1629///     R(p) = {
1630///         0.03 / sqrt(2 * 0.01),         if 0 <= p <= 0.01
1631///         0.03 / sqrt(2 * p),            if 0.01 < p <= 1
1632///     }
1633///
1634pub fn calculate_proportion_staked_and_reward_rate(
1635    total_stake: &BigDecimal,
1636    total_supply: &BigDecimal,
1637) -> anyhow::Result<(BigDecimal, BigDecimal)> {
1638    if total_supply.is_zero() {
1639        return Err(anyhow::anyhow!("Total supply cannot be zero"));
1640    }
1641
1642    let proportion_staked = total_stake / total_supply;
1643
1644    if proportion_staked < BigDecimal::zero() || proportion_staked > BigDecimal::one() {
1645        return Err(anyhow::anyhow!("Stake ratio p must be in the range [0, 1]"));
1646    }
1647
1648    let two = BigDecimal::from_u32(2).unwrap();
1649    let min_stake_ratio = BigDecimal::from_str("0.01")?;
1650    let numerator = BigDecimal::from_str("0.03")?;
1651
1652    let denominator = (&two * (&proportion_staked).max(&min_stake_ratio))
1653        .sqrt()
1654        .context("Failed to compute sqrt in R(p)")?;
1655
1656    let reward_rate = numerator / denominator;
1657
1658    tracing::debug!("rp={reward_rate}");
1659
1660    Ok((proportion_staked, reward_rate))
1661}
1662
1663pub(crate) fn compute_block_reward(
1664    epoch: EpochNumber,
1665    total_supply: U256,
1666    total_stake: U256,
1667    avg_block_time_ms: u64,
1668) -> anyhow::Result<RewardAmount> {
1669    // Convert to BigDecimal for precision
1670    let total_stake_bd = BigDecimal::from_str(&total_stake.to_string())?;
1671    let total_supply_bd = BigDecimal::from_str(&(total_supply.to_string()))?;
1672
1673    tracing::debug!(?epoch, "total_stake={total_stake}");
1674    tracing::debug!(?epoch, "total_supply_bd={total_supply_bd}");
1675
1676    let (proportion, reward_rate) =
1677        calculate_proportion_staked_and_reward_rate(&total_stake_bd, &total_supply_bd)?;
1678    let inflation_rate = proportion * reward_rate;
1679
1680    tracing::debug!(?epoch, "inflation_rate={inflation_rate:?}");
1681
1682    let blocks_per_year = MILLISECONDS_PER_YEAR
1683        .checked_div(avg_block_time_ms.into())
1684        .context("avg_block_time_ms is zero")?;
1685
1686    tracing::debug!(?epoch, "blocks_per_year={blocks_per_year:?}");
1687
1688    ensure!(!blocks_per_year.is_zero(), "blocks per year is zero");
1689    let block_reward = (total_supply_bd * inflation_rate) / blocks_per_year;
1690
1691    let block_reward_u256 = U256::from_str(&block_reward.round(0).to_string())?;
1692
1693    Ok(block_reward_u256.into())
1694}
1695
1696#[derive(Error, Debug)]
1697/// Error representing fail cases for retrieving the stake table.
1698enum GetStakeTablesError {
1699    #[error("Error fetching from L1: {0}")]
1700    L1ClientFetchError(anyhow::Error),
1701}
1702
1703#[cfg(any(test, feature = "testing"))]
1704impl super::v0_3::StakeTable {
1705    /// Generate a `StakeTable` with `n` members.
1706    pub fn mock(n: u64) -> Self {
1707        [..n]
1708            .iter()
1709            .map(|_| PeerConfig::test_default())
1710            .collect::<Vec<PeerConfig<SeqTypes>>>()
1711            .into()
1712    }
1713}
1714
1715#[cfg(any(test, feature = "testing"))]
1716impl DAMembers {
1717    /// Generate a `DaMembers` (alias committee) with `n` members.
1718    pub fn mock(n: u64) -> Self {
1719        [..n]
1720            .iter()
1721            .map(|_| PeerConfig::test_default())
1722            .collect::<Vec<PeerConfig<SeqTypes>>>()
1723            .into()
1724    }
1725}
1726
1727#[cfg(any(test, feature = "testing"))]
1728pub mod testing {
1729    use alloy::primitives::Bytes;
1730    use hotshot_contract_adapter::{
1731        sol_types::{EdOnBN254PointSol, G1PointSol, G2PointSol},
1732        stake_table::{StateSignatureSol, sign_address_bls, sign_address_schnorr},
1733    };
1734    use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
1735    use rand::{Rng as _, RngCore as _};
1736
1737    use super::*;
1738
1739    // TODO: current tests are just sanity checks, we need more.
1740
1741    #[derive(Debug, Clone)]
1742    pub struct TestValidator {
1743        pub account: Address,
1744        pub bls_vk: G2PointSol,
1745        pub schnorr_vk: EdOnBN254PointSol,
1746        pub commission: u16,
1747        pub bls_sig: G1PointSol,
1748        pub schnorr_sig: Bytes,
1749        pub x25519_key: [u8; 32],
1750        pub p2p_addr: String,
1751    }
1752
1753    impl TestValidator {
1754        pub fn random() -> Self {
1755            let account = Address::random();
1756            let commission = rand::thread_rng().gen_range(0..10000);
1757            Self::random_update_keys(account, commission)
1758        }
1759
1760        pub fn randomize_keys(&self) -> Self {
1761            Self::random_update_keys(self.account, self.commission)
1762        }
1763
1764        pub fn random_update_keys(account: Address, commission: u16) -> Self {
1765            let mut rng = &mut rand::thread_rng();
1766            let mut seed = [0u8; 32];
1767            rng.fill_bytes(&mut seed);
1768            let bls_key_pair = BLSKeyPair::generate(&mut rng);
1769            let bls_sig = sign_address_bls(&bls_key_pair, account);
1770            let schnorr_key_pair = StateKeyPair::generate_from_seed_indexed(seed, 0);
1771            let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, account);
1772            let mut x25519_key = [0u8; 32];
1773            rng.fill_bytes(&mut x25519_key);
1774            Self {
1775                account,
1776                bls_vk: bls_key_pair.ver_key().to_affine().into(),
1777                schnorr_vk: schnorr_key_pair.ver_key().to_affine().into(),
1778                commission,
1779                bls_sig: bls_sig.into(),
1780                schnorr_sig: StateSignatureSol::from(schnorr_sig).into(),
1781                x25519_key,
1782                p2p_addr: "127.0.0.1:9000".to_string(),
1783            }
1784        }
1785
1786        pub fn with_x25519_key(mut self, x25519_key: [u8; 32]) -> Self {
1787            self.x25519_key = x25519_key;
1788            self
1789        }
1790
1791        pub fn with_p2p_addr(mut self, p2p_addr: impl Into<String>) -> Self {
1792            self.p2p_addr = p2p_addr.into();
1793            self
1794        }
1795
1796        pub fn x25519_update(&self, x25519_key: [u8; 32]) -> StakeTableEvent {
1797            StakeTableEvent::X25519KeyUpdate(X25519KeyUpdated {
1798                validator: self.account,
1799                x25519Key: alloy::primitives::FixedBytes(x25519_key),
1800            })
1801        }
1802
1803        pub fn p2p_update(&self, p2p_addr: impl Into<String>) -> StakeTableEvent {
1804            StakeTableEvent::P2pAddrUpdate(P2pAddrUpdated {
1805                validator: self.account,
1806                p2pAddr: p2p_addr.into(),
1807            })
1808        }
1809    }
1810
1811    impl From<&TestValidator> for ValidatorRegistered {
1812        fn from(value: &TestValidator) -> Self {
1813            Self {
1814                account: value.account,
1815                blsVk: value.bls_vk,
1816                schnorrVk: value.schnorr_vk,
1817                commission: value.commission,
1818            }
1819        }
1820    }
1821
1822    impl From<&TestValidator> for ValidatorRegisteredV2 {
1823        fn from(value: &TestValidator) -> Self {
1824            Self {
1825                account: value.account,
1826                blsVK: value.bls_vk,
1827                schnorrVK: value.schnorr_vk,
1828                commission: value.commission,
1829                blsSig: value.bls_sig.into(),
1830                schnorrSig: value.schnorr_sig.clone(),
1831                metadataUri: "dummy-meta".to_string(),
1832            }
1833        }
1834    }
1835
1836    impl From<&TestValidator> for ValidatorRegisteredV3 {
1837        fn from(value: &TestValidator) -> Self {
1838            Self {
1839                account: value.account,
1840                blsVK: value.bls_vk,
1841                schnorrVK: value.schnorr_vk,
1842                commission: value.commission,
1843                blsSig: value.bls_sig.into(),
1844                schnorrSig: value.schnorr_sig.clone(),
1845                metadataUri: "dummy-meta".to_string(),
1846                x25519Key: alloy::primitives::FixedBytes(value.x25519_key),
1847                p2pAddr: value.p2p_addr.clone(),
1848            }
1849        }
1850    }
1851
1852    impl From<&TestValidator> for ConsensusKeysUpdated {
1853        fn from(value: &TestValidator) -> Self {
1854            Self {
1855                account: value.account,
1856                blsVK: value.bls_vk,
1857                schnorrVK: value.schnorr_vk,
1858            }
1859        }
1860    }
1861
1862    impl From<&TestValidator> for ConsensusKeysUpdatedV2 {
1863        fn from(value: &TestValidator) -> Self {
1864            Self {
1865                account: value.account,
1866                blsVK: value.bls_vk,
1867                schnorrVK: value.schnorr_vk,
1868                blsSig: value.bls_sig.into(),
1869                schnorrSig: value.schnorr_sig.clone(),
1870            }
1871        }
1872    }
1873
1874    impl From<&TestValidator> for ValidatorExit {
1875        fn from(value: &TestValidator) -> Self {
1876            Self {
1877                validator: value.account,
1878            }
1879        }
1880    }
1881
1882    impl RegisteredValidator<BLSPubKey> {
1883        pub fn mock() -> RegisteredValidator<BLSPubKey> {
1884            let val = TestValidator::random();
1885            let rng = &mut rand::thread_rng();
1886            let mut seed = [1u8; 32];
1887            rng.fill_bytes(&mut seed);
1888            let mut validator_stake = alloy::primitives::U256::from(0);
1889            let mut delegators = HashMap::new();
1890            for _i in 0..=5000 {
1891                let stake: u64 = rng.gen_range(0..10000);
1892                delegators.insert(Address::random(), alloy::primitives::U256::from(stake));
1893                validator_stake += alloy::primitives::U256::from(stake);
1894            }
1895
1896            let stake_table_key = val.bls_vk.into();
1897            let state_ver_key = val.schnorr_vk.into();
1898
1899            RegisteredValidator {
1900                account: val.account,
1901                stake_table_key,
1902                state_ver_key,
1903                stake: validator_stake,
1904                commission: val.commission,
1905                delegators,
1906                authenticated: true,
1907                x25519_key: None,
1908                p2p_addr: None,
1909            }
1910        }
1911    }
1912
1913    impl AuthenticatedValidator<BLSPubKey> {
1914        pub fn mock() -> AuthenticatedValidator<BLSPubKey> {
1915            RegisteredValidator::mock()
1916                .try_into()
1917                .expect("mock validator is always authenticated")
1918        }
1919
1920        pub fn mock_with_commission(commission: u16) -> AuthenticatedValidator<BLSPubKey> {
1921            let mut inner = RegisteredValidator::mock();
1922            inner.commission = commission;
1923            inner
1924                .try_into()
1925                .expect("mock validator is always authenticated")
1926        }
1927    }
1928}
1929
1930#[cfg(test)]
1931mod tests {
1932    use alloy::{primitives::Address, rpc::types::Log};
1933    use hotshot_contract_adapter::stake_table::{StakeTableContractVersion, sign_address_bls};
1934    use hotshot_types::signature_key::BLSKeyPair;
1935    use pretty_assertions::assert_matches;
1936    use rstest::rstest;
1937    use versions::{
1938        DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION,
1939    };
1940
1941    use super::*;
1942    use crate::{L1ClientOptions, v0::impls::testing::*};
1943
1944    #[test_log::test]
1945    fn test_from_l1_events() -> anyhow::Result<()> {
1946        // Build a stake table with one DA node and one consensus node.
1947        let val_1 = TestValidator::random();
1948        let val_1_new_keys = val_1.randomize_keys();
1949        let val_2 = TestValidator::random();
1950        let val_2_new_keys = val_2.randomize_keys();
1951        let delegator = Address::random();
1952        let mut events: Vec<StakeTableEvent> = [
1953            ValidatorRegistered::from(&val_1).into(),
1954            ValidatorRegisteredV2::from(&val_2).into(),
1955            Delegated {
1956                delegator,
1957                validator: val_1.account,
1958                amount: U256::from(10),
1959            }
1960            .into(),
1961            ConsensusKeysUpdated::from(&val_1_new_keys).into(),
1962            ConsensusKeysUpdatedV2::from(&val_2_new_keys).into(),
1963            Undelegated {
1964                delegator,
1965                validator: val_1.account,
1966                amount: U256::from(7),
1967            }
1968            .into(),
1969            // delegate to the same validator again
1970            Delegated {
1971                delegator,
1972                validator: val_1.account,
1973                amount: U256::from(5),
1974            }
1975            .into(),
1976            // delegate to the second validator
1977            Delegated {
1978                delegator: Address::random(),
1979                validator: val_2.account,
1980                amount: U256::from(3),
1981            }
1982            .into(),
1983        ]
1984        .to_vec();
1985
1986        let validators_set = ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION)?;
1987        let st = validators_set.active_validators;
1988        let st_val_1 = st.get(&val_1.account).unwrap();
1989        // final staked amount should be 10 (delegated) - 7 (undelegated) + 5 (Delegated)
1990        assert_eq!(st_val_1.stake, U256::from(8));
1991        assert_eq!(st_val_1.commission, val_1.commission);
1992        assert_eq!(st_val_1.delegators.len(), 1);
1993        // final delegated amount should be 10 (delegated) - 7 (undelegated) + 5 (Delegated)
1994        assert_eq!(*st_val_1.delegators.get(&delegator).unwrap(), U256::from(8));
1995
1996        let st_val_2 = st.get(&val_2.account).unwrap();
1997        assert_eq!(st_val_2.stake, U256::from(3));
1998        assert_eq!(st_val_2.commission, val_2.commission);
1999        assert_eq!(st_val_2.delegators.len(), 1);
2000
2001        events.push(ValidatorExit::from(&val_1).into());
2002
2003        let validator_set = ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION)?;
2004        let st = validator_set.active_validators;
2005        // The first validator should have been removed
2006        assert_eq!(st.get(&val_1.account), None);
2007
2008        // The second validator should be unchanged
2009        let st_val_2 = st.get(&val_2.account).unwrap();
2010        assert_eq!(st_val_2.stake, U256::from(3));
2011        assert_eq!(st_val_2.commission, val_2.commission);
2012        assert_eq!(st_val_2.delegators.len(), 1);
2013
2014        // remove the 2nd validator
2015        events.push(ValidatorExit::from(&val_2).into());
2016
2017        // This should fail because the validator has exited and no longer exists in the stake table.
2018        assert!(ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION).is_err());
2019
2020        Ok(())
2021    }
2022
2023    #[test]
2024    fn test_from_l1_events_failures() -> anyhow::Result<()> {
2025        let val = TestValidator::random();
2026        let delegator = Address::random();
2027
2028        let register: StakeTableEvent = ValidatorRegistered::from(&val).into();
2029        let register_v2: StakeTableEvent = ValidatorRegisteredV2::from(&val).into();
2030        let delegate: StakeTableEvent = Delegated {
2031            delegator,
2032            validator: val.account,
2033            amount: U256::from(10),
2034        }
2035        .into();
2036        let key_update: StakeTableEvent = ConsensusKeysUpdated::from(&val).into();
2037        let key_update_v2: StakeTableEvent = ConsensusKeysUpdatedV2::from(&val).into();
2038        let undelegate: StakeTableEvent = Undelegated {
2039            delegator,
2040            validator: val.account,
2041            amount: U256::from(7),
2042        }
2043        .into();
2044
2045        let exit: StakeTableEvent = ValidatorExit::from(&val).into();
2046
2047        let cases = [
2048            vec![exit],
2049            vec![undelegate.clone()],
2050            vec![delegate.clone()],
2051            vec![key_update],
2052            vec![key_update_v2],
2053            vec![register.clone(), register.clone()],
2054            vec![register_v2.clone(), register_v2.clone()],
2055            vec![register.clone(), register_v2.clone()],
2056            vec![register_v2.clone(), register.clone()],
2057            vec![
2058                register,
2059                delegate.clone(),
2060                undelegate.clone(),
2061                undelegate.clone(),
2062            ],
2063            vec![register_v2, delegate, undelegate.clone(), undelegate],
2064        ];
2065
2066        for events in cases.iter() {
2067            // NOTE: not selecting the active validator set because we care about wrong sequences of
2068            // events being detected. If we compute the active set we will also get an error if the
2069            // set is empty but that's not what we want to test here.
2070            let res = validators_from_l1_events(events.iter().cloned());
2071            assert!(
2072                res.is_err(),
2073                "events {res:?}, not a valid sequence of events"
2074            );
2075        }
2076        Ok(())
2077    }
2078
2079    #[test]
2080    fn test_validators_selection() {
2081        let mut candidates = IndexMap::new();
2082        let mut highest_stake = alloy::primitives::U256::ZERO;
2083
2084        for _i in 0..3000 {
2085            let candidate = RegisteredValidator::mock();
2086            candidates.insert(candidate.account, candidate.clone());
2087
2088            if candidate.stake > highest_stake {
2089                highest_stake = candidate.stake;
2090            }
2091        }
2092
2093        let minimum_stake = highest_stake / U256::from(VID_TARGET_TOTAL_STAKE);
2094
2095        let selected_validators = select_active_validator_set(&candidates, EPOCH_VERSION)
2096            .expect("Failed to select validators");
2097        assert!(
2098            selected_validators.len() <= MAX_VALIDATORS,
2099            "validators len is {}, expected at most {MAX_VALIDATORS}",
2100            selected_validators.len()
2101        );
2102
2103        let mut selected_validators_highest_stake = alloy::primitives::U256::ZERO;
2104        // Ensure every validator in the final selection is above or equal to minimum stake
2105        for (address, validator) in &selected_validators {
2106            assert!(
2107                validator.stake >= minimum_stake,
2108                "Validator {:?} has stake below minimum: {}",
2109                address,
2110                validator.stake
2111            );
2112
2113            if validator.stake > selected_validators_highest_stake {
2114                selected_validators_highest_stake = validator.stake;
2115            }
2116        }
2117    }
2118
2119    // For a bug where the GCL did not match the stake table contract implementation and allowed
2120    // duplicated BLS keys via the update keys events.
2121    #[rstest::rstest]
2122    fn test_regression_non_unique_bls_keys_not_discarded(
2123        #[values(
2124            StakeTableContractVersion::V1,
2125            StakeTableContractVersion::V2,
2126            StakeTableContractVersion::V3
2127        )]
2128        version: StakeTableContractVersion,
2129    ) {
2130        let val = TestValidator::random();
2131        let register: StakeTableEvent = match version {
2132            StakeTableContractVersion::V1 => ValidatorRegistered::from(&val).into(),
2133            StakeTableContractVersion::V2 => ValidatorRegisteredV2::from(&val).into(),
2134            StakeTableContractVersion::V3 => StakeTableEvent::RegisterV3((&val).into()),
2135        };
2136        let delegate: StakeTableEvent = Delegated {
2137            delegator: Address::random(),
2138            validator: val.account,
2139            amount: U256::from(10),
2140        }
2141        .into();
2142
2143        // first ensure that wan build a valid stake table
2144        assert!(
2145            ValidatorSet::from_l1_events(
2146                vec![register.clone(), delegate.clone()].into_iter(),
2147                EPOCH_VERSION,
2148            )
2149            .is_ok()
2150        );
2151
2152        // add the invalid key update (re-using the same consensus keys)
2153        let key_update = ConsensusKeysUpdated::from(&val).into();
2154        let err = ValidatorSet::from_l1_events(
2155            vec![register, delegate, key_update].into_iter(),
2156            EPOCH_VERSION,
2157        )
2158        .unwrap_err();
2159
2160        let bls: BLSPubKey = val.bls_vk.into();
2161        assert!(matches!(err, StakeTableError::BlsKeyAlreadyUsed(addr) if addr == bls.to_string()));
2162    }
2163
2164    // Test that the GCL does not
2165    // allow re-registration for the same Ethereum account.
2166    #[test]
2167    fn test_regression_reregister_eth_account() {
2168        let val1 = TestValidator::random();
2169        let val2 = val1.randomize_keys();
2170        let account = val1.account;
2171
2172        let register1 = ValidatorRegisteredV2::from(&val1).into();
2173        let deregister1 = ValidatorExit::from(&val1).into();
2174        let register2 = ValidatorRegisteredV2::from(&val2).into();
2175        let events = [register1, deregister1, register2];
2176        let error = validators_from_l1_events(events.iter().cloned()).unwrap_err();
2177        assert_matches!(error, StakeTableError::ValidatorAlreadyExited(addr) if addr == account);
2178    }
2179
2180    #[test]
2181    fn test_display_log() {
2182        let serialized = r#"{"address":"0x0000000000000000000000000000000000000069",
2183            "topics":["0x0000000000000000000000000000000000000000000000000000000000000069"],
2184            "data":"0x69",
2185            "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2186            "blockNumber":"0x69","blockTimestamp":"0x69",
2187            "transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2188            "transactionIndex":"0x69","logIndex":"0x70","removed":false}"#;
2189        let log: Log = serde_json::from_str(serialized).unwrap();
2190        assert_eq!(
2191            log.display(),
2192            "Log(block=105,index=112,\
2193             transaction_hash=0x0000000000000000000000000000000000000000000000000000000000000069)"
2194        )
2195    }
2196
2197    #[rstest]
2198    #[case::v1(StakeTableContractVersion::V1)]
2199    #[case::v2(StakeTableContractVersion::V2)]
2200    #[case::v3(StakeTableContractVersion::V3)]
2201    fn test_register_validator(#[case] version: StakeTableContractVersion) {
2202        let mut state = StakeTableState::default();
2203        let validator = TestValidator::random();
2204
2205        let event = match version {
2206            StakeTableContractVersion::V1 => StakeTableEvent::Register((&validator).into()),
2207            StakeTableContractVersion::V2 => StakeTableEvent::RegisterV2((&validator).into()),
2208            StakeTableContractVersion::V3 => StakeTableEvent::RegisterV3((&validator).into()),
2209        };
2210
2211        state.apply_event(event).unwrap().unwrap();
2212
2213        let stored = state.validators.get(&validator.account).unwrap();
2214        assert_eq!(stored.account, validator.account);
2215    }
2216
2217    #[rstest]
2218    #[case::v1(StakeTableContractVersion::V1)]
2219    #[case::v2(StakeTableContractVersion::V2)]
2220    #[case::v3(StakeTableContractVersion::V3)]
2221    fn test_validator_already_registered(#[case] version: StakeTableContractVersion) {
2222        let mut stake_table_state = StakeTableState::default();
2223
2224        let test_validator = TestValidator::random();
2225
2226        // First registration attempt using the specified contract version
2227        match version {
2228            StakeTableContractVersion::V1 => {
2229                stake_table_state.apply_event(StakeTableEvent::Register((&test_validator).into()))
2230            },
2231            StakeTableContractVersion::V2 => {
2232                stake_table_state.apply_event(StakeTableEvent::RegisterV2((&test_validator).into()))
2233            },
2234            StakeTableContractVersion::V3 => {
2235                stake_table_state.apply_event(StakeTableEvent::RegisterV3((&test_validator).into()))
2236            },
2237        }
2238        .unwrap()
2239        .unwrap(); // Expect the first registration to succeed
2240
2241        // attempt using V1 registration (should fail with AlreadyRegistered)
2242        let v1_already_registered_result = stake_table_state
2243            .clone()
2244            .apply_event(StakeTableEvent::Register((&test_validator).into()));
2245
2246        pretty_assertions::assert_matches!(
2247           v1_already_registered_result,  Err(StakeTableError::AlreadyRegistered(account))
2248                if account == test_validator.account,
2249           "Expected AlreadyRegistered error. version ={version:?} result={v1_already_registered_result:?}",
2250        );
2251
2252        // attempt using V2 registration (should also fail with AlreadyRegistered)
2253        let v2_already_registered_result = stake_table_state
2254            .clone()
2255            .apply_event(StakeTableEvent::RegisterV2((&test_validator).into()));
2256
2257        pretty_assertions::assert_matches!(
2258            v2_already_registered_result,
2259            Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
2260            "Expected AlreadyRegistered error. version ={version:?} result={v2_already_registered_result:?}",
2261
2262        );
2263
2264        // attempt using V3 registration with a different x25519 key (should also fail
2265        // with AlreadyRegistered because the validator address is already registered)
2266        let v3_already_registered_result =
2267            stake_table_state
2268                .clone()
2269                .apply_event(StakeTableEvent::RegisterV3(
2270                    (&test_validator
2271                        .clone()
2272                        .with_x25519_key([43u8; 32])
2273                        .with_p2p_addr("127.0.0.1:9001"))
2274                        .into(),
2275                ));
2276
2277        pretty_assertions::assert_matches!(
2278            v3_already_registered_result,
2279            Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
2280            "Expected AlreadyRegistered error. version ={version:?} result={v3_already_registered_result:?}",
2281        );
2282    }
2283
2284    #[test]
2285    fn test_register_validator_v2_auth_fails_marks_as_unauthenticated() {
2286        let mut state = StakeTableState::default();
2287        let mut val = TestValidator::random();
2288        val.bls_sig = Default::default();
2289        let event = StakeTableEvent::RegisterV2((&val).into());
2290
2291        let result = state.apply_event(event);
2292        assert!(
2293            result.is_ok(),
2294            "Validator with invalid auth should still be accepted"
2295        );
2296
2297        let validator = state
2298            .validators()
2299            .get(&val.account)
2300            .expect("validator should exist");
2301        assert!(
2302            !validator.authenticated,
2303            "Validator should be marked as not authenticated"
2304        );
2305
2306        let event = StakeTableEvent::Delegate(Delegated {
2307            delegator: Address::random(),
2308            validator: val.account,
2309            amount: U256::from(100),
2310        });
2311        state.apply_event(event).unwrap().unwrap();
2312
2313        let active = select_active_validator_set(state.validators(), EPOCH_VERSION);
2314        match active {
2315            Err(_) => {}, // No validators is valid - means the unauthenticated one was filtered
2316            Ok(map) => {
2317                assert!(
2318                    map.get(&val.account).is_none(),
2319                    "Unauthenticated validator should not be in active set"
2320                );
2321            },
2322        }
2323    }
2324
2325    #[test]
2326    fn test_authenticated_validator_deserialize_rejects_unauthenticated() {
2327        let mut validator = RegisteredValidator::<BLSPubKey>::mock();
2328        validator.authenticated = false;
2329
2330        let json = serde_json::to_string(&validator).unwrap();
2331        let result: Result<AuthenticatedValidator<BLSPubKey>, _> = serde_json::from_str(&json);
2332
2333        assert!(result.is_err());
2334        let err = result.unwrap_err().to_string();
2335        assert!(
2336            err.contains("cannot deserialize unauthenticated validator"),
2337            "unexpected error: {err}"
2338        );
2339    }
2340
2341    #[rstest]
2342    #[case::v1(StakeTableContractVersion::V1)]
2343    #[case::v2(StakeTableContractVersion::V2)]
2344    #[case::v3(StakeTableContractVersion::V3)]
2345    fn test_deregister_validator(#[case] version: StakeTableContractVersion) {
2346        let mut state = StakeTableState::default();
2347        let val = TestValidator::random();
2348
2349        let reg = StakeTableEvent::Register((&val).into());
2350        state.apply_event(reg).unwrap().unwrap();
2351
2352        let dereg = match version {
2353            StakeTableContractVersion::V1 => StakeTableEvent::Deregister((&val).into()),
2354            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2355                StakeTableEvent::DeregisterV2(ValidatorExitV2 {
2356                    validator: val.account,
2357                    unlocksAt: U256::from(1000u64),
2358                })
2359            },
2360        };
2361        state.apply_event(dereg).unwrap().unwrap();
2362        assert!(!state.validators.contains_key(&val.account));
2363    }
2364
2365    #[rstest]
2366    #[case::v1(StakeTableContractVersion::V1)]
2367    #[case::v2(StakeTableContractVersion::V2)]
2368    #[case::v3(StakeTableContractVersion::V3)]
2369    fn test_delegate_and_undelegate(#[case] version: StakeTableContractVersion) {
2370        let mut state = StakeTableState::default();
2371        let val = TestValidator::random();
2372        state
2373            .apply_event(StakeTableEvent::Register((&val).into()))
2374            .unwrap()
2375            .unwrap();
2376
2377        let delegator = Address::random();
2378        let amount = U256::from(1000);
2379        let delegate_event = StakeTableEvent::Delegate(Delegated {
2380            delegator,
2381            validator: val.account,
2382            amount,
2383        });
2384        state.apply_event(delegate_event).unwrap().unwrap();
2385
2386        let validator = state.validators.get(&val.account).unwrap();
2387        assert_eq!(validator.delegators.get(&delegator).cloned(), Some(amount));
2388
2389        let undelegate_event = match version {
2390            StakeTableContractVersion::V1 => StakeTableEvent::Undelegate(Undelegated {
2391                delegator,
2392                validator: val.account,
2393                amount,
2394            }),
2395            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2396                StakeTableEvent::UndelegateV2(UndelegatedV2 {
2397                    delegator,
2398                    validator: val.account,
2399                    amount,
2400                    unlocksAt: U256::from(2000u64),
2401                    undelegationId: 1,
2402                })
2403            },
2404        };
2405        state.apply_event(undelegate_event).unwrap().unwrap();
2406        let validator = state.validators.get(&val.account).unwrap();
2407        assert!(!validator.delegators.contains_key(&delegator));
2408    }
2409
2410    #[rstest]
2411    #[case::v1(StakeTableContractVersion::V1)]
2412    #[case::v2(StakeTableContractVersion::V2)]
2413    #[case::v3(StakeTableContractVersion::V3)]
2414    fn test_key_update_event(#[case] version: StakeTableContractVersion) {
2415        let mut state = StakeTableState::default();
2416        let val = TestValidator::random();
2417
2418        // Always register first using V1 to simulate upgrade scenarios
2419        state
2420            .apply_event(StakeTableEvent::Register((&val).into()))
2421            .unwrap()
2422            .unwrap();
2423
2424        let new_keys = val.randomize_keys();
2425
2426        let event = match version {
2427            StakeTableContractVersion::V1 => StakeTableEvent::KeyUpdate((&new_keys).into()),
2428            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2429                StakeTableEvent::KeyUpdateV2((&new_keys).into())
2430            },
2431        };
2432
2433        state.apply_event(event).unwrap().unwrap();
2434
2435        let updated = state.validators.get(&val.account).unwrap();
2436        assert_eq!(updated.stake_table_key, new_keys.bls_vk.into());
2437        assert_eq!(updated.state_ver_key, new_keys.schnorr_vk.into());
2438    }
2439
2440    #[test]
2441    fn test_duplicate_bls_key() {
2442        let mut state = StakeTableState::default();
2443        let val = TestValidator::random();
2444        let event1 = StakeTableEvent::Register((&val).into());
2445        let mut val2 = TestValidator::random();
2446        val2.bls_vk = val.bls_vk;
2447        val2.account = Address::random();
2448
2449        let event2 = StakeTableEvent::Register((&val2).into());
2450        state.apply_event(event1).unwrap().unwrap();
2451        let result = state.apply_event(event2);
2452
2453        let expected_bls_key = BLSPubKey::from(val.bls_vk).to_string();
2454
2455        assert_matches!(
2456            result,
2457            Err(StakeTableError::BlsKeyAlreadyUsed(key))
2458                if key == expected_bls_key,
2459            "Expected BlsKeyAlreadyUsed({expected_bls_key}), but got: {result:?}",
2460        );
2461    }
2462
2463    #[test]
2464    fn test_duplicate_schnorr_key() {
2465        let mut state = StakeTableState::default();
2466        let val = TestValidator::random();
2467        let event1 = StakeTableEvent::Register((&val).into());
2468        let mut val2 = TestValidator::random();
2469        val2.schnorr_vk = val.schnorr_vk;
2470        val2.account = Address::random();
2471        val2.bls_vk = val2.randomize_keys().bls_vk;
2472
2473        let event2 = StakeTableEvent::Register((&val2).into());
2474        state.apply_event(event1).unwrap().unwrap();
2475        let result = state.apply_event(event2);
2476
2477        let schnorr: SchnorrPubKey = val.schnorr_vk.into();
2478        assert_matches!(
2479            result,
2480            Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(key)))
2481                if key == schnorr.to_string(),
2482            "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
2483
2484        );
2485    }
2486
2487    #[test]
2488    fn test_duplicate_schnorr_key_v2_during_update() {
2489        let mut state = StakeTableState::default();
2490
2491        let val1 = TestValidator::random();
2492
2493        let mut rng = &mut rand::thread_rng();
2494        let bls_key_pair = BLSKeyPair::generate(&mut rng);
2495
2496        let val2 = TestValidator {
2497            bls_vk: bls_key_pair.ver_key().to_affine().into(),
2498            bls_sig: sign_address_bls(&bls_key_pair, val1.account).into(),
2499            ..val1.clone()
2500        };
2501        let event1 = StakeTableEvent::RegisterV2((&val1).into());
2502        let event2 = StakeTableEvent::KeyUpdateV2((&val2).into());
2503
2504        state.apply_event(event1).unwrap().unwrap();
2505        let result = state.apply_event(event2);
2506
2507        let schnorr: SchnorrPubKey = val1.schnorr_vk.into();
2508        assert_matches!(
2509            result,
2510            Err(StakeTableError::SchnorrKeyAlreadyUsed(key))
2511                if key == schnorr.to_string(),
2512            "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
2513        );
2514    }
2515
2516    #[test]
2517    fn test_register_and_deregister_validator() {
2518        let mut state = StakeTableState::default();
2519        let validator = TestValidator::random();
2520        let event = StakeTableEvent::Register((&validator).into());
2521        state.apply_event(event).unwrap().unwrap();
2522
2523        let deregister_event = StakeTableEvent::Deregister((&validator).into());
2524        assert!(state.apply_event(deregister_event).unwrap().is_ok());
2525    }
2526
2527    #[test]
2528    fn test_commission_validation_exceeds_basis_points() {
2529        // Create a simple stake table with one validator
2530        let validator = TestValidator::random();
2531        let mut stake_table = StakeTableState::default();
2532
2533        // Register the validator first
2534        let registration_event = ValidatorRegistered::from(&validator).into();
2535        stake_table
2536            .apply_event(registration_event)
2537            .unwrap()
2538            .unwrap();
2539
2540        // Test that a commission exactly at the limit is allowed
2541        let valid_commission_event = CommissionUpdated {
2542            validator: validator.account,
2543            timestamp: Default::default(),
2544            oldCommission: 0,
2545            newCommission: COMMISSION_BASIS_POINTS, // Exactly at the limit
2546        }
2547        .into();
2548        stake_table
2549            .apply_event(valid_commission_event)
2550            .unwrap()
2551            .unwrap();
2552
2553        let invalid_commission = COMMISSION_BASIS_POINTS + 1;
2554        let invalid_commission_event = CommissionUpdated {
2555            validator: validator.account,
2556            timestamp: Default::default(),
2557            oldCommission: 0,
2558            newCommission: invalid_commission,
2559        }
2560        .into();
2561
2562        let err = stake_table
2563            .apply_event(invalid_commission_event)
2564            .unwrap_err();
2565
2566        assert_matches!(
2567            err,
2568            StakeTableError::InvalidCommission(addr, invalid_commission)
2569                if addr == addr && invalid_commission == invalid_commission);
2570    }
2571
2572    #[test]
2573    fn test_delegate_zero_amount_is_rejected() {
2574        let mut state = StakeTableState::default();
2575        let validator = TestValidator::random();
2576        let account = validator.account;
2577        state
2578            .apply_event(StakeTableEvent::Register((&validator).into()))
2579            .unwrap()
2580            .unwrap();
2581
2582        let delegator = Address::random();
2583        let amount = U256::ZERO;
2584        let event = StakeTableEvent::Delegate(Delegated {
2585            delegator,
2586            validator: account,
2587            amount,
2588        });
2589        let result = state.apply_event(event);
2590
2591        assert_matches!(
2592            result,
2593            Err(StakeTableError::ZeroDelegatorStake(addr))
2594                if addr == delegator,
2595            "delegator stake is zero"
2596
2597        );
2598    }
2599
2600    #[test]
2601    fn test_undelegate_more_than_stake_fails() {
2602        let mut state = StakeTableState::default();
2603        let validator = TestValidator::random();
2604        let account = validator.account;
2605        state
2606            .apply_event(StakeTableEvent::Register((&validator).into()))
2607            .unwrap()
2608            .unwrap();
2609
2610        let delegator = Address::random();
2611        let event = StakeTableEvent::Delegate(Delegated {
2612            delegator,
2613            validator: account,
2614            amount: U256::from(10u64),
2615        });
2616        state.apply_event(event).unwrap().unwrap();
2617
2618        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2619            delegator,
2620            validator: account,
2621            amount: U256::from(20u64),
2622        }));
2623        assert_matches!(
2624            result,
2625            Err(StakeTableError::InsufficientStake),
2626            "Expected InsufficientStake error, got: {result:?}",
2627        );
2628    }
2629
2630    #[test]
2631    fn test_apply_event_does_not_modify_state_on_error() {
2632        let mut state = StakeTableState::default();
2633        let validator = TestValidator::random();
2634        let delegator = Address::random();
2635
2636        state
2637            .apply_event(StakeTableEvent::Register((&validator).into()))
2638            .unwrap()
2639            .unwrap();
2640
2641        // AlreadyRegistered error
2642        let state_before = state.clone();
2643        let result = state.apply_event(StakeTableEvent::Register((&validator).into()));
2644        assert_matches!(result, Err(StakeTableError::AlreadyRegistered(_)));
2645        assert_eq!(
2646            state, state_before,
2647            "State should not change on AlreadyRegistered error"
2648        );
2649
2650        // Duplicate BLS key error
2651        let state_before = state.clone();
2652        let mut validator2 = TestValidator::random();
2653        validator2.bls_vk = validator.bls_vk; // Reuse BLS key
2654        let result = state.apply_event(StakeTableEvent::Register((&validator2).into()));
2655        assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
2656        assert_eq!(
2657            state, state_before,
2658            "State should not change on BlsKeyAlreadyUsed error"
2659        );
2660
2661        // ValidatorNotFound error on deregister
2662        let state_before = state.clone();
2663        let nonexistent_validator = TestValidator::random();
2664        let result =
2665            state.apply_event(StakeTableEvent::Deregister((&nonexistent_validator).into()));
2666        assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
2667        assert_eq!(
2668            state, state_before,
2669            "State should not change on ValidatorNotFound error"
2670        );
2671
2672        // ValidatorNotFound error on undelegate
2673        let state_before = state.clone();
2674        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2675            delegator: Address::random(),
2676            validator: Address::random(),
2677            amount: U256::from(100u64),
2678        }));
2679        assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
2680        assert_eq!(
2681            state, state_before,
2682            "State should not change on ValidatorNotFound error for Undelegate"
2683        );
2684
2685        state
2686            .apply_event(StakeTableEvent::Delegate(Delegated {
2687                delegator,
2688                validator: validator.account,
2689                amount: U256::from(100u64),
2690            }))
2691            .unwrap()
2692            .unwrap();
2693
2694        // DelegatorNotFound error on undelegate
2695        let state_before = state.clone();
2696        let non_existent_delegator = Address::random();
2697        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2698            delegator: non_existent_delegator,
2699            validator: validator.account,
2700            amount: U256::from(50u64),
2701        }));
2702        assert_matches!(result, Err(StakeTableError::DelegatorNotFound(_)));
2703        assert_eq!(
2704            state, state_before,
2705            "State should not change on DelegatorNotFound error"
2706        );
2707
2708        // InsufficientStake error on undelegate
2709        let state_before = state.clone();
2710        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2711            delegator,
2712            validator: validator.account,
2713            amount: U256::from(200u64),
2714        }));
2715        assert_matches!(result, Err(StakeTableError::InsufficientStake));
2716        assert_eq!(
2717            state, state_before,
2718            "State should not change on InsufficientStake error"
2719        );
2720
2721        // InsufficientStake when validator total stake would be less than amount
2722        let validator2 = TestValidator::random();
2723        let delegator2 = Address::random();
2724
2725        state
2726            .apply_event(StakeTableEvent::Register((&validator2).into()))
2727            .unwrap()
2728            .unwrap();
2729
2730        state
2731            .apply_event(StakeTableEvent::Delegate(Delegated {
2732                delegator: delegator2,
2733                validator: validator2.account,
2734                amount: U256::from(50u64),
2735            }))
2736            .unwrap()
2737            .unwrap();
2738        let state_before = state.clone();
2739        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2740            delegator: delegator2,
2741            validator: validator2.account,
2742            amount: U256::from(100u64),
2743        }));
2744        assert_matches!(result, Err(StakeTableError::InsufficientStake));
2745        assert_eq!(state, state_before,);
2746
2747        // ZeroDelegatorStake error
2748        let state_before = state.clone();
2749        let result = state.apply_event(StakeTableEvent::Delegate(Delegated {
2750            delegator: Address::random(),
2751            validator: validator.account,
2752            amount: U256::ZERO,
2753        }));
2754        assert_matches!(result, Err(StakeTableError::ZeroDelegatorStake(_)));
2755        assert_eq!(
2756            state, state_before,
2757            "State should not change on ZeroDelegatorStake error"
2758        );
2759    }
2760
2761    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2762    async fn test_decaf_stake_table() {
2763        // The following commented-out block demonstrates how the `decaf_stake_table_events.json`
2764        // and `decaf_stake_table.json` files were originally generated.
2765
2766        // It generates decaf stake table data by fetching events from the contract,
2767        // writes events and the constructed stake table to JSON files.
2768
2769        /*
2770        let l1 = L1Client::new(vec!["https://ethereum-sepolia.publicnode.com"
2771            .parse()
2772            .unwrap()])
2773        .unwrap();
2774        let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
2775
2776        let events = Fetcher::fetch_events_from_contract(
2777            l1,
2778            contract_address.parse().unwrap(),
2779            None,
2780            8582328,
2781        )
2782        .await?;
2783
2784        // Serialize and write sorted events
2785        let json_events = serde_json::to_string_pretty(&sorted_events)?;
2786        let mut events_file = File::create("decaf_stake_table_events.json").await?;
2787        events_file.write_all(json_events.as_bytes()).await?;
2788
2789        // Process into stake table
2790        let stake_table = validators_from_l1_events(sorted_events.into_iter().map(|(_, e)| e))?;
2791
2792        // Serialize and write stake table
2793        let json_stake_table = serde_json::to_string_pretty(&stake_table)?;
2794        let mut stake_file = File::create("decaf_stake_table.json").await?;
2795        stake_file.write_all(json_stake_table.as_bytes()).await?;
2796        */
2797
2798        let events_json =
2799            std::fs::read_to_string("../../../data/v3/decaf_stake_table_events.json").unwrap();
2800        let events: Vec<(EventKey, StakeTableEvent)> = serde_json::from_str(&events_json).unwrap();
2801
2802        // Reconstruct stake table from events
2803        let reconstructed_stake_table =
2804            ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), EPOCH_VERSION)
2805                .unwrap()
2806                .active_validators;
2807
2808        let stake_table_json =
2809            std::fs::read_to_string("../../../data/v3/decaf_stake_table.json").unwrap();
2810        let expected: AuthenticatedValidatorMap = serde_json::from_str(&stake_table_json).unwrap();
2811
2812        assert_eq!(
2813            reconstructed_stake_table, expected,
2814            "Stake table reconstructed from events does not match the expected stake table "
2815        );
2816    }
2817
2818    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2819    #[should_panic]
2820    async fn test_large_max_events_range_panic() {
2821        // decaf stake table contract address
2822        let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
2823
2824        let l1 = L1ClientOptions {
2825            l1_events_max_retry_duration: Duration::from_secs(30),
2826            // max block range for public node rpc is 50000 so this should result in a panic
2827            l1_events_max_block_range: 10_u64.pow(9),
2828            l1_retry_delay: Duration::from_secs(1),
2829            ..Default::default()
2830        }
2831        .connect(vec![
2832            "https://ethereum-sepolia.publicnode.com".parse().unwrap(),
2833        ])
2834        .expect("unable to construct l1 client");
2835
2836        let latest_block = l1.provider.get_block_number().await.unwrap();
2837        let _events = Fetcher::fetch_events_from_contract(
2838            l1,
2839            contract_address.parse().unwrap(),
2840            None,
2841            latest_block,
2842        )
2843        .await
2844        .unwrap();
2845    }
2846
2847    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2848    async fn sanity_check_block_reward_v3() {
2849        // 10b tokens
2850        let initial_supply = U256::from_str("10000000000000000000000000000").unwrap();
2851
2852        let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
2853            .checked_div(U256::from(COMMISSION_BASIS_POINTS))
2854            .unwrap();
2855
2856        println!("Calculated reward: {reward}");
2857        assert!(reward > U256::ZERO);
2858    }
2859
2860    #[test]
2861    fn sanity_check_p_and_rp() {
2862        let total_stake = BigDecimal::from_str("1000").unwrap();
2863        let total_supply = BigDecimal::from_str("10000").unwrap(); // p = 0.1
2864
2865        let (p, rp) =
2866            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
2867
2868        assert!(p > BigDecimal::zero());
2869        assert!(p < BigDecimal::one());
2870        assert!(rp > BigDecimal::zero());
2871    }
2872
2873    #[test]
2874    fn test_p_out_of_range() {
2875        let total_stake = BigDecimal::from_str("1000").unwrap();
2876        let total_supply = BigDecimal::from_str("500").unwrap(); // p = 2.0
2877
2878        let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
2879        assert!(result.is_err());
2880    }
2881
2882    #[test]
2883    fn test_zero_total_supply() {
2884        let total_stake = BigDecimal::from_str("1000").unwrap();
2885        let total_supply = BigDecimal::from(0);
2886
2887        let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
2888        assert!(result.is_err());
2889    }
2890
2891    #[test]
2892    fn test_valid_p_and_rp() {
2893        let total_stake = BigDecimal::from_str("5000").unwrap();
2894        let total_supply = BigDecimal::from_str("10000").unwrap();
2895
2896        let (p, rp) =
2897            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
2898
2899        assert_eq!(p, BigDecimal::from_str("0.5").unwrap());
2900        assert!(rp > BigDecimal::from_str("0.0").unwrap());
2901    }
2902
2903    #[test]
2904    fn test_very_small_p() {
2905        let total_stake = BigDecimal::from_str("1").unwrap(); // 1 wei
2906        let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap(); // 10B * 1e18
2907
2908        let (p, rp) =
2909            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
2910
2911        assert!(p > BigDecimal::from_str("0").unwrap());
2912        assert!(p < BigDecimal::from_str("1e-18").unwrap()); // p should be extremely small
2913        assert!(rp > BigDecimal::zero());
2914    }
2915
2916    #[test]
2917    fn test_p_very_close_to_one() {
2918        let total_stake = BigDecimal::from_str("9999999999999999999999999999").unwrap();
2919        let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap();
2920
2921        let (p, rp) =
2922            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
2923
2924        assert!(p < BigDecimal::one());
2925        assert!(p > BigDecimal::from_str("0.999999999999999999999999999").unwrap());
2926        assert!(rp > BigDecimal::zero());
2927    }
2928
2929    /// tests `calculate_proportion_staked_and_reward_rate` produces correct p and R(p) values
2930    /// across a range of stake proportions within a small numerical tolerance.
2931    ///
2932    #[test]
2933    fn test_reward_rate_rp() {
2934        let test_cases = [
2935            // p , R(p)
2936            ("0.0000", "0.2121"), // 0%
2937            ("0.0050", "0.2121"), // 0.5%
2938            ("0.0100", "0.2121"), // 1%
2939            ("0.0250", "0.1342"), // 2.5%
2940            ("0.0500", "0.0949"), // 5%
2941            ("0.1000", "0.0671"), // 10%
2942            ("0.2500", "0.0424"), // 25%
2943            ("0.5000", "0.0300"), // 50%
2944            ("0.7500", "0.0245"), // 75%
2945            ("1.0000", "0.0212"), // 100%
2946        ];
2947
2948        let tolerance = BigDecimal::from_str("0.0001").unwrap();
2949
2950        let total_supply = BigDecimal::from_u32(10_000).unwrap();
2951
2952        for (p, rp) in test_cases {
2953            let p = BigDecimal::from_str(p).unwrap();
2954            let expected_rp = BigDecimal::from_str(rp).unwrap();
2955
2956            let total_stake = &p * &total_supply;
2957
2958            let (computed_p, computed_rp) =
2959                calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
2960
2961            assert!(
2962                (&computed_p - &p).abs() < tolerance,
2963                "p mismatch: got {computed_p}, expected {p}"
2964            );
2965
2966            assert!(
2967                (&computed_rp - &expected_rp).abs() < tolerance,
2968                "R(p) mismatch for p={p}: got {computed_rp}, expected {expected_rp}"
2969            );
2970        }
2971    }
2972
2973    #[tokio::test(flavor = "multi_thread")]
2974    async fn test_dynamic_block_reward_with_expected_values() {
2975        // 10B tokens = 10_000_000_000 * 10^18
2976        let total_supply = U256::from_str("10000000000000000000000000000").unwrap();
2977        let total_supply_bd = BigDecimal::from_str(&total_supply.to_string()).unwrap();
2978
2979        let test_cases = [
2980            // --- Block time: 1 ms ---
2981            ("0.0000", "0.2121", 1, "0"), // 0% staked → inflation = 0 → 0 tokens
2982            ("0.0050", "0.2121", 1, "3362823439878234"), // 0.105% inflation → ~0.00336 tokens
2983            ("0.0100", "0.2121", 1, "6725646879756468"), // 0.2121% inflation → ~0.00673 tokens
2984            ("0.0250", "0.1342", 1, "10638635210553018"), // 0.3355% inflation → ~0.01064 tokens
2985            ("0.0500", "0.0949", 1, "15046296296296296"), // 0.4745% inflation → ~0.01505 tokens
2986            ("0.1000", "0.0671", 1, "21277270421106037"), // 0.671% inflation → ~0.02128 tokens
2987            ("0.2500", "0.0424", 1, "33612379502790461"), // 1.06% inflation → ~0.03361 tokens
2988            ("0.5000", "0.0300", 1, "47564687975646879"), // 1.5% inflation → ~0.04756 tokens
2989            ("0.7500", "0.0245", 1, "58266742770167427"), // 1.8375% inflation → ~0.05827 tokens
2990            ("1.0000", "0.0212", 1, "67224759005580923"), // 2.12% inflation → ~0.06722 tokens
2991            // --- Block time: 2000 ms (2 seconds) ---
2992            ("0.0000", "0.2121", 2000, "0"), // 0% staked → inflation = 0 → 0 tokens
2993            ("0.0050", "0.2121", 2000, "672564687975646880"), // 0.105% inflation → ~0.67256 tokens
2994            ("0.0100", "0.2121", 2000, "1345129375951293760"), // 0.2121% inflation → ~1.34513 tokens
2995            ("0.0250", "0.1342", 2000, "2127727042110603754"), // 0.3355% inflation → ~2.12773 tokens
2996            ("0.0500", "0.0949", 2000, "3009259259259259259"), // 0.4745% inflation → ~3.00926 tokens
2997            ("0.1000", "0.0671", 2000, "4255454084221207509"), // 0.671% inflation → ~4.25545 tokens
2998            ("0.2500", "0.0424", 2000, "6722475900558092339"), // 1.06% inflation → ~6.72248 tokens
2999            ("0.5000", "0.0300", 2000, "9512937595129375951"), // 1.5% inflation → ~9.51294 tokens
3000            ("0.7500", "0.0245", 2000, "11653348554033485540"), // 1.8375% inflation → ~11.65335 tokens
3001            ("1.0000", "0.0212", 2000, "13444951801116184678"), // 2.12% inflation → ~13.44495 tokens
3002            // --- Block time: 10000 ms (10 seconds) ---
3003            ("0.0000", "0.2121", 10000, "0"), // 0% staked → inflation = 0 → 0 tokens
3004            ("0.0050", "0.2121", 10000, "3362823439878234400"), // 0.105% inflation → ~3.36 tokens
3005            ("0.0100", "0.2121", 10000, "6725646879756468800"), // 0.2121% inflation → ~6.73 tokens
3006            ("0.0250", "0.1342", 10000, "10638635210553018770"), // 0.3355% inflation → ~10.64 tokens
3007            ("0.0500", "0.0949", 10000, "15046296296296296295"), // 0.4745% inflation → ~15.05 tokens
3008            ("0.1000", "0.0671", 10000, "21277270421106037545"), // 0.671% inflation → ~21.28 tokens
3009            ("0.2500", "0.0424", 10000, "33612379502790461695"), // 1.06% inflation → ~33.61 tokens
3010            ("0.5000", "0.0300", 10000, "47564687975646879755"), // 1.5% inflation → ~47.56 tokens
3011            ("0.7500", "0.0245", 10000, "58266742770167427700"), // 1.8375% inflation → ~58.27 tokens
3012            ("1.0000", "0.0212", 10000, "67224759005580923390"), // 2.12% inflation → ~67.22 tokens
3013        ];
3014
3015        let tolerance = U256::from(100_000_000_000_000_000u128); // 0.1 token
3016
3017        for (p, rp, avg_block_time_ms, expected_reward) in test_cases {
3018            let p = BigDecimal::from_str(p).unwrap();
3019            let total_stake_bd = (&p * &total_supply_bd).round(0);
3020            println!("total_stake_bd={total_stake_bd}");
3021
3022            let total_stake = U256::from_str(&total_stake_bd.to_plain_string()).unwrap();
3023            let expected_reward = U256::from_str(expected_reward).unwrap();
3024
3025            let epoch = EpochNumber::new(0);
3026            let actual_reward =
3027                compute_block_reward(epoch, total_supply, total_stake, avg_block_time_ms)
3028                    .unwrap()
3029                    .0;
3030
3031            let diff = if actual_reward > expected_reward {
3032                actual_reward - expected_reward
3033            } else {
3034                expected_reward - actual_reward
3035            };
3036
3037            assert!(
3038                diff <= tolerance,
3039                "Reward mismatch for p = {p}, R(p) = {rp}, block_time = {avg_block_time_ms}: \
3040                 expected = {expected_reward}, actual = {actual_reward}, diff = {diff}"
3041            );
3042        }
3043    }
3044
3045    // Uses V2 events where available. Delegate and CommissionUpdate don't have V2 versions.
3046    #[derive(Debug, Clone, Copy)]
3047    enum EventType {
3048        Delegate,
3049        Undelegate,
3050        KeyUpdate,
3051        CommissionUpdate,
3052        Exit,
3053        X25519KeyUpdate,
3054        P2pAddrUpdate,
3055    }
3056
3057    // Regression for PR #3903: validators with invalid signatures used to be dropped during
3058    // registration, so any later event targeting them failed with ValidatorNotFound and broke
3059    // stake table reconstruction. Now they're stored with authenticated=false and subsequent
3060    // events must succeed against them (though they stay out of the active consensus set).
3061    #[rstest]
3062    #[case::delegate(EventType::Delegate)]
3063    #[case::undelegate(EventType::Undelegate)]
3064    #[case::key_update(EventType::KeyUpdate)]
3065    #[case::commission_update(EventType::CommissionUpdate)]
3066    #[case::exit(EventType::Exit)]
3067    #[case::x25519_key_update(EventType::X25519KeyUpdate)]
3068    #[case::p2p_addr_update(EventType::P2pAddrUpdate)]
3069    fn test_events_targeting_unauthenticated_validator(
3070        #[case] event_type: EventType,
3071    ) -> anyhow::Result<()> {
3072        let mut state = StakeTableState::default();
3073        let mut val = TestValidator::random();
3074        val.bls_sig = Default::default();
3075        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3076
3077        let validator = state.validators().get(&val.account).context("validator")?;
3078        assert!(!validator.authenticated);
3079
3080        let delegator = Address::random();
3081        let initial_amount = U256::from(1000);
3082        state.apply_event(StakeTableEvent::Delegate(Delegated {
3083            delegator,
3084            validator: val.account,
3085            amount: initial_amount,
3086        }))??;
3087
3088        match event_type {
3089            EventType::Delegate => {
3090                let new_delegator = Address::random();
3091                let amount = U256::from(500);
3092                state.apply_event(StakeTableEvent::Delegate(Delegated {
3093                    delegator: new_delegator,
3094                    validator: val.account,
3095                    amount,
3096                }))??;
3097
3098                let validator = state.validators().get(&val.account).context("validator")?;
3099                assert_eq!(validator.stake, initial_amount + amount);
3100                assert_eq!(
3101                    validator.delegators.get(&new_delegator).cloned(),
3102                    Some(amount)
3103                );
3104            },
3105            EventType::Undelegate => {
3106                let undelegate_amount = U256::from(300);
3107                state.apply_event(StakeTableEvent::UndelegateV2(UndelegatedV2 {
3108                    delegator,
3109                    validator: val.account,
3110                    undelegationId: 1,
3111                    amount: undelegate_amount,
3112                    unlocksAt: U256::from(1000u64),
3113                }))??;
3114
3115                let validator = state.validators().get(&val.account).context("validator")?;
3116                assert_eq!(validator.stake, initial_amount - undelegate_amount);
3117                assert_eq!(
3118                    validator.delegators.get(&delegator).cloned(),
3119                    Some(initial_amount - undelegate_amount)
3120                );
3121            },
3122            EventType::KeyUpdate => {
3123                let new_keys = val.randomize_keys();
3124                state.apply_event(StakeTableEvent::KeyUpdateV2((&new_keys).into()))??;
3125
3126                let validator = state.validators().get(&val.account).context("validator")?;
3127                let expected_bls: BLSPubKey = new_keys.bls_vk.into();
3128                let expected_schnorr: SchnorrPubKey = new_keys.schnorr_vk.into();
3129                assert_eq!(validator.stake_table_key, expected_bls);
3130                assert_eq!(validator.state_ver_key, expected_schnorr);
3131                // KeyUpdate does not re-authenticate
3132                assert!(!validator.authenticated);
3133            },
3134            EventType::CommissionUpdate => {
3135                let new_commission: u16 = 5000;
3136                state.apply_event(StakeTableEvent::CommissionUpdate(CommissionUpdated {
3137                    validator: val.account,
3138                    timestamp: Default::default(),
3139                    oldCommission: val.commission,
3140                    newCommission: new_commission,
3141                }))??;
3142
3143                let validator = state.validators().get(&val.account).context("validator")?;
3144                assert_eq!(validator.commission, new_commission);
3145            },
3146            EventType::Exit => {
3147                state.apply_event(StakeTableEvent::DeregisterV2(ValidatorExitV2 {
3148                    validator: val.account,
3149                    unlocksAt: U256::from(1000u64),
3150                }))??;
3151
3152                assert!(!state.validators().contains_key(&val.account));
3153                return Ok(());
3154            },
3155            EventType::X25519KeyUpdate => {
3156                let new_x25519_key = [99u8; 32];
3157                state.apply_event(val.x25519_update(new_x25519_key))??;
3158
3159                let validator = state.validators().get(&val.account).context("validator")?;
3160                let expected = x25519::PublicKey::try_from(new_x25519_key.as_slice())
3161                    .expect("valid x25519 key");
3162                assert_eq!(validator.x25519_key, Some(expected));
3163                assert!(!validator.authenticated);
3164            },
3165            EventType::P2pAddrUpdate => {
3166                state.apply_event(val.p2p_update("10.0.0.1:9000"))??;
3167
3168                let validator = state.validators().get(&val.account).context("validator")?;
3169                let expected: NetAddr = "10.0.0.1:9000".parse().expect("valid p2p addr");
3170                assert_eq!(validator.p2p_addr, Some(expected));
3171                assert!(!validator.authenticated);
3172            },
3173        }
3174
3175        let active = select_active_validator_set(state.validators(), EPOCH_VERSION);
3176        match active {
3177            Err(StakeTableError::NoValidValidators) => {},
3178            Err(e) => bail!("Unexpected error: {e}"),
3179            Ok(map) => assert!(!map.contains_key(&val.account)),
3180        }
3181        Ok(())
3182    }
3183
3184    #[test]
3185    fn test_register_v3_sets_x25519_and_p2p() -> anyhow::Result<()> {
3186        let val = TestValidator::random();
3187
3188        let mut state = StakeTableState::default();
3189        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3190
3191        let registered = state.validators().get(&val.account).unwrap();
3192        assert!(registered.authenticated);
3193
3194        let expected_x25519 =
3195            x25519::PublicKey::try_from(val.x25519_key.as_slice()).expect("valid x25519 key");
3196        assert_eq!(registered.x25519_key, Some(expected_x25519));
3197
3198        let expected_p2p: NetAddr = val.p2p_addr.parse().expect("valid p2p addr");
3199        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3200
3201        Ok(())
3202    }
3203
3204    #[test]
3205    fn test_register_v3_invalid_sig() -> anyhow::Result<()> {
3206        let val = TestValidator::random();
3207        let other = TestValidator::random();
3208
3209        // Build a V3 registration with val's keys but other's BLS sig (mismatched)
3210        let bad_val = TestValidator {
3211            bls_sig: other.bls_sig,
3212            ..val.clone()
3213        };
3214        let event = StakeTableEvent::RegisterV3((&bad_val).into());
3215
3216        let mut state = StakeTableState::default();
3217        state.apply_event(event)??;
3218
3219        let registered = state.validators().get(&val.account).unwrap();
3220        assert!(!registered.authenticated);
3221
3222        Ok(())
3223    }
3224
3225    #[test]
3226    fn test_register_v3_hostname_p2p() -> anyhow::Result<()> {
3227        let val = TestValidator::random();
3228
3229        let mut state = StakeTableState::default();
3230        // Empty p2p addr: NetAddr parses it as Name("", 0) which is still Some.
3231        // Non-IP strings become NetAddr::Name variant.
3232        let val = val.with_p2p_addr("my-host:9000");
3233        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3234
3235        let registered = state.validators().get(&val.account).unwrap();
3236        let expected_p2p: NetAddr = "my-host:9000".parse().unwrap();
3237        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3238
3239        Ok(())
3240    }
3241
3242    #[test]
3243    fn test_register_v3_invalid_p2p_addr_degrades_to_none() -> anyhow::Result<()> {
3244        let val = TestValidator::random();
3245
3246        let mut state = StakeTableState::default();
3247        let val = val.with_p2p_addr("host:notaport");
3248        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3249
3250        let registered = state.validators().get(&val.account).unwrap();
3251        assert_eq!(registered.p2p_addr, None);
3252
3253        Ok(())
3254    }
3255
3256    #[test]
3257    fn test_x25519_key_update_sets_value() -> anyhow::Result<()> {
3258        let val = TestValidator::random();
3259
3260        let mut state = StakeTableState::default();
3261        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3262
3263        assert_eq!(
3264            state.validators().get(&val.account).unwrap().x25519_key,
3265            None
3266        );
3267
3268        let x25519_key = [99u8; 32];
3269        state.apply_event(val.x25519_update(x25519_key))??;
3270
3271        let registered = state.validators().get(&val.account).unwrap();
3272        let expected_x25519 =
3273            x25519::PublicKey::try_from(x25519_key.as_slice()).expect("valid x25519 key");
3274        assert_eq!(registered.x25519_key, Some(expected_x25519));
3275
3276        Ok(())
3277    }
3278
3279    #[test]
3280    fn test_p2p_addr_update_sets_value() -> anyhow::Result<()> {
3281        let val = TestValidator::random();
3282
3283        let mut state = StakeTableState::default();
3284        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3285
3286        assert_eq!(state.validators().get(&val.account).unwrap().p2p_addr, None);
3287
3288        let p2p_addr = "10.0.0.1:8080";
3289        state.apply_event(val.p2p_update(p2p_addr))??;
3290
3291        let registered = state.validators().get(&val.account).unwrap();
3292        let expected_p2p: NetAddr = p2p_addr.parse().expect("valid p2p addr");
3293        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3294
3295        Ok(())
3296    }
3297
3298    #[test]
3299    fn test_p2p_addr_unparsable_sets_none() -> anyhow::Result<()> {
3300        let val = TestValidator::random();
3301
3302        let mut state = StakeTableState::default();
3303        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3304
3305        // Set a valid address first.
3306        state.apply_event(val.p2p_update("10.0.0.1:8080"))??;
3307        assert!(
3308            state
3309                .validators()
3310                .get(&val.account)
3311                .unwrap()
3312                .p2p_addr
3313                .is_some()
3314        );
3315
3316        // An address with an invalid port that Rust's NetAddr parser rejects degrades to None.
3317        state.apply_event(val.p2p_update("host:notaport"))??;
3318        assert_eq!(state.validators().get(&val.account).unwrap().p2p_addr, None);
3319
3320        Ok(())
3321    }
3322
3323    #[test]
3324    fn test_x25519_key_update_unknown_validator() {
3325        let mut state = StakeTableState::default();
3326        let unknown = TestValidator::random();
3327
3328        let result = state.apply_event(unknown.x25519_update([1u8; 32]));
3329        assert_matches!(
3330            result,
3331            Err(StakeTableError::ValidatorNotFound(addr)) if addr == unknown.account
3332        );
3333    }
3334
3335    #[test]
3336    fn test_p2p_addr_update_unknown_validator() {
3337        let mut state = StakeTableState::default();
3338        let unknown = TestValidator::random();
3339
3340        let result = state.apply_event(unknown.p2p_update("127.0.0.1:9000"));
3341        assert_matches!(
3342            result,
3343            Err(StakeTableError::ValidatorNotFound(addr)) if addr == unknown.account
3344        );
3345    }
3346
3347    #[test]
3348    fn test_x25519_key_update_duplicate() -> anyhow::Result<()> {
3349        let shared_key = [55u8; 32];
3350        let val1 = TestValidator::random().with_x25519_key(shared_key);
3351        let val2 = TestValidator::random().with_x25519_key([2u8; 32]);
3352
3353        let mut state = StakeTableState::default();
3354        // Register both validators via V3
3355        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3356        state.apply_event(StakeTableEvent::RegisterV3((&val2).into()))??;
3357
3358        // Try to update val2's x25519 key to the same as val1's
3359        let result = state.apply_event(val2.x25519_update(shared_key));
3360        assert_matches!(result, Err(StakeTableError::X25519KeyAlreadyUsed(_)));
3361
3362        Ok(())
3363    }
3364
3365    #[test]
3366    fn test_p2p_addr_update_hostname() -> anyhow::Result<()> {
3367        let val = TestValidator::random();
3368
3369        let mut state = StakeTableState::default();
3370        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3371
3372        state.apply_event(val.p2p_update("my-node.example.com:9000"))??;
3373
3374        let registered = state.validators().get(&val.account).unwrap();
3375        let expected_p2p: NetAddr = "my-node.example.com:9000".parse().unwrap();
3376        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3377
3378        Ok(())
3379    }
3380
3381    #[test]
3382    fn test_register_v3_duplicate_bls_key() -> anyhow::Result<()> {
3383        let val1 = TestValidator::random();
3384        let mut val2 = TestValidator::random();
3385        val2.bls_vk = val1.bls_vk;
3386
3387        let mut state = StakeTableState::default();
3388        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3389        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
3390        assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
3391
3392        Ok(())
3393    }
3394
3395    #[test]
3396    fn test_register_v3_duplicate_schnorr_key() -> anyhow::Result<()> {
3397        let val1 = TestValidator::random();
3398        let mut val2 = TestValidator::random();
3399        val2.schnorr_vk = val1.schnorr_vk;
3400
3401        let mut state = StakeTableState::default();
3402        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3403        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
3404        assert_matches!(result, Err(StakeTableError::SchnorrKeyAlreadyUsed(_)));
3405
3406        Ok(())
3407    }
3408
3409    #[test]
3410    fn test_register_v3_duplicate_x25519_key() -> anyhow::Result<()> {
3411        let shared_x25519 = [7u8; 32];
3412        let val1 = TestValidator::random().with_x25519_key(shared_x25519);
3413        let val2 = TestValidator::random().with_x25519_key(shared_x25519);
3414
3415        let mut state = StakeTableState::default();
3416        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3417        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
3418        assert_matches!(result, Err(StakeTableError::X25519KeyAlreadyUsed(_)));
3419
3420        Ok(())
3421    }
3422
3423    #[test]
3424    fn test_register_v3_rejects_exited_validator() -> anyhow::Result<()> {
3425        let val = TestValidator::random();
3426
3427        let mut state = StakeTableState::default();
3428        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3429        state.apply_event(StakeTableEvent::DeregisterV2(ValidatorExitV2 {
3430            validator: val.account,
3431            unlocksAt: U256::from(1000u64),
3432        }))??;
3433
3434        let result = state.apply_event(StakeTableEvent::RegisterV3((&val).into()));
3435        assert_matches!(
3436            result,
3437            Err(StakeTableError::ValidatorAlreadyExited(addr)) if addr == val.account
3438        );
3439
3440        Ok(())
3441    }
3442
3443    #[test]
3444    fn test_register_v3_zero_x25519_errors() -> anyhow::Result<()> {
3445        let val = TestValidator::random().with_x25519_key([0u8; 32]);
3446
3447        let mut state = StakeTableState::default();
3448        let result = state.apply_event(StakeTableEvent::RegisterV3((&val).into()));
3449        assert_matches!(result, Err(StakeTableError::InvalidX25519Key(_)));
3450
3451        Ok(())
3452    }
3453
3454    /// `used_x25519_keys` must be part of the state commitment. Without this,
3455    /// nodes diverging on x25519 uniqueness tracking would produce identical
3456    /// state hashes and the divergence would go undetected.
3457    #[test]
3458    fn test_state_commit_includes_used_x25519_keys() -> anyhow::Result<()> {
3459        let val = TestValidator::random();
3460
3461        let mut state_without = StakeTableState::default();
3462        state_without.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3463
3464        let mut state_with = state_without.clone();
3465        state_with
3466            .used_x25519_keys
3467            .insert(x25519::PublicKey::try_from([7u8; 32].as_slice()).unwrap());
3468
3469        assert_ne!(state_without.commit(), state_with.commit());
3470        Ok(())
3471    }
3472
3473    // --- NEW_PROTOCOL_VERSION selection filter tests ---
3474
3475    /// Construct a `RegisteredValidator` with both x25519_key and p2p_addr populated.
3476    fn complete_mock_validator() -> RegisteredValidator<BLSPubKey> {
3477        let mut v = RegisteredValidator::<BLSPubKey>::mock();
3478        v.x25519_key = Some(x25519::PublicKey::try_from([42u8; 32].as_slice()).unwrap());
3479        v.p2p_addr = Some("127.0.0.1:9000".parse().unwrap());
3480        v
3481    }
3482
3483    /// Pre-upgrade: validators with missing x25519/p2p fields must still be selected.
3484    #[test]
3485    fn test_select_pre_upgrade_includes_validators_missing_network_info() {
3486        let mut map = RegisteredValidatorMap::new();
3487
3488        let incomplete = RegisteredValidator::<BLSPubKey>::mock();
3489        assert!(incomplete.x25519_key.is_none());
3490        assert!(incomplete.p2p_addr.is_none());
3491        map.insert(incomplete.account, incomplete.clone());
3492
3493        let complete = complete_mock_validator();
3494        map.insert(complete.account, complete.clone());
3495
3496        let active = select_active_validator_set(&map, EPOCH_VERSION).unwrap();
3497        assert!(active.contains_key(&incomplete.account));
3498        assert!(active.contains_key(&complete.account));
3499    }
3500
3501    /// Post-upgrade: validator missing x25519_key is filtered out.
3502    #[test]
3503    fn test_select_post_upgrade_excludes_missing_x25519() {
3504        let mut map = RegisteredValidatorMap::new();
3505
3506        let mut missing_x25519 = complete_mock_validator();
3507        missing_x25519.x25519_key = None;
3508        map.insert(missing_x25519.account, missing_x25519.clone());
3509
3510        let complete = complete_mock_validator();
3511        map.insert(complete.account, complete.clone());
3512
3513        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3514        assert!(!active.contains_key(&missing_x25519.account));
3515        assert!(active.contains_key(&complete.account));
3516    }
3517
3518    /// Post-upgrade: validator missing p2p_addr is filtered out.
3519    #[test]
3520    fn test_select_post_upgrade_excludes_missing_p2p() {
3521        let mut map = RegisteredValidatorMap::new();
3522
3523        let mut missing_p2p = complete_mock_validator();
3524        missing_p2p.p2p_addr = None;
3525        map.insert(missing_p2p.account, missing_p2p.clone());
3526
3527        let complete = complete_mock_validator();
3528        map.insert(complete.account, complete.clone());
3529
3530        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3531        assert!(!active.contains_key(&missing_p2p.account));
3532        assert!(active.contains_key(&complete.account));
3533    }
3534
3535    /// Post-upgrade: a single complete validator is kept.
3536    #[test]
3537    fn test_select_post_upgrade_keeps_complete() {
3538        let mut map = RegisteredValidatorMap::new();
3539        let complete = complete_mock_validator();
3540        map.insert(complete.account, complete.clone());
3541
3542        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3543        assert!(active.contains_key(&complete.account));
3544    }
3545
3546    /// Post-upgrade: if all validators are incomplete, selection returns
3547    /// `NoValidValidators`.
3548    #[test]
3549    fn test_select_post_upgrade_all_incomplete_fails() {
3550        let mut map = RegisteredValidatorMap::new();
3551        for _ in 0..5 {
3552            let v = RegisteredValidator::<BLSPubKey>::mock();
3553            map.insert(v.account, v);
3554        }
3555
3556        let result = select_active_validator_set(&map, NEW_PROTOCOL_VERSION);
3557        assert_matches!(result, Err(StakeTableError::NoValidValidators));
3558    }
3559
3560    /// Post-upgrade: when more than MAX_VALIDATORS complete validators exist alongside
3561    /// incomplete ones, only complete validators appear in the result and the count is
3562    /// clamped to MAX_VALIDATORS.
3563    #[test]
3564    fn test_select_post_upgrade_top_n_only_complete() {
3565        let mut map = RegisteredValidatorMap::new();
3566        let mut complete_accounts = HashSet::new();
3567        let mut incomplete_accounts = HashSet::new();
3568
3569        // Build MAX_VALIDATORS + 50 validators, alternating complete/incomplete. Give
3570        // complete validators higher stake than incomplete ones so both the filter and
3571        // the stake-based top-N truncation cleanly pick complete ones.
3572        for i in 0..(MAX_VALIDATORS + 50) {
3573            if i % 2 == 0 {
3574                let mut v = complete_mock_validator();
3575                v.stake = U256::from(1_000_000_000_u64 + i as u64);
3576                complete_accounts.insert(v.account);
3577                map.insert(v.account, v);
3578            } else {
3579                let mut v = RegisteredValidator::<BLSPubKey>::mock();
3580                v.stake = U256::from(10_u64);
3581                incomplete_accounts.insert(v.account);
3582                map.insert(v.account, v);
3583            }
3584        }
3585
3586        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3587
3588        assert!(
3589            active.len() <= MAX_VALIDATORS,
3590            "active has {} validators, expected at most {}",
3591            active.len(),
3592            MAX_VALIDATORS
3593        );
3594        for addr in active.keys() {
3595            assert!(
3596                complete_accounts.contains(addr),
3597                "incomplete validator {addr:?} ended up in active set"
3598            );
3599            assert!(!incomplete_accounts.contains(addr));
3600        }
3601    }
3602
3603    /// Verify the version boundary: the filter only kicks in at/after
3604    /// `NEW_PROTOCOL_VERSION`. A validator missing the x25519 key must be retained at
3605    /// every prior version and rejected at `NEW_PROTOCOL_VERSION`.
3606    #[test]
3607    fn test_select_version_boundary() {
3608        let mut v = complete_mock_validator();
3609        v.x25519_key = None;
3610        let mut map = RegisteredValidatorMap::new();
3611        map.insert(v.account, v.clone());
3612
3613        for protocol_version in [
3614            EPOCH_VERSION,
3615            DRB_AND_HEADER_UPGRADE_VERSION,
3616            EPOCH_REWARD_VERSION,
3617        ] {
3618            let active = select_active_validator_set(&map, protocol_version).unwrap();
3619            assert!(
3620                active.contains_key(&v.account),
3621                "missing x25519 validator should be included at protocol version \
3622                 {protocol_version}",
3623            );
3624        }
3625
3626        let result = select_active_validator_set(&map, NEW_PROTOCOL_VERSION);
3627        assert_matches!(result, Err(StakeTableError::NoValidValidators));
3628    }
3629
3630    /// Integration test: a RegisterV3 event with an unparsable p2p address results in
3631    /// `p2p_addr: None`, which is accepted pre-upgrade and rejected post-upgrade.
3632    #[test]
3633    fn test_p2p_parse_fail_pre_upgrade_included_post_upgrade_excluded() -> anyhow::Result<()> {
3634        let val = TestValidator::random().with_p2p_addr("host:notaport");
3635
3636        let mut state = StakeTableState::default();
3637        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3638        // Delegate so the validator passes the stake/delegator checks.
3639        state.apply_event(StakeTableEvent::Delegate(Delegated {
3640            delegator: Address::random(),
3641            validator: val.account,
3642            amount: U256::from(100),
3643        }))??;
3644
3645        let registered = state.validators().get(&val.account).unwrap();
3646        assert_eq!(registered.p2p_addr, None);
3647
3648        // Pre-upgrade: the validator is selected.
3649        let pre = select_active_validator_set(state.validators(), EPOCH_VERSION).unwrap();
3650        assert!(pre.contains_key(&val.account));
3651
3652        // Post-upgrade: the sole validator is filtered out, so selection fails.
3653        let post = select_active_validator_set(state.validators(), NEW_PROTOCOL_VERSION);
3654        assert_matches!(post, Err(StakeTableError::NoValidValidators));
3655
3656        Ok(())
3657    }
3658}