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