espresso_types/v0/impls/
stake_table.rs

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