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