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