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 vbs::version::StaticVersionType;
61
62#[cfg(any(test, feature = "testing"))]
63use super::v0_3::DAMembers;
64use super::{
65 traits::{MembershipPersistence, StateCatchup},
66 v0_3::{ChainConfig, EventKey, Fetcher, StakeTableEvent, StakeTableUpdateTask, Validator},
67 Header, L1Client, Leaf2, PubKey, SeqTypes,
68};
69use crate::{
70 traits::EventsPersistenceRead,
71 v0_1::L1Provider,
72 v0_3::{
73 EventSortingError, ExpectedStakeTableError, FetchRewardError, RewardAmount,
74 StakeTableError, ASSUMED_BLOCK_TIME_SECONDS, BLOCKS_PER_YEAR, COMMISSION_BASIS_POINTS,
75 INFLATION_RATE, MILLISECONDS_PER_YEAR,
76 },
77 DrbAndHeaderUpgradeVersion, EpochVersion,
78};
79
80type Epoch = <SeqTypes as NodeType>::Epoch;
81pub type ValidatorMap = IndexMap<Address, Validator<BLSPubKey>>;
82
83pub type StakeTableHash = Commitment<StakeTableState>;
84
85type ApplyEventResult<T> = Result<Result<T, ExpectedStakeTableError>, StakeTableError>;
90
91trait DisplayLog {
93 fn display(&self) -> String;
94}
95
96impl DisplayLog for Log {
97 fn display(&self) -> String {
98 let block = self.block_number.unwrap_or_default();
102 let index = self.log_index.unwrap_or_default();
103 let hash = self.transaction_hash.unwrap_or_default();
104 format!("Log(block={block},index={index},transaction_hash={hash})")
105 }
106}
107
108impl TryFrom<StakeTableV2Events> for StakeTableEvent {
109 type Error = anyhow::Error;
110
111 fn try_from(value: StakeTableV2Events) -> anyhow::Result<Self> {
112 match value {
113 StakeTableV2Events::ValidatorRegistered(v) => Ok(StakeTableEvent::Register(v)),
114 StakeTableV2Events::ValidatorRegisteredV2(v) => Ok(StakeTableEvent::RegisterV2(v)),
115 StakeTableV2Events::ValidatorExit(v) => Ok(StakeTableEvent::Deregister(v)),
116 StakeTableV2Events::ValidatorExitV2(v) => Ok(StakeTableEvent::DeregisterV2(v)),
117 StakeTableV2Events::Delegated(v) => Ok(StakeTableEvent::Delegate(v)),
118 StakeTableV2Events::Undelegated(v) => Ok(StakeTableEvent::Undelegate(v)),
119 StakeTableV2Events::UndelegatedV2(v) => Ok(StakeTableEvent::UndelegateV2(v)),
120 StakeTableV2Events::ConsensusKeysUpdated(v) => Ok(StakeTableEvent::KeyUpdate(v)),
121 StakeTableV2Events::ConsensusKeysUpdatedV2(v) => Ok(StakeTableEvent::KeyUpdateV2(v)),
122 StakeTableV2Events::CommissionUpdated(v) => Ok(StakeTableEvent::CommissionUpdate(v)),
123 StakeTableV2Events::ExitEscrowPeriodUpdated(v) => Err(anyhow::anyhow!(
124 "Unsupported StakeTableV2Events::ExitEscrowPeriodUpdated({v:?})"
125 )),
126 StakeTableV2Events::Initialized(v) => Err(anyhow::anyhow!(
127 "Unsupported StakeTableV2Events::Initialized({v:?})"
128 )),
129 StakeTableV2Events::MaxCommissionIncreaseUpdated(v) => Err(anyhow::anyhow!(
130 "Unsupported StakeTableV2Events::MaxCommissionIncreaseUpdated({v:?})"
131 )),
132 StakeTableV2Events::MinCommissionUpdateIntervalUpdated(v) => Err(anyhow::anyhow!(
133 "Unsupported StakeTableV2Events::MinCommissionUpdateIntervalUpdated({v:?})"
134 )),
135 StakeTableV2Events::OwnershipTransferred(v) => Err(anyhow::anyhow!(
136 "Unsupported StakeTableV2Events::OwnershipTransferred({v:?})"
137 )),
138 StakeTableV2Events::Paused(v) => Err(anyhow::anyhow!(
139 "Unsupported StakeTableV2Events::Paused({v:?})"
140 )),
141 StakeTableV2Events::RoleAdminChanged(v) => Err(anyhow::anyhow!(
142 "Unsupported StakeTableV2Events::RoleAdminChanged({v:?})"
143 )),
144 StakeTableV2Events::RoleGranted(v) => Err(anyhow::anyhow!(
145 "Unsupported StakeTableV2Events::RoleGranted({v:?})"
146 )),
147 StakeTableV2Events::RoleRevoked(v) => Err(anyhow::anyhow!(
148 "Unsupported StakeTableV2Events::RoleRevoked({v:?})"
149 )),
150 StakeTableV2Events::Unpaused(v) => Err(anyhow::anyhow!(
151 "Unsupported StakeTableV2Events::Unpaused({v:?})"
152 )),
153 StakeTableV2Events::Upgraded(v) => Err(anyhow::anyhow!(
154 "Unsupported StakeTableV2Events::Upgraded({v:?})"
155 )),
156 StakeTableV2Events::WithdrawalClaimed(v) => Err(anyhow::anyhow!(
157 "Unsupported StakeTableV2Events::WithdrawalClaimed({v:?})"
158 )),
159 StakeTableV2Events::ValidatorExitClaimed(v) => Err(anyhow::anyhow!(
160 "Unsupported StakeTableV2Events::ValidatorExitClaimed({v:?})"
161 )),
162 StakeTableV2Events::Withdrawal(v) => Err(anyhow::anyhow!(
163 "Unsupported StakeTableV2Events::Withdrawal({v:?})"
164 )),
165 StakeTableV2Events::MetadataUriUpdated(v) => Err(anyhow::anyhow!(
166 "Unsupported StakeTableV2Events::MetadataUriUpdated({v:?})"
167 )),
168 StakeTableV2Events::MinDelegateAmountUpdated(v) => Err(anyhow::anyhow!(
169 "Unsupported StakeTableV2Events::MinDelegateAmountUpdated({v:?})"
170 )),
171 }
172 }
173}
174
175fn sort_stake_table_events(
176 event_logs: Vec<(StakeTableV2Events, Log)>,
177) -> Result<Vec<(EventKey, StakeTableEvent)>, EventSortingError> {
178 let mut events: Vec<(EventKey, StakeTableEvent)> = Vec::new();
179
180 let key = |log: &Log| -> Result<EventKey, EventSortingError> {
181 let block_number = log
182 .block_number
183 .ok_or(EventSortingError::MissingBlockNumber)?;
184 let log_index = log.log_index.ok_or(EventSortingError::MissingLogIndex)?;
185 Ok((block_number, log_index))
186 };
187
188 for (e, log) in event_logs {
189 let k = key(&log)?;
190 let evt: StakeTableEvent = e
191 .try_into()
192 .map_err(|_| EventSortingError::InvalidStakeTableV2Event)?;
193 events.push((k, evt));
194 }
195
196 events.sort_by_key(|(key, _)| *key);
197 Ok(events)
198}
199
200#[derive(Clone, Debug, Default, PartialEq)]
201pub struct StakeTableState {
202 validators: ValidatorMap,
203 validator_exits: HashSet<Address>,
204 used_bls_keys: HashSet<BLSPubKey>,
205 used_schnorr_keys: HashSet<SchnorrPubKey>,
206}
207
208impl Committable for StakeTableState {
209 fn commit(&self) -> committable::Commitment<Self> {
210 let mut builder = RawCommitmentBuilder::new(&Self::tag());
211
212 for (_, validator) in self.validators.iter().sorted_by_key(|(a, _)| *a) {
213 builder = builder.field("validator", validator.commit());
214 }
215
216 builder = builder.constant_str("used_bls_keys");
217 for key in self.used_bls_keys.iter().sorted() {
218 builder = builder.var_size_bytes(&key.to_bytes());
219 }
220
221 builder = builder.constant_str("used_schnorr_keys");
222 for key in self
223 .used_schnorr_keys
224 .iter()
225 .sorted_by(|a, b| a.to_affine().xy().cmp(&b.to_affine().xy()))
226 {
227 let mut schnorr_key_bytes = vec![];
228 key.serialize_with_mode(&mut schnorr_key_bytes, ark_serialize::Compress::Yes)
229 .unwrap();
230 builder = builder.var_size_bytes(&schnorr_key_bytes);
231 }
232
233 builder = builder.constant_str("validator_exits");
234
235 for key in self.validator_exits.iter().sorted() {
236 builder = builder.fixed_size_bytes(&key.into_array());
237 }
238
239 builder.finalize()
240 }
241
242 fn tag() -> String {
243 "STAKE_TABLE".to_string()
244 }
245}
246
247impl StakeTableState {
248 pub fn new(
249 validators: ValidatorMap,
250 validator_exits: HashSet<Address>,
251 used_bls_keys: HashSet<BLSPubKey>,
252 used_schnorr_keys: HashSet<SchnorrPubKey>,
253 ) -> Self {
254 Self {
255 validators,
256 validator_exits,
257 used_bls_keys,
258 used_schnorr_keys,
259 }
260 }
261
262 pub fn validators(&self) -> &ValidatorMap {
263 &self.validators
264 }
265
266 pub fn into_validators(self) -> ValidatorMap {
267 self.validators
268 }
269
270 pub fn used_bls_keys(&self) -> &HashSet<BLSPubKey> {
271 &self.used_bls_keys
272 }
273
274 pub fn used_schnorr_keys(&self) -> &HashSet<SchnorrPubKey> {
275 &self.used_schnorr_keys
276 }
277
278 pub fn validator_exits(&self) -> &HashSet<Address> {
279 &self.validator_exits
280 }
281
282 pub fn apply_event(&mut self, event: StakeTableEvent) -> ApplyEventResult<()> {
288 match event {
289 StakeTableEvent::Register(ValidatorRegistered {
290 account,
291 blsVk,
292 schnorrVk,
293 commission,
294 }) => {
295 let stake_table_key: BLSPubKey = blsVk.into();
296 let state_ver_key: SchnorrPubKey = schnorrVk.into();
297
298 if self.validator_exits.contains(&account) {
299 return Err(StakeTableError::ValidatorAlreadyExited(account));
300 }
301
302 let entry = self.validators.entry(account);
303 if let indexmap::map::Entry::Occupied(_) = entry {
304 return Err(StakeTableError::AlreadyRegistered(account));
305 }
306
307 if self.used_bls_keys.contains(&stake_table_key) {
309 return Err(StakeTableError::BlsKeyAlreadyUsed(
310 stake_table_key.to_string(),
311 ));
312 }
313
314 if self.used_schnorr_keys.contains(&state_ver_key) {
316 return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
317 state_ver_key.to_string(),
318 )));
319 }
320
321 self.used_bls_keys.insert(stake_table_key);
323 self.used_schnorr_keys.insert(state_ver_key.clone());
324
325 entry.or_insert(Validator {
326 account,
327 stake_table_key,
328 state_ver_key,
329 stake: U256::ZERO,
330 commission,
331 delegators: HashMap::new(),
332 });
333 },
334
335 StakeTableEvent::RegisterV2(reg) => {
336 reg.authenticate()
339 .map_err(|e| StakeTableError::AuthenticationFailed(e.to_string()))?;
340
341 let ValidatorRegisteredV2 {
342 account,
343 blsVK,
344 schnorrVK,
345 commission,
346 ..
347 } = reg;
348
349 let stake_table_key: BLSPubKey = blsVK.into();
350 let state_ver_key: SchnorrPubKey = schnorrVK.into();
351
352 if self.validator_exits.contains(&account) {
354 return Err(StakeTableError::ValidatorAlreadyExited(account));
355 }
356
357 let entry = self.validators.entry(account);
358 if let indexmap::map::Entry::Occupied(_) = entry {
359 return Err(StakeTableError::AlreadyRegistered(account));
360 }
361
362 if self.used_bls_keys.contains(&stake_table_key) {
364 return Err(StakeTableError::BlsKeyAlreadyUsed(
365 stake_table_key.to_string(),
366 ));
367 }
368
369 if self.used_schnorr_keys.contains(&state_ver_key) {
371 return Err(StakeTableError::SchnorrKeyAlreadyUsed(
372 state_ver_key.to_string(),
373 ));
374 }
375
376 self.used_bls_keys.insert(stake_table_key);
378 self.used_schnorr_keys.insert(state_ver_key.clone());
379
380 entry.or_insert(Validator {
381 account,
382 stake_table_key,
383 state_ver_key,
384 stake: U256::ZERO,
385 commission,
386 delegators: HashMap::new(),
387 });
388 },
389
390 StakeTableEvent::Deregister(ValidatorExit { validator })
391 | StakeTableEvent::DeregisterV2(ValidatorExitV2 { validator, .. }) => {
392 if !self.validators.contains_key(&validator) {
393 return Err(StakeTableError::ValidatorNotFound(validator));
394 }
395
396 self.validator_exits.insert(validator);
398 self.validators.shift_remove(&validator);
399 },
400
401 StakeTableEvent::Delegate(delegated) => {
402 let Delegated {
403 delegator,
404 validator,
405 amount,
406 } = delegated;
407
408 if amount.is_zero() {
410 return Err(StakeTableError::ZeroDelegatorStake(delegator));
411 }
412
413 let val = self
414 .validators
415 .get_mut(&validator)
416 .ok_or(StakeTableError::ValidatorNotFound(validator))?;
417
418 val.stake = val.stake.checked_add(amount).unwrap_or_else(|| {
421 panic!(
422 "validator stake overflow: validator={validator}, stake={}, \
423 amount={amount}",
424 val.stake
425 )
426 });
427 val.delegators
430 .entry(delegator)
431 .and_modify(|stake| {
432 *stake = stake.checked_add(amount).unwrap_or_else(|| {
433 panic!(
434 "delegator stake overflow: delegator={delegator}, stake={stake}, \
435 amount={amount}"
436 )
437 });
438 })
439 .or_insert(amount);
440 },
441
442 StakeTableEvent::Undelegate(Undelegated {
443 delegator,
444 validator,
445 amount,
446 })
447 | StakeTableEvent::UndelegateV2(UndelegatedV2 {
448 delegator,
449 validator,
450 amount,
451 ..
452 }) => {
453 let val = self
454 .validators
455 .get_mut(&validator)
456 .ok_or(StakeTableError::ValidatorNotFound(validator))?;
457
458 if val.stake < amount {
459 tracing::warn!("validator_stake={}, amount={amount}", val.stake);
460 return Err(StakeTableError::InsufficientStake);
461 }
462
463 let delegator_stake = val
464 .delegators
465 .get_mut(&delegator)
466 .ok_or(StakeTableError::DelegatorNotFound(delegator))?;
467
468 if *delegator_stake < amount {
469 tracing::warn!("delegator_stake={delegator_stake}, amount={amount}");
470 return Err(StakeTableError::InsufficientStake);
471 }
472
473 let new_delegator_stake = delegator_stake.checked_sub(amount).unwrap();
475
476 val.stake = val.stake.checked_sub(amount).unwrap();
479
480 if new_delegator_stake.is_zero() {
481 val.delegators.remove(&delegator);
482 } else {
483 *delegator_stake = new_delegator_stake;
484 }
485 },
486
487 StakeTableEvent::KeyUpdate(update) => {
488 let ConsensusKeysUpdated {
489 account,
490 blsVK,
491 schnorrVK,
492 } = update;
493
494 let stake_table_key: BLSPubKey = blsVK.into();
495 let state_ver_key: SchnorrPubKey = schnorrVK.into();
496
497 if !self.validators.contains_key(&account) {
498 return Err(StakeTableError::ValidatorNotFound(account));
499 }
500
501 if self.used_bls_keys.contains(&stake_table_key) {
502 return Err(StakeTableError::BlsKeyAlreadyUsed(
503 stake_table_key.to_string(),
504 ));
505 }
506
507 if self.used_schnorr_keys.contains(&state_ver_key) {
510 return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
511 state_ver_key.to_string(),
512 )));
513 }
514
515 self.used_bls_keys.insert(stake_table_key);
517 self.used_schnorr_keys.insert(state_ver_key.clone());
518 let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
520 panic!("validator {account} must exist after contains_key check")
521 });
522 validator.stake_table_key = stake_table_key;
523 validator.state_ver_key = state_ver_key;
524 },
525
526 StakeTableEvent::KeyUpdateV2(update) => {
527 update
530 .authenticate()
531 .map_err(|e| StakeTableError::AuthenticationFailed(e.to_string()))?;
532
533 let ConsensusKeysUpdatedV2 {
534 account,
535 blsVK,
536 schnorrVK,
537 ..
538 } = update;
539
540 let stake_table_key: BLSPubKey = blsVK.into();
541 let state_ver_key: SchnorrPubKey = schnorrVK.into();
542
543 if !self.validators.contains_key(&account) {
544 return Err(StakeTableError::ValidatorNotFound(account));
545 }
546
547 if self.used_bls_keys.contains(&stake_table_key) {
549 return Err(StakeTableError::BlsKeyAlreadyUsed(
550 stake_table_key.to_string(),
551 ));
552 }
553
554 if self.used_schnorr_keys.contains(&state_ver_key) {
556 return Err(StakeTableError::SchnorrKeyAlreadyUsed(
557 state_ver_key.to_string(),
558 ));
559 }
560
561 self.used_bls_keys.insert(stake_table_key);
563 self.used_schnorr_keys.insert(state_ver_key.clone());
564
565 let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
567 panic!("validator {account} must exist after contains_key check")
568 });
569 validator.stake_table_key = stake_table_key;
570 validator.state_ver_key = state_ver_key;
571 },
572
573 StakeTableEvent::CommissionUpdate(CommissionUpdated {
574 validator,
575 newCommission,
576 ..
577 }) => {
578 if newCommission > COMMISSION_BASIS_POINTS {
581 return Err(StakeTableError::InvalidCommission(validator, newCommission));
582 }
583
584 let val = self
588 .validators
589 .get_mut(&validator)
590 .ok_or(StakeTableError::ValidatorNotFound(validator))?;
591 val.commission = newCommission;
592 },
593 }
594
595 Ok(Ok(()))
596 }
597}
598
599pub fn validators_from_l1_events<I: Iterator<Item = StakeTableEvent>>(
600 events: I,
601) -> Result<(ValidatorMap, StakeTableHash), StakeTableError> {
602 let mut state = StakeTableState::default();
603 for event in events {
604 match state.apply_event(event.clone()) {
605 Ok(Ok(())) => {
606 },
608 Ok(Err(expected_err)) => {
609 tracing::warn!("Expected error while applying event {event:?}: {expected_err}");
611 },
612 Err(err) => {
613 tracing::error!("Fatal error in applying event {event:?}: {err}");
614 return Err(err);
615 },
616 }
617 }
618 let commit = state.commit();
619 Ok((state.into_validators(), commit))
620}
621
622pub fn select_active_validator_set(
627 validators: &ValidatorMap,
628) -> Result<ValidatorMap, StakeTableError> {
629 let total_validators = validators.len();
630
631 let valid_validators: ValidatorMap = validators
633 .iter()
634 .filter(|(address, validator)| {
635 if validator.delegators.is_empty() {
636 tracing::info!("Validator {address:?} does not have any delegator");
637 return false;
638 }
639
640 if validator.stake.is_zero() {
641 tracing::info!("Validator {address:?} does not have any stake");
642 return false;
643 }
644
645 true
646 })
647 .map(|(addr, val)| (*addr, val.clone()))
648 .collect();
649
650 tracing::debug!(
651 total_validators,
652 filtered = valid_validators.len(),
653 "Filtered out invalid validators"
654 );
655
656 if valid_validators.is_empty() {
657 tracing::warn!("Validator selection failed: no validators passed minimum criteria");
658 return Err(StakeTableError::NoValidValidators);
659 }
660
661 let maximum_stake = valid_validators.values().map(|v| v.stake).max().unwrap();
662
663 let minimum_stake = maximum_stake
664 .checked_div(U256::from(VID_TARGET_TOTAL_STAKE))
665 .ok_or_else(|| {
666 tracing::error!("Overflow while calculating minimum stake threshold");
667 StakeTableError::MinimumStakeOverflow
668 })?;
669
670 let mut valid_stakers: Vec<_> = valid_validators
671 .iter()
672 .filter(|(_, v)| v.stake >= minimum_stake)
673 .map(|(addr, v)| (*addr, v.stake))
674 .collect();
675
676 tracing::info!(
677 count = valid_stakers.len(),
678 "Number of validators above minimum stake threshold"
679 );
680
681 valid_stakers.sort_by_key(|(_, stake)| std::cmp::Reverse(*stake));
683
684 if valid_stakers.len() > 100 {
685 valid_stakers.truncate(100);
686 }
687
688 let selected_addresses: HashSet<_> = valid_stakers.iter().map(|(addr, _)| *addr).collect();
689 let selected_validators: ValidatorMap = valid_validators
690 .into_iter()
691 .filter(|(address, _)| selected_addresses.contains(address))
692 .collect();
693
694 tracing::info!(
695 final_count = selected_validators.len(),
696 "Selected active validator set"
697 );
698
699 Ok(selected_validators)
700}
701
702#[derive(Clone, Debug)]
703pub struct ValidatorSet {
704 pub all_validators: ValidatorMap,
705 pub active_validators: ValidatorMap,
706 pub stake_table_hash: Option<StakeTableHash>,
707}
708
709pub(crate) fn validator_set_from_l1_events<I: Iterator<Item = StakeTableEvent>>(
711 events: I,
712) -> Result<ValidatorSet, StakeTableError> {
713 let (all_validators, stake_table_hash) = validators_from_l1_events(events)?;
714 let active_validators = select_active_validator_set(&all_validators)?;
715
716 let validator_set = ValidatorSet {
717 all_validators,
718 active_validators,
719 stake_table_hash: Some(stake_table_hash),
720 };
721
722 Ok(validator_set)
723}
724
725impl std::fmt::Debug for StakeTableEvent {
726 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
727 match self {
728 StakeTableEvent::Register(event) => write!(f, "Register({:?})", event.account),
729 StakeTableEvent::RegisterV2(event) => write!(f, "RegisterV2({:?})", event.account),
730 StakeTableEvent::Deregister(event) => write!(f, "Deregister({:?})", event.validator),
731 StakeTableEvent::DeregisterV2(event) => {
732 write!(f, "DeregisterV2({:?})", event.validator)
733 },
734 StakeTableEvent::Delegate(event) => write!(f, "Delegate({:?})", event.delegator),
735 StakeTableEvent::Undelegate(event) => write!(f, "Undelegate({:?})", event.delegator),
736 StakeTableEvent::UndelegateV2(event) => {
737 write!(f, "UndelegateV2({:?})", event.delegator)
738 },
739 StakeTableEvent::KeyUpdate(event) => write!(f, "KeyUpdate({:?})", event.account),
740 StakeTableEvent::KeyUpdateV2(event) => write!(f, "KeyUpdateV2({:?})", event.account),
741 StakeTableEvent::CommissionUpdate(event) => {
742 write!(f, "CommissionUpdate({:?})", event.validator)
743 },
744 }
745 }
746}
747
748#[derive(Clone, derive_more::derive::Debug)]
749pub struct EpochCommittees {
751 non_epoch_committee: NonEpochCommittee,
753 state: HashMap<Epoch, EpochCommittee>,
755 all_validators: BTreeMap<Epoch, ValidatorMap>,
757 randomized_committees: BTreeMap<Epoch, RandomizedCommittee<StakeTableEntry<PubKey>>>,
759 da_committees: BTreeMap<u64, DaCommittee>,
761 first_epoch: Option<Epoch>,
762 epoch_height: u64,
763 fixed_block_reward: Option<RewardAmount>,
766 fetcher: Arc<Fetcher>,
767}
768
769#[derive(Debug, Clone)]
770struct DaCommittee {
771 committee: Vec<PeerConfig<SeqTypes>>,
772 indexed_committee: HashMap<PubKey, PeerConfig<SeqTypes>>,
773}
774
775impl Fetcher {
776 pub fn new(
777 peers: Arc<dyn StateCatchup>,
778 persistence: Arc<Mutex<dyn MembershipPersistence>>,
779 l1_client: L1Client,
780 chain_config: ChainConfig,
781 ) -> Self {
782 Self {
783 peers,
784 persistence,
785 l1_client,
786 chain_config: Arc::new(Mutex::new(chain_config)),
787 update_task: StakeTableUpdateTask(Mutex::new(None)).into(),
788 initial_supply: Arc::new(RwLock::new(None)),
789 }
790 }
791
792 pub async fn spawn_update_loop(&self) {
793 let mut update_task = self.update_task.0.lock().await;
794 if update_task.is_none() {
795 *update_task = Some(spawn(self.update_loop()));
796 }
797 }
798
799 fn update_loop(&self) -> impl Future<Output = ()> {
804 let span = tracing::warn_span!("Stake table update loop");
805 let self_clone = self.clone();
806 let state = self.l1_client.state.clone();
807 let l1_retry = self.l1_client.options().l1_retry_delay;
808 let update_delay = self.l1_client.options().stake_table_update_interval;
809 let chain_config = self.chain_config.clone();
810
811 async move {
812 let stake_contract_address = loop {
817 let contract = chain_config.lock().await.stake_table_contract;
818 match contract {
819 Some(addr) => break addr,
820 None => {
821 tracing::debug!(
822 "Stake table contract address not found. Retrying in {l1_retry:?}...",
823 );
824 },
825 }
826 sleep(l1_retry).await;
827 };
828
829 loop {
831 let finalized_block = loop {
832 let last_finalized = state.lock().await.last_finalized;
833 if let Some(block) = last_finalized {
834 break block;
835 }
836 tracing::debug!("Finalized block not yet available. Retrying in {l1_retry:?}",);
837 sleep(l1_retry).await;
838 };
839
840 tracing::debug!("Attempting to fetch stake table at L1 block {finalized_block:?}",);
841
842 loop {
843 match self_clone
844 .fetch_and_store_stake_table_events(stake_contract_address, finalized_block)
845 .await
846 {
847 Ok(events) => {
848 tracing::info!(
849 "Successfully fetched and stored stake table events at \
850 block={finalized_block:?}"
851 );
852 tracing::debug!("events={events:?}");
853 break;
854 },
855 Err(e) => {
856 tracing::error!(
857 "Error fetching stake table at block {finalized_block:?}. err= \
858 {e:#}",
859 );
860 sleep(l1_retry).await;
861 },
862 }
863 }
864
865 tracing::debug!("Waiting {update_delay:?} before next stake table update...",);
866 sleep(update_delay).await;
867 }
868 }
869 .instrument(span)
870 }
871
872 pub async fn fetch_and_store_stake_table_events(
879 &self,
880 contract: Address,
881 to_block: u64,
882 ) -> anyhow::Result<Vec<(EventKey, StakeTableEvent)>> {
883 let (read_l1_offset, persistence_events) = {
884 let persistence_lock = self.persistence.lock().await;
885 persistence_lock.load_events(0, to_block).await?
886 };
887
888 tracing::info!("loaded events from storage to_block={to_block:?}");
889
890 if let Some(EventsPersistenceRead::Complete) = read_l1_offset {
893 return Ok(persistence_events);
894 }
895
896 let from_block = read_l1_offset
897 .map(|read| match read {
898 EventsPersistenceRead::UntilL1Block(block) => Ok(block + 1),
899 EventsPersistenceRead::Complete => Err(anyhow::anyhow!(
900 "Unexpected state. offset is complete after returning early"
901 )),
902 })
903 .transpose()?;
904
905 ensure!(
906 Some(to_block) >= from_block,
907 "to_block {to_block:?} is less than from_block {from_block:?}"
908 );
909
910 tracing::info!(%to_block, from_block = ?from_block, "Fetching events from contract");
911
912 let contract_events = Self::fetch_events_from_contract(
913 self.l1_client.clone(),
914 contract,
915 from_block,
916 to_block,
917 )
918 .await?;
919
920 if !contract_events.is_empty() {
922 tracing::info!(
923 "storing {} new events in storage to_block={to_block:?}",
924 contract_events.len()
925 );
926 {
927 let persistence_lock = self.persistence.lock().await;
928 persistence_lock
929 .store_events(to_block, contract_events.clone())
930 .await
931 .inspect_err(|e| tracing::error!("failed to store events. err={e}"))?;
932 }
933 }
934
935 let mut events = match from_block {
936 Some(_) => persistence_events
937 .into_iter()
938 .chain(contract_events)
939 .collect(),
940 None => contract_events,
941 };
942
943 let len_before_dedup = events.len();
948 events.dedup();
949 let len_after_dedup = events.len();
950 if len_before_dedup != len_after_dedup {
951 tracing::warn!("Duplicate events found and removed. This should not normally happen.")
952 }
953
954 Ok(events)
955 }
956
957 fn validate_event(event: &StakeTableV2Events, log: &Log) -> Result<bool, StakeTableError> {
964 match event {
965 StakeTableV2Events::ValidatorRegisteredV2(evt) => {
966 if let Err(err) = evt.authenticate() {
967 tracing::warn!(
968 %err,
969 "Failed to authenticate ValidatorRegisteredV2 event: {}",
970 log.display()
971 );
972 return Ok(false);
973 }
974 },
975 StakeTableV2Events::ConsensusKeysUpdatedV2(evt) => {
976 if let Err(err) = evt.authenticate() {
977 tracing::warn!(
978 %err,
979 "Failed to authenticate ConsensusKeysUpdatedV2 event: {}",
980 log.display()
981 );
982 return Ok(false);
983 }
984 },
985 StakeTableV2Events::CommissionUpdated(CommissionUpdated {
986 validator,
987 newCommission,
988 ..
989 }) => {
990 if *newCommission > COMMISSION_BASIS_POINTS {
991 return Err(StakeTableError::InvalidCommission(
992 *validator,
993 *newCommission,
994 ));
995 }
996 },
997 _ => {},
998 }
999
1000 Ok(true)
1001 }
1002
1003 fn block_range_chunks(
1005 from_block: u64,
1006 to_block: u64,
1007 chunk_size: u64,
1008 ) -> impl Iterator<Item = (u64, u64)> {
1009 let mut start = from_block;
1010 let end = to_block;
1011 std::iter::from_fn(move || {
1012 let chunk_end = min(start + chunk_size - 1, end);
1013 if chunk_end < start {
1014 return None;
1015 }
1016 let chunk = (start, chunk_end);
1017 start = chunk_end + 1;
1018 Some(chunk)
1019 })
1020 }
1021
1022 pub async fn fetch_events_from_contract(
1024 l1_client: L1Client,
1025 contract: Address,
1026 from_block: Option<u64>,
1027 to_block: u64,
1028 ) -> Result<Vec<(EventKey, StakeTableEvent)>, StakeTableError> {
1029 let stake_table_contract = StakeTableV2::new(contract, l1_client.provider.clone());
1030 let max_retry_duration = l1_client.options().l1_events_max_retry_duration;
1031 let retry_delay = l1_client.options().l1_retry_delay;
1032 let from_block = match from_block {
1035 Some(block) => block,
1036 None => {
1037 let start = Instant::now();
1038 loop {
1039 match stake_table_contract.initializedAtBlock().call().await {
1040 Ok(init_block) => break init_block.to::<u64>(),
1041 Err(err) => {
1042 if start.elapsed() >= max_retry_duration {
1043 panic!(
1044 "Failed to retrieve initial block after `{}`: {err}",
1045 format_duration(max_retry_duration)
1046 );
1047 }
1048 tracing::warn!(%err, "Failed to retrieve initial block, retrying...");
1049 sleep(retry_delay).await;
1050 },
1051 }
1052 }
1053 },
1054 };
1055
1056 let chunk_size = l1_client.options().l1_events_max_block_range;
1060 let chunks = Self::block_range_chunks(from_block, to_block, chunk_size);
1061
1062 let mut events = vec![];
1063
1064 for (from, to) in chunks {
1065 let provider = l1_client.provider.clone();
1066
1067 tracing::debug!(from, to, "fetch all stake table events in range");
1068 let logs: Vec<Log> = retry(
1071 retry_delay,
1072 max_retry_duration,
1073 "stake table events fetch",
1074 move || {
1075 let provider = provider.clone();
1076
1077 Box::pin(async move {
1078 let filter = Filter::new()
1079 .events([
1080 ValidatorRegistered::SIGNATURE,
1081 ValidatorRegisteredV2::SIGNATURE,
1082 ValidatorExit::SIGNATURE,
1083 ValidatorExitV2::SIGNATURE,
1084 Delegated::SIGNATURE,
1085 Undelegated::SIGNATURE,
1086 UndelegatedV2::SIGNATURE,
1087 ConsensusKeysUpdated::SIGNATURE,
1088 ConsensusKeysUpdatedV2::SIGNATURE,
1089 CommissionUpdated::SIGNATURE,
1090 ])
1091 .address(contract)
1092 .from_block(from)
1093 .to_block(to);
1094 provider.get_logs(&filter).await
1095 })
1096 },
1097 )
1098 .await;
1099
1100 let chunk_events = logs
1101 .into_iter()
1102 .filter_map(|log| {
1103 let event =
1104 StakeTableV2Events::decode_raw_log(log.topics(), &log.data().data).ok()?;
1105 match Self::validate_event(&event, &log) {
1106 Ok(true) => Some(Ok((event, log))),
1107 Ok(false) => None,
1108 Err(e) => Some(Err(e)),
1109 }
1110 })
1111 .collect::<Result<Vec<_>, _>>()?;
1112
1113 events.extend(chunk_events);
1114 }
1115
1116 sort_stake_table_events(events).map_err(Into::into)
1117 }
1118
1119 pub async fn fetch_all_validators_from_contract(
1121 l1_client: L1Client,
1122 contract: Address,
1123 to_block: u64,
1124 ) -> anyhow::Result<(ValidatorMap, StakeTableHash)> {
1125 let events = Self::fetch_events_from_contract(l1_client, contract, None, to_block).await?;
1126
1127 validators_from_l1_events(events.into_iter().map(|(_, e)| e))
1129 .context("failed to construct validators set from l1 events")
1130 }
1131
1132 pub async fn fetch_fixed_block_reward(&self) -> Result<RewardAmount, FetchRewardError> {
1136 let initial_supply = *self.initial_supply.read().await;
1138 let initial_supply = match initial_supply {
1139 Some(supply) => supply,
1140 None => self.fetch_and_update_initial_supply().await?,
1141 };
1142
1143 let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
1144 .checked_div(U256::from(COMMISSION_BASIS_POINTS))
1145 .ok_or(FetchRewardError::DivisionByZero(
1146 "COMMISSION_BASIS_POINTS is zero",
1147 ))?;
1148
1149 Ok(RewardAmount(reward))
1150 }
1151
1152 pub async fn fetch_and_update_initial_supply(&self) -> Result<U256, FetchRewardError> {
1169 tracing::info!("Fetching token initial supply");
1170 let chain_config = *self.chain_config.lock().await;
1171
1172 let stake_table_contract = chain_config
1173 .stake_table_contract
1174 .ok_or(FetchRewardError::MissingStakeTableContract)?;
1175
1176 let provider = self.l1_client.provider.clone();
1177 let stake_table = StakeTableV2::new(stake_table_contract, provider.clone());
1178
1179 let stake_table_init_block = stake_table
1183 .initializedAtBlock()
1184 .block(BlockId::finalized())
1185 .call()
1186 .await
1187 .map_err(FetchRewardError::ContractCall)?
1188 .to::<u64>();
1189
1190 tracing::info!("stake table init block ={stake_table_init_block}");
1191
1192 let token_address = stake_table
1193 .token()
1194 .block(BlockId::finalized())
1195 .call()
1196 .await
1197 .map_err(FetchRewardError::TokenAddressFetch)?;
1198
1199 let token = EspToken::new(token_address, provider.clone());
1200
1201 let init_logs = token
1208 .Initialized_filter()
1209 .from_block(0u64)
1210 .to_block(BlockNumberOrTag::Finalized)
1211 .query()
1212 .await;
1213
1214 let init_log = match init_logs {
1215 Ok(init_logs) => {
1216 if init_logs.is_empty() {
1217 tracing::error!(
1218 "Token Initialized event logs are empty. This should never happen"
1219 );
1220 return Err(FetchRewardError::MissingInitializedEvent);
1221 }
1222
1223 let (_, init_log) = init_logs[0].clone();
1224
1225 tracing::debug!(tx_hash = ?init_log.transaction_hash, "Found token `Initialized` event");
1226 init_log
1227 },
1228 Err(err) => {
1229 tracing::warn!(
1230 "RPC returned error {err:?}. will fallback to scanning over fixed block range"
1231 );
1232 self.scan_token_contract_initialized_event_log(stake_table_init_block, token)
1233 .await?
1234 },
1235 };
1236
1237 let tx_hash =
1239 init_log
1240 .transaction_hash
1241 .ok_or_else(|| FetchRewardError::MissingTransactionHash {
1242 init_log: init_log.clone().into(),
1243 })?;
1244
1245 let init_tx = provider
1247 .get_transaction_receipt(tx_hash)
1248 .await
1249 .map_err(FetchRewardError::Rpc)?
1250 .ok_or_else(|| FetchRewardError::MissingTransactionReceipt {
1251 tx_hash: tx_hash.to_string(),
1252 })?;
1253
1254 let mint_transfer = init_tx.decoded_log::<EspToken::Transfer>().ok_or(
1255 FetchRewardError::DecodeTransferLog {
1256 tx_hash: tx_hash.to_string(),
1257 },
1258 )?;
1259
1260 tracing::debug!("mint transfer event ={mint_transfer:?}");
1261 if mint_transfer.from != Address::ZERO {
1262 return Err(FetchRewardError::InvalidMintFromAddress);
1263 }
1264
1265 let initial_supply = mint_transfer.value;
1266
1267 tracing::info!("Initial token amount: {} ESP", format_ether(initial_supply));
1268
1269 let mut writer = self.initial_supply.write().await;
1270 *writer = Some(initial_supply);
1271
1272 Ok(initial_supply)
1273 }
1274
1275 pub async fn scan_token_contract_initialized_event_log(
1283 &self,
1284 stake_table_init_block: u64,
1285 token: EspTokenInstance<L1Provider>,
1286 ) -> Result<Log, FetchRewardError> {
1287 let max_events_range = self.l1_client.options().l1_events_max_block_range;
1288 const MAX_BLOCKS_SCANNED: u64 = 200_000;
1289 let mut total_scanned = 0;
1290
1291 let mut from_block = stake_table_init_block.saturating_sub(max_events_range);
1292 let mut to_block = stake_table_init_block;
1293
1294 loop {
1295 if total_scanned >= MAX_BLOCKS_SCANNED {
1296 tracing::error!(
1297 total_scanned,
1298 "Exceeded maximum scan range while searching for token Initialized event"
1299 );
1300 return Err(FetchRewardError::ExceededMaxScanRange(MAX_BLOCKS_SCANNED));
1301 }
1302
1303 let init_logs = token
1304 .Initialized_filter()
1305 .from_block(from_block)
1306 .to_block(to_block)
1307 .query()
1308 .await
1309 .map_err(FetchRewardError::ScanQueryFailed)?;
1310
1311 if !init_logs.is_empty() {
1312 let (_, init_log) = init_logs[0].clone();
1313 tracing::info!(
1314 from_block,
1315 tx_hash = ?init_log.transaction_hash,
1316 "Found token Initialized event during scan"
1317 );
1318 return Ok(init_log);
1319 }
1320
1321 total_scanned += max_events_range;
1322 from_block = from_block.saturating_sub(max_events_range);
1323 to_block = to_block.saturating_sub(max_events_range);
1324 }
1325 }
1326
1327 pub async fn update_chain_config(&self, header: &Header) -> anyhow::Result<()> {
1328 let chain_config = self.get_chain_config(header).await?;
1329 *self.chain_config.lock().await = chain_config;
1331
1332 Ok(())
1333 }
1334
1335 pub async fn fetch(&self, epoch: Epoch, header: &Header) -> anyhow::Result<ValidatorSet> {
1336 let chain_config = *self.chain_config.lock().await;
1337 let Some(address) = chain_config.stake_table_contract else {
1338 bail!("No stake table contract address found in Chain config");
1339 };
1340
1341 let Some(l1_finalized_block_info) = header.l1_finalized() else {
1342 bail!(
1343 "The epoch root for epoch {epoch} is missing the L1 finalized block info. This is \
1344 a fatal error. Consensus is blocked and will not recover."
1345 );
1346 };
1347
1348 let events = match self
1349 .fetch_and_store_stake_table_events(address, l1_finalized_block_info.number())
1350 .await
1351 .map_err(GetStakeTablesError::L1ClientFetchError)
1352 {
1353 Ok(events) => events,
1354 Err(e) => {
1355 bail!("failed to fetch stake table events {e:?}");
1356 },
1357 };
1358
1359 match validator_set_from_l1_events(events.into_iter().map(|(_, e)| e)) {
1360 Ok(res) => Ok(res),
1361 Err(e) => {
1362 bail!("failed to construct stake table {e:?}");
1363 },
1364 }
1365 }
1366
1367 pub(crate) async fn get_chain_config(&self, header: &Header) -> anyhow::Result<ChainConfig> {
1370 let chain_config = self.chain_config.lock().await;
1371 let peers = self.peers.clone();
1372 let header_cf = header.chain_config();
1373 if chain_config.commit() == header_cf.commit() {
1374 return Ok(*chain_config);
1375 }
1376
1377 let cf = match header_cf.resolve() {
1378 Some(cf) => cf,
1379 None => peers
1380 .fetch_chain_config(header_cf.commit())
1381 .await
1382 .inspect_err(|err| {
1383 tracing::error!("failed to get chain_config from peers. err: {err:?}");
1384 })?,
1385 };
1386
1387 Ok(cf)
1388 }
1389
1390 #[cfg(any(test, feature = "testing"))]
1391 pub fn mock() -> Self {
1392 use crate::{mock, v0_1::NoStorage};
1393 let chain_config = ChainConfig::default();
1394 let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
1395 .expect("Failed to create L1 client");
1396
1397 let peers = Arc::new(mock::MockStateCatchup::default());
1398 let persistence = NoStorage;
1399
1400 Self::new(peers, Arc::new(Mutex::new(persistence)), l1, chain_config)
1401 }
1402}
1403
1404async fn retry<F, T, E>(
1405 retry_delay: Duration,
1406 max_duration: Duration,
1407 operation_name: &str,
1408 mut operation: F,
1409) -> T
1410where
1411 F: FnMut() -> BoxFuture<'static, Result<T, E>>,
1412 E: std::fmt::Display,
1413{
1414 let start = Instant::now();
1415 loop {
1416 match operation().await {
1417 Ok(result) => return result,
1418 Err(err) => {
1419 if start.elapsed() >= max_duration {
1420 panic!(
1421 r#"
1422 Failed to complete operation `{operation_name}` after `{}`.
1423 error: {err}
1424
1425
1426 This might be caused by:
1427 - The current block range being too large for your RPC provider.
1428 - The event query returning more data than your RPC allows as
1429 some RPC providers limit the number of events returned.
1430 - RPC provider outage
1431
1432 Suggested solution:
1433 - Reduce the value of the environment variable
1434 `ESPRESSO_SEQUENCER_L1_EVENTS_MAX_BLOCK_RANGE` to query smaller ranges.
1435 - Add multiple RPC providers
1436 - Use a different RPC provider with higher rate limits."#,
1437 format_duration(max_duration)
1438 );
1439 }
1440 tracing::warn!(%err, "Retrying `{operation_name}` after error");
1441 sleep(retry_delay).await;
1442 },
1443 }
1444 }
1445}
1446
1447#[derive(Clone, Debug)]
1449struct NonEpochCommittee {
1450 eligible_leaders: Vec<PeerConfig<SeqTypes>>,
1454
1455 stake_table: Vec<PeerConfig<SeqTypes>>,
1457
1458 da_committee: DaCommittee,
1459
1460 indexed_stake_table: HashMap<PubKey, PeerConfig<SeqTypes>>,
1462}
1463
1464#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
1466pub struct EpochCommittee {
1467 eligible_leaders: Vec<PeerConfig<SeqTypes>>,
1471 stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>>,
1473 validators: ValidatorMap,
1474 address_mapping: HashMap<BLSPubKey, Address>,
1475 block_reward: Option<RewardAmount>,
1476 stake_table_hash: Option<StakeTableHash>,
1477 header: Option<Header>,
1478}
1479
1480impl EpochCommittees {
1481 pub fn first_epoch(&self) -> Option<Epoch> {
1482 self.first_epoch
1483 }
1484
1485 pub fn fetcher(&self) -> &Fetcher {
1486 &self.fetcher
1487 }
1488
1489 pub fn fixed_block_reward(&self) -> Option<RewardAmount> {
1490 self.fixed_block_reward
1491 }
1492
1493 async fn fetch_and_update_fixed_block_reward(
1498 membership: Arc<RwLock<Self>>,
1499 epoch: EpochNumber,
1500 ) -> anyhow::Result<RewardAmount> {
1501 let membership_reader = membership.upgradable_read().await;
1502 let fetcher = membership_reader.fetcher.clone();
1503 match membership_reader.fixed_block_reward {
1504 Some(reward) => Ok(reward),
1505 None => {
1506 tracing::warn!(%epoch,
1507 "Block reward is None. attempting to fetch it from L1",
1508
1509 );
1510 let block_reward = fetcher
1511 .fetch_fixed_block_reward()
1512 .await
1513 .inspect_err(|err| {
1514 tracing::error!(?epoch, ?err, "failed to fetch block_reward");
1515 })?;
1516 let mut writer = RwLockUpgradableReadGuard::upgrade(membership_reader).await;
1517 writer.fixed_block_reward = Some(block_reward);
1518 Ok(block_reward)
1519 },
1520 }
1521 }
1522
1523 pub fn compute_block_reward(
1524 epoch: &EpochNumber,
1525 total_supply: U256,
1526 total_stake: U256,
1527 avg_block_time_ms: u64,
1528 ) -> anyhow::Result<RewardAmount> {
1529 let total_stake_bd = BigDecimal::from_str(&total_stake.to_string())?;
1531 let total_supply_bd = BigDecimal::from_str(&(total_supply.to_string()))?;
1532
1533 tracing::debug!(?epoch, "total_stake={total_stake}");
1534 tracing::debug!(?epoch, "total_supply_bd={total_supply_bd}");
1535
1536 let (proportion, reward_rate) =
1537 calculate_proportion_staked_and_reward_rate(&total_stake_bd, &total_supply_bd)?;
1538 let inflation_rate = proportion * reward_rate;
1539
1540 tracing::debug!(?epoch, "inflation_rate={inflation_rate:?}");
1541
1542 let blocks_per_year = MILLISECONDS_PER_YEAR
1543 .checked_div(avg_block_time_ms.into())
1544 .context("avg_block_time_ms is zero")?;
1545
1546 tracing::debug!(?epoch, "blocks_per_year={blocks_per_year:?}");
1547
1548 ensure!(!blocks_per_year.is_zero(), "blocks per year is zero");
1549 let block_reward = (total_supply_bd * inflation_rate) / blocks_per_year;
1550
1551 let block_reward_u256 = U256::from_str(&block_reward.round(0).to_string())?;
1552
1553 Ok(block_reward_u256.into())
1554 }
1555
1556 pub async fn fetch_and_calculate_block_reward(
1566 current_epoch: Epoch,
1567 coordinator: EpochMembershipCoordinator<SeqTypes>,
1568 ) -> anyhow::Result<RewardAmount> {
1569 let membership_read = coordinator.membership().read().await;
1570 let fixed_block_reward = membership_read.fixed_block_reward;
1571
1572 let committee = membership_read
1573 .state
1574 .get(¤t_epoch)
1575 .context(format!("committee not found for epoch={current_epoch:?}"))?
1576 .clone();
1577
1578 if let Some(reward) = committee.block_reward {
1580 return Ok(reward);
1581 }
1582
1583 let first_epoch = *membership_read.first_epoch().context(format!(
1584 "First epoch not initialized (current_epoch={current_epoch})"
1585 ))?;
1586
1587 drop(membership_read);
1588
1589 if *current_epoch <= first_epoch + 1 {
1590 bail!(
1591 "epoch is in first two epochs: current_epoch={current_epoch}, \
1592 first_epoch={first_epoch}"
1593 );
1594 }
1595
1596 let header = match committee.header.clone() {
1597 Some(header) => header,
1598 None => {
1599 let root_epoch = current_epoch.checked_sub(2).context(format!(
1600 "Epoch calculation underflow (current_epoch={current_epoch})"
1601 ))?;
1602
1603 tracing::info!(?root_epoch, "catchup epoch root header");
1604
1605 let membership = coordinator.membership();
1606 let leaf = Self::get_epoch_root(membership.clone(), EpochNumber::new(root_epoch))
1607 .await
1608 .with_context(|| {
1609 format!("Failed to get epoch root for root_epoch={root_epoch}")
1610 })?;
1611 leaf.block_header().clone()
1612 },
1613 };
1614
1615 if header.version() <= EpochVersion::version() {
1616 return fixed_block_reward.context(format!(
1617 "Fixed block reward not found for current_epoch={current_epoch}"
1618 ));
1619 }
1620
1621 let prev_epoch_u64 = current_epoch.checked_sub(1).context(format!(
1622 "Underflow: cannot compute previous epoch when current_epoch={current_epoch}"
1623 ))?;
1624
1625 let prev_epoch = EpochNumber::new(prev_epoch_u64);
1626
1627 if *prev_epoch > first_epoch + 1 {
1630 if let Err(err) = coordinator.stake_table_for_epoch(Some(prev_epoch)).await {
1631 tracing::info!("failed to get membership for epoch={prev_epoch:?}: {err:#}");
1632
1633 coordinator
1634 .wait_for_catchup(prev_epoch)
1635 .await
1636 .context(format!("failed to catch up for epoch={prev_epoch}"))?;
1637 }
1638 }
1639
1640 let membership_read = coordinator.membership().read().await;
1641
1642 membership_read
1643 .calculate_dynamic_block_reward(¤t_epoch, &header, &committee.validators)
1644 .await
1645 .with_context(|| {
1646 format!("dynamic block reward calculation failed for epoch={current_epoch}")
1647 })?
1648 .with_context(|| format!("dynamic block reward returned None. epoch={current_epoch}"))
1649 }
1650
1651 async fn calculate_dynamic_block_reward(
1658 &self,
1659 epoch: &Epoch,
1660 header: &Header,
1661 validators: &ValidatorMap,
1662 ) -> anyhow::Result<Option<RewardAmount>> {
1663 let epoch_height = self.epoch_height;
1664 let current_epoch = epoch_from_block_number(header.height(), epoch_height);
1665 let previous_epoch = current_epoch
1666 .checked_sub(1)
1667 .context("underflow: cannot get previous epoch when current_epoch is 0")?;
1668 tracing::debug!(?epoch, "previous_epoch={previous_epoch:?}");
1669
1670 let first_epoch = *self.first_epoch().context("first epoch is None")?;
1671
1672 if previous_epoch > first_epoch + 1
1675 && !self.has_stake_table(EpochNumber::new(previous_epoch))
1676 {
1677 tracing::warn!(?previous_epoch, "missing stake table for previous epoch");
1678 return Ok(None);
1679 }
1680
1681 let fetcher = self.fetcher.clone();
1682
1683 let previous_reward_distributed = header
1684 .total_reward_distributed()
1685 .context("Invalid block header: missing total_reward_distributed field")?;
1686
1687 let total_stake: U256 = validators.values().map(|v| v.stake).sum();
1689 let initial_supply = *fetcher.initial_supply.read().await;
1690 let initial_supply = match initial_supply {
1691 Some(supply) => supply,
1692 None => fetcher.fetch_and_update_initial_supply().await?,
1693 };
1694 let total_supply = initial_supply
1695 .checked_add(previous_reward_distributed.0)
1696 .context("initial_supply + previous_reward_distributed overflow")?;
1697
1698 let curr_ts = header.timestamp_millis_internal();
1700 tracing::debug!(?epoch, "curr_ts={curr_ts:?}");
1701
1702 let average_block_time_ms = if previous_epoch <= first_epoch + 1 {
1706 ASSUMED_BLOCK_TIME_SECONDS as u64 * 1000 } else {
1708 let next_epoch = epoch
1712 .checked_sub(1)
1713 .context("underflow: cannot get next epoch when epoch is 0")?;
1714 let prev_ts = match self.get_header(EpochNumber::new(next_epoch)) {
1715 Some(header) => header.timestamp_millis_internal(),
1716 None => {
1717 tracing::info!(
1718 "Calculating rewards for epoch {}, we have no root leaf header for epoch \
1719 - 1. Fetching from peers",
1720 epoch
1721 );
1722
1723 let root_height = header.height().checked_sub(epoch_height).context(
1724 "Epoch height is greater than block height. cannot compute previous epoch \
1725 root height",
1726 )?;
1727
1728 let prev_stake_table = self
1729 .get_stake_table(&Some(EpochNumber::new(previous_epoch)))
1730 .context("Stake table not found")?
1731 .into();
1732
1733 let success_threshold =
1734 self.success_threshold(Some(EpochNumber::new(previous_epoch)));
1735
1736 fetcher
1737 .peers
1738 .fetch_leaf(root_height, prev_stake_table, success_threshold)
1739 .await
1740 .context("Epoch root leaf not found")?
1741 .block_header()
1742 .timestamp_millis_internal()
1743 },
1744 };
1745
1746 let time_diff = curr_ts.checked_sub(prev_ts).context(
1747 "Current timestamp is earlier than previous. underflow in block time calculation",
1748 )?;
1749
1750 time_diff
1751 .checked_div(epoch_height)
1752 .context("Epoch height is zero. cannot compute average block time")?
1753 };
1754 tracing::info!(?epoch, %total_supply, %total_stake, %average_block_time_ms,
1755 "dynamic block reward parameters");
1756
1757 let block_reward =
1758 Self::compute_block_reward(epoch, total_supply, total_stake, average_block_time_ms)?;
1759
1760 Ok(Some(block_reward))
1761 }
1762
1763 pub fn epoch_block_reward(&self, epoch: EpochNumber) -> Option<RewardAmount> {
1765 self.state
1766 .get(&epoch)
1767 .and_then(|committee| committee.block_reward)
1768 }
1769 fn insert_committee(
1775 &mut self,
1776 epoch: EpochNumber,
1777 validators: ValidatorMap,
1778 block_reward: Option<RewardAmount>,
1779 hash: Option<StakeTableHash>,
1780 header: Option<Header>,
1781 ) {
1782 let mut address_mapping = HashMap::new();
1783 let stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>> = validators
1784 .values()
1785 .map(|v| {
1786 address_mapping.insert(v.stake_table_key, v.account);
1787 (
1788 v.stake_table_key,
1789 PeerConfig {
1790 stake_table_entry: BLSPubKey::stake_table_entry(
1791 &v.stake_table_key,
1792 v.stake,
1793 ),
1794 state_ver_key: v.state_ver_key.clone(),
1795 },
1796 )
1797 })
1798 .collect();
1799
1800 let eligible_leaders: Vec<PeerConfig<SeqTypes>> =
1801 stake_table.iter().map(|(_, l)| l.clone()).collect();
1802
1803 self.state.insert(
1804 epoch,
1805 EpochCommittee {
1806 eligible_leaders,
1807 stake_table,
1808 validators,
1809 address_mapping,
1810 block_reward,
1811 stake_table_hash: hash,
1812 header,
1813 },
1814 );
1815 }
1816
1817 pub fn active_validators(&self, epoch: &Epoch) -> anyhow::Result<ValidatorMap> {
1818 Ok(self
1819 .state
1820 .get(epoch)
1821 .context("state for found")?
1822 .validators
1823 .clone())
1824 }
1825
1826 pub fn address(&self, epoch: &Epoch, bls_key: BLSPubKey) -> anyhow::Result<Address> {
1827 let mapping = self
1828 .state
1829 .get(epoch)
1830 .context("state for found")?
1831 .address_mapping
1832 .clone();
1833
1834 Ok(*mapping.get(&bls_key).context(format!(
1835 "failed to get ethereum address for bls key {bls_key}. epoch={epoch}"
1836 ))?)
1837 }
1838
1839 pub fn get_validator_config(
1840 &self,
1841 epoch: &Epoch,
1842 key: BLSPubKey,
1843 ) -> anyhow::Result<Validator<BLSPubKey>> {
1844 let address = self.address(epoch, key)?;
1845 let validators = self.active_validators(epoch)?;
1846 validators
1847 .get(&address)
1848 .context("validator not found")
1849 .cloned()
1850 }
1851
1852 pub fn new_stake(
1854 committee_members: Vec<PeerConfig<SeqTypes>>,
1857 da_members: Vec<PeerConfig<SeqTypes>>,
1858 fixed_block_reward: Option<RewardAmount>,
1859 fetcher: Fetcher,
1860 epoch_height: u64,
1861 ) -> Self {
1862 let stake_table: Vec<_> = committee_members
1864 .iter()
1865 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
1866 .cloned()
1867 .collect();
1868
1869 let eligible_leaders = stake_table.clone();
1870 let da_members: Vec<_> = da_members
1872 .iter()
1873 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
1874 .cloned()
1875 .collect();
1876
1877 let indexed_stake_table: HashMap<PubKey, _> = stake_table
1879 .iter()
1880 .map(|peer_config| {
1881 (
1882 PubKey::public_key(&peer_config.stake_table_entry),
1883 peer_config.clone(),
1884 )
1885 })
1886 .collect();
1887
1888 let indexed_da_members: HashMap<PubKey, _> = da_members
1890 .iter()
1891 .map(|peer_config| {
1892 (
1893 PubKey::public_key(&peer_config.stake_table_entry),
1894 peer_config.clone(),
1895 )
1896 })
1897 .collect();
1898
1899 let da_committee = DaCommittee {
1900 committee: da_members,
1901 indexed_committee: indexed_da_members,
1902 };
1903
1904 let members = NonEpochCommittee {
1905 eligible_leaders,
1906 stake_table,
1907 indexed_stake_table,
1908 da_committee,
1909 };
1910
1911 let mut map = HashMap::new();
1912 let epoch_committee = EpochCommittee {
1913 eligible_leaders: members.eligible_leaders.clone(),
1914 stake_table: members
1915 .stake_table
1916 .iter()
1917 .map(|x| (PubKey::public_key(&x.stake_table_entry), x.clone()))
1918 .collect(),
1919 validators: Default::default(),
1920 address_mapping: HashMap::new(),
1921 block_reward: Default::default(),
1922 stake_table_hash: None,
1923 header: None,
1924 };
1925 map.insert(Epoch::genesis(), epoch_committee.clone());
1926 map.insert(Epoch::genesis() + 1u64, epoch_committee.clone());
1928
1929 Self {
1930 non_epoch_committee: members,
1931 da_committees: BTreeMap::new(),
1932 state: map,
1933 all_validators: BTreeMap::new(),
1934 randomized_committees: BTreeMap::new(),
1935 first_epoch: None,
1936 fixed_block_reward,
1937 fetcher: Arc::new(fetcher),
1938 epoch_height,
1939 }
1940 }
1941
1942 pub async fn reload_stake(&mut self, limit: u64) {
1943 match self.fetcher.fetch_fixed_block_reward().await {
1944 Ok(block_reward) => {
1945 tracing::info!("Fetched block reward: {block_reward}");
1946 self.fixed_block_reward = Some(block_reward);
1947 },
1948 Err(err) => {
1949 tracing::warn!(
1950 "Failed to fetch the block reward when reloading the stake tables: {err}"
1951 );
1952 },
1953 }
1954
1955 let loaded_stake = match self
1957 .fetcher
1958 .persistence
1959 .lock()
1960 .await
1961 .load_latest_stake(limit)
1962 .await
1963 {
1964 Ok(Some(loaded)) => loaded,
1965 Ok(None) => {
1966 tracing::warn!("No stake table history found in persistence!");
1967 return;
1968 },
1969 Err(e) => {
1970 tracing::error!("Failed to load stake table history from persistence: {e}");
1971 return;
1972 },
1973 };
1974
1975 for (epoch, (stake_table, block_reward), stake_table_hash) in loaded_stake {
1976 self.insert_committee(epoch, stake_table, block_reward, stake_table_hash, None);
1977 }
1978 }
1979
1980 fn get_stake_table(&self, epoch: &Option<Epoch>) -> Option<Vec<PeerConfig<SeqTypes>>> {
1981 if let Some(epoch) = epoch {
1982 self.state
1983 .get(epoch)
1984 .map(|committee| committee.stake_table.clone().into_values().collect())
1985 } else {
1986 Some(self.non_epoch_committee.stake_table.clone())
1987 }
1988 }
1989
1990 fn get_da_committee(&self, epoch: Option<Epoch>) -> DaCommittee {
1991 if let Some(e) = epoch {
1992 self.da_committees
1994 .range((Bound::Included(&0), Bound::Included(&*e)))
1995 .last()
1996 .map(|(_, committee)| committee.clone())
1997 .unwrap_or(self.non_epoch_committee.da_committee.clone())
1998 } else {
1999 self.non_epoch_committee.da_committee.clone()
2000 }
2001 }
2002
2003 fn get_header(&self, epoch: Epoch) -> Option<&Header> {
2005 self.state
2006 .get(&epoch)
2007 .and_then(|committee| committee.header.as_ref())
2008 }
2009}
2010
2011pub fn calculate_proportion_staked_and_reward_rate(
2021 total_stake: &BigDecimal,
2022 total_supply: &BigDecimal,
2023) -> anyhow::Result<(BigDecimal, BigDecimal)> {
2024 if total_supply.is_zero() {
2025 return Err(anyhow::anyhow!("Total supply cannot be zero"));
2026 }
2027
2028 let proportion_staked = total_stake / total_supply;
2029
2030 if proportion_staked < BigDecimal::from(0) || proportion_staked > BigDecimal::from(1) {
2031 return Err(anyhow::anyhow!("Stake ratio p must be in the range [0, 1]"));
2032 }
2033
2034 let two = BigDecimal::from_u32(2).unwrap();
2035 let min_stake_ratio = BigDecimal::from_str("0.01")?;
2036 let numerator = BigDecimal::from_str("0.03")?;
2037
2038 let denominator = (&two * (&proportion_staked).max(&min_stake_ratio))
2039 .sqrt()
2040 .context("Failed to compute sqrt in R(p)")?;
2041
2042 let reward_rate = numerator / denominator;
2043
2044 tracing::debug!("rp={reward_rate}");
2045
2046 Ok((proportion_staked, reward_rate))
2047}
2048
2049#[derive(Error, Debug)]
2050enum GetStakeTablesError {
2052 #[error("Error fetching from L1: {0}")]
2053 L1ClientFetchError(anyhow::Error),
2054}
2055
2056#[derive(Error, Debug)]
2057#[error("Could not lookup leader")] pub struct LeaderLookupError;
2059
2060impl Membership<SeqTypes> for EpochCommittees {
2062 type Error = LeaderLookupError;
2063 type Storage = ();
2064 type StakeTableHash = StakeTableState;
2065
2066 fn new<I: NodeImplementation<SeqTypes>>(
2068 _committee_members: Vec<PeerConfig<SeqTypes>>,
2071 _da_members: Vec<PeerConfig<SeqTypes>>,
2072 _storage: Self::Storage,
2073 _network: Arc<<I as NodeImplementation<SeqTypes>>::Network>,
2074 _public_key: <SeqTypes as NodeType>::SignatureKey,
2075 _epoch_height: u64,
2076 ) -> Self {
2077 panic!("This function has been replaced with new_stake()");
2078 }
2079
2080 fn stake_table(&self, epoch: Option<Epoch>) -> HSStakeTable<SeqTypes> {
2082 self.get_stake_table(&epoch).unwrap_or_default().into()
2083 }
2084 fn da_stake_table(&self, epoch: Option<Epoch>) -> HSStakeTable<SeqTypes> {
2086 self.get_da_committee(epoch).committee.clone().into()
2087 }
2088
2089 fn committee_members(
2091 &self,
2092 _view_number: <SeqTypes as NodeType>::View,
2093 epoch: Option<Epoch>,
2094 ) -> BTreeSet<PubKey> {
2095 let stake_table = self.stake_table(epoch);
2096 stake_table
2097 .iter()
2098 .map(|x| PubKey::public_key(&x.stake_table_entry))
2099 .collect()
2100 }
2101
2102 fn da_committee_members(
2104 &self,
2105 _view_number: <SeqTypes as NodeType>::View,
2106 epoch: Option<Epoch>,
2107 ) -> BTreeSet<PubKey> {
2108 self.da_stake_table(epoch)
2109 .iter()
2110 .map(|peer_config| PubKey::public_key(&peer_config.stake_table_entry))
2111 .collect()
2112 }
2113
2114 fn stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> Option<PeerConfig<SeqTypes>> {
2116 if let Some(epoch) = epoch {
2118 self.state
2119 .get(&epoch)
2120 .and_then(|h| h.stake_table.get(pub_key))
2121 .cloned()
2122 } else {
2123 self.non_epoch_committee
2124 .indexed_stake_table
2125 .get(pub_key)
2126 .cloned()
2127 }
2128 }
2129
2130 fn da_stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> Option<PeerConfig<SeqTypes>> {
2132 self.get_da_committee(epoch)
2133 .indexed_committee
2134 .get(pub_key)
2135 .cloned()
2136 }
2137
2138 fn has_stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> bool {
2140 self.stake(pub_key, epoch)
2141 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
2142 .unwrap_or_default()
2143 }
2144
2145 fn has_da_stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> bool {
2147 self.da_stake(pub_key, epoch)
2148 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
2149 .unwrap_or_default()
2150 }
2151
2152 fn lookup_leader(
2165 &self,
2166 view_number: <SeqTypes as NodeType>::View,
2167 epoch: Option<Epoch>,
2168 ) -> Result<PubKey, Self::Error> {
2169 match (self.first_epoch(), epoch) {
2170 (Some(first_epoch), Some(epoch)) => {
2171 if epoch < first_epoch {
2172 tracing::error!(
2173 "lookup_leader called with epoch {} before first epoch {}",
2174 epoch,
2175 first_epoch,
2176 );
2177 return Err(LeaderLookupError);
2178 }
2179 let Some(randomized_committee) = self.randomized_committees.get(&epoch) else {
2180 tracing::error!(
2181 "We are missing the randomized committee for epoch {}",
2182 epoch
2183 );
2184 return Err(LeaderLookupError);
2185 };
2186
2187 Ok(PubKey::public_key(&select_randomized_leader(
2188 randomized_committee,
2189 *view_number,
2190 )))
2191 },
2192 (_, None) => {
2193 let leaders = &self.non_epoch_committee.eligible_leaders;
2194
2195 let index = *view_number as usize % leaders.len();
2196 let res = leaders[index].clone();
2197 Ok(PubKey::public_key(&res.stake_table_entry))
2198 },
2199 (None, Some(epoch)) => {
2200 tracing::error!(
2201 "lookup_leader called with epoch {} but we don't have a first epoch",
2202 epoch,
2203 );
2204 Err(LeaderLookupError)
2205 },
2206 }
2207 }
2208
2209 fn total_nodes(&self, epoch: Option<Epoch>) -> usize {
2211 self.stake_table(epoch).len()
2212 }
2213
2214 fn da_total_nodes(&self, epoch: Option<Epoch>) -> usize {
2216 self.da_stake_table(epoch).len()
2217 }
2218
2219 async fn add_epoch_root(
2223 membership: Arc<RwLock<Self>>,
2224 block_header: Header,
2225 ) -> anyhow::Result<()> {
2226 let block_number = block_header.block_number();
2227
2228 let membership_reader = membership.read().await;
2229 let epoch_height = membership_reader.epoch_height;
2230
2231 let epoch =
2232 Epoch::new(epoch_from_block_number(block_number, membership_reader.epoch_height) + 2);
2233
2234 tracing::info!(?epoch, "adding epoch root. height={:?}", block_number);
2235
2236 if !is_epoch_root(block_number, epoch_height) {
2237 tracing::error!(
2238 "`add_epoch_root` was called with a block header that was not the root block for \
2239 an epoch. This should never happen. Header:\n\n{block_header:?}"
2240 );
2241 bail!(
2242 "Failed to add epoch root: block {block_number:?} is not a root block for an epoch"
2243 );
2244 }
2245
2246 let fetcher = membership_reader.fetcher.clone();
2247
2248 drop(membership_reader);
2249
2250 let version = block_header.version();
2251 fetcher.update_chain_config(&block_header).await?;
2253
2254 let mut block_reward = None;
2255 if version == EpochVersion::version() {
2258 let reward =
2259 Self::fetch_and_update_fixed_block_reward(membership.clone(), epoch).await?;
2260 block_reward = Some(reward);
2261 }
2262
2263 let epoch_committee = {
2264 let membership_reader = membership.read().await;
2265 membership_reader.state.get(&epoch).cloned()
2266 };
2267
2268 let (active_validators, all_validators, stake_table_hash) = match epoch_committee {
2274 Some(committee)
2275 if committee.block_reward.is_some()
2276 && committee.header.is_some()
2277 && committee.stake_table_hash.is_some() =>
2278 {
2279 tracing::info!(
2280 ?epoch,
2281 "committee already has block reward, header, and stake table hash; skipping \
2282 add_epoch_root"
2283 );
2284 return Ok(());
2285 },
2286
2287 Some(committee) => {
2288 if let Some(reward) = committee.block_reward {
2289 block_reward = Some(reward);
2290 }
2291
2292 if let Some(hash) = committee.stake_table_hash {
2293 (committee.validators.clone(), Default::default(), Some(hash))
2294 } else {
2295 tracing::info!(
2297 "Stake table hash missing for epoch {epoch}. recalculating by fetching \
2298 from l1."
2299 );
2300 let set = fetcher.fetch(epoch, &block_header).await?;
2301 (
2302 set.active_validators,
2303 set.all_validators,
2304 set.stake_table_hash,
2305 )
2306 }
2307 },
2308
2309 None => {
2310 tracing::info!("Stake table missing for epoch {epoch}. Fetching from L1.");
2311 let set = fetcher.fetch(epoch, &block_header).await?;
2312 (
2313 set.active_validators,
2314 set.all_validators,
2315 set.stake_table_hash,
2316 )
2317 },
2318 };
2319
2320 if block_reward.is_none() && version >= DrbAndHeaderUpgradeVersion::version() {
2324 tracing::info!(?epoch, "calculating dynamic block reward");
2325 let reader = membership.read().await;
2326 let reward = reader
2327 .calculate_dynamic_block_reward(&epoch, &block_header, &active_validators)
2328 .await?;
2329
2330 tracing::info!(?epoch, "calculated dynamic block reward = {reward:?}");
2331 block_reward = reward;
2332 }
2333
2334 let mut membership_writer = membership.write().await;
2335 membership_writer.insert_committee(
2336 epoch,
2337 active_validators.clone(),
2338 block_reward,
2339 stake_table_hash,
2340 Some(block_header.clone()),
2341 );
2342
2343 let previous_epoch = EpochNumber::new(epoch.saturating_sub(1));
2346 let previous_committee = membership_writer.state.get(&previous_epoch).cloned();
2347 membership_writer.all_validators =
2349 membership_writer.all_validators.split_off(&previous_epoch);
2350 let previous_validators = membership_writer.all_validators.remove(&previous_epoch);
2352 membership_writer
2353 .all_validators
2354 .insert(epoch, all_validators.clone());
2355 drop(membership_writer);
2356
2357 let persistence_lock = fetcher.persistence.lock().await;
2358
2359 let decided_hash = block_header.next_stake_table_hash();
2360
2361 if let Some(previous_committee) = previous_committee {
2368 if decided_hash.is_none() || decided_hash == previous_committee.stake_table_hash {
2369 if let Err(e) = persistence_lock
2370 .store_stake(
2371 previous_epoch,
2372 previous_committee.validators,
2373 previous_committee.block_reward,
2374 previous_committee.stake_table_hash,
2375 )
2376 .await
2377 {
2378 tracing::error!(
2379 ?e,
2380 ?previous_epoch,
2381 "`add_epoch_root`, error storing stake table"
2382 );
2383 }
2384
2385 if let Some(previous_validators) = previous_validators {
2386 if let Err(e) = persistence_lock
2387 .store_all_validators(previous_epoch, previous_validators)
2388 .await
2389 {
2390 tracing::error!(
2391 ?e,
2392 ?epoch,
2393 "`add_epoch_root`, error storing all validators"
2394 );
2395 }
2396 }
2397 } else {
2398 panic!(
2399 "The decided block header's `next_stake_table_hash` does not match the hash \
2400 of the stake table we have. This is an unrecoverable error likely due to \
2401 issues with the your L1 RPC provider. Decided:\n\n{:?}Actual:\n\n{:?}",
2402 decided_hash, previous_committee.stake_table_hash
2403 );
2404 }
2405 }
2406
2407 Ok(())
2408 }
2409
2410 fn has_stake_table(&self, epoch: Epoch) -> bool {
2411 self.state.contains_key(&epoch)
2412 }
2413
2414 fn has_randomized_stake_table(&self, epoch: Epoch) -> anyhow::Result<bool> {
2426 let Some(first_epoch) = self.first_epoch else {
2427 bail!(
2428 "Called has_randomized_stake_table with epoch {} but first_epoch is None",
2429 epoch
2430 );
2431 };
2432 ensure!(
2433 epoch >= first_epoch,
2434 "Called has_randomized_stake_table with epoch {} but first_epoch is {}",
2435 epoch,
2436 first_epoch
2437 );
2438 Ok(self.randomized_committees.contains_key(&epoch))
2439 }
2440
2441 async fn get_epoch_root(membership: Arc<RwLock<Self>>, epoch: Epoch) -> anyhow::Result<Leaf2> {
2442 let membership_reader = membership.read().await;
2443 let block_height = root_block_in_epoch(*epoch, membership_reader.epoch_height);
2444 let peers = membership_reader.fetcher.peers.clone();
2445 let stake_table = membership_reader.stake_table(Some(epoch)).clone();
2446 let success_threshold = membership_reader.success_threshold(Some(epoch));
2447 drop(membership_reader);
2448
2449 let leaf: Leaf2 = peers
2451 .fetch_leaf(block_height, stake_table.clone(), success_threshold)
2452 .await?;
2453
2454 Ok(leaf)
2455 }
2456
2457 async fn get_epoch_drb(
2458 membership: Arc<RwLock<Self>>,
2459 epoch: Epoch,
2460 ) -> anyhow::Result<DrbResult> {
2461 let membership_reader = membership.read().await;
2462 let peers = membership_reader.fetcher.peers.clone();
2463
2464 if let Some(randomized_committee) = membership_reader.randomized_committees.get(&epoch) {
2466 return Ok(randomized_committee.drb_result());
2467 }
2468
2469 let previous_epoch = match epoch.checked_sub(1) {
2471 Some(epoch) => EpochNumber::new(epoch),
2472 None => {
2473 return membership_reader
2474 .randomized_committees
2475 .get(&epoch)
2476 .map(|committee| committee.drb_result())
2477 .context(format!("Missing randomized committee for epoch {epoch}"))
2478 },
2479 };
2480
2481 let stake_table = membership_reader.stake_table(Some(previous_epoch)).clone();
2482 let success_threshold = membership_reader.success_threshold(Some(previous_epoch));
2483
2484 let block_height =
2485 transition_block_for_epoch(*previous_epoch, membership_reader.epoch_height);
2486
2487 drop(membership_reader);
2488
2489 tracing::debug!(
2490 "Getting DRB for epoch {}, block height {}",
2491 epoch,
2492 block_height
2493 );
2494 let drb_leaf = peers
2495 .try_fetch_leaf(1, block_height, stake_table, success_threshold)
2496 .await?;
2497
2498 let Some(drb) = drb_leaf.next_drb_result else {
2499 tracing::error!(
2500 "We received a leaf that should contain a DRB result, but the DRB result is \
2501 missing: {:?}",
2502 drb_leaf
2503 );
2504
2505 bail!("DRB leaf is missing the DRB result.");
2506 };
2507
2508 Ok(drb)
2509 }
2510
2511 fn add_drb_result(&mut self, epoch: Epoch, drb: DrbResult) {
2512 tracing::info!("Adding DRB result {drb:?} to epoch {epoch}");
2513
2514 let Some(raw_stake_table) = self.state.get(&epoch) else {
2515 tracing::error!(
2516 "add_drb_result({epoch}, {drb:?}) was called, but we do not yet have the stake \
2517 table for epoch {epoch}"
2518 );
2519 return;
2520 };
2521
2522 let leaders = raw_stake_table
2523 .eligible_leaders
2524 .clone()
2525 .into_iter()
2526 .map(|peer_config| peer_config.stake_table_entry)
2527 .collect::<Vec<_>>();
2528 let randomized_committee = generate_stake_cdf(leaders, drb);
2529
2530 self.randomized_committees
2531 .insert(epoch, randomized_committee);
2532 }
2533
2534 fn set_first_epoch(&mut self, epoch: Epoch, initial_drb_result: DrbResult) {
2535 self.first_epoch = Some(epoch);
2536
2537 let epoch_committee = self.state.get(&Epoch::genesis()).unwrap().clone();
2538 self.state.insert(epoch, epoch_committee.clone());
2539 self.state.insert(epoch + 1, epoch_committee);
2540 self.add_drb_result(epoch, initial_drb_result);
2541 self.add_drb_result(epoch + 1, initial_drb_result);
2542 }
2543
2544 fn first_epoch(&self) -> Option<<SeqTypes as NodeType>::Epoch> {
2545 self.first_epoch
2546 }
2547
2548 fn stake_table_hash(&self, epoch: Epoch) -> Option<StakeTableHash> {
2549 let committee = self.state.get(&epoch)?;
2550 committee.stake_table_hash
2551 }
2552
2553 fn add_da_committee(&mut self, first_epoch: u64, committee: Vec<PeerConfig<SeqTypes>>) {
2554 let indexed_committee: HashMap<PubKey, _> = committee
2555 .iter()
2556 .map(|peer_config| {
2557 (
2558 PubKey::public_key(&peer_config.stake_table_entry),
2559 peer_config.clone(),
2560 )
2561 })
2562 .collect();
2563
2564 let da_committee = DaCommittee {
2565 committee,
2566 indexed_committee,
2567 };
2568
2569 self.da_committees.insert(first_epoch, da_committee);
2570 }
2571}
2572
2573#[cfg(any(test, feature = "testing"))]
2574impl super::v0_3::StakeTable {
2575 pub fn mock(n: u64) -> Self {
2577 [..n]
2578 .iter()
2579 .map(|_| PeerConfig::default())
2580 .collect::<Vec<PeerConfig<SeqTypes>>>()
2581 .into()
2582 }
2583}
2584
2585#[cfg(any(test, feature = "testing"))]
2586impl DAMembers {
2587 pub fn mock(n: u64) -> Self {
2589 [..n]
2590 .iter()
2591 .map(|_| PeerConfig::default())
2592 .collect::<Vec<PeerConfig<SeqTypes>>>()
2593 .into()
2594 }
2595}
2596
2597#[cfg(any(test, feature = "testing"))]
2598pub mod testing {
2599 use alloy::primitives::Bytes;
2600 use hotshot_contract_adapter::{
2601 sol_types::{EdOnBN254PointSol, G1PointSol, G2PointSol},
2602 stake_table::{sign_address_bls, sign_address_schnorr, StateSignatureSol},
2603 };
2604 use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
2605 use rand::{Rng as _, RngCore as _};
2606
2607 use super::*;
2608
2609 #[derive(Debug, Clone)]
2612 pub struct TestValidator {
2613 pub account: Address,
2614 pub bls_vk: G2PointSol,
2615 pub schnorr_vk: EdOnBN254PointSol,
2616 pub commission: u16,
2617 pub bls_sig: G1PointSol,
2618 pub schnorr_sig: Bytes,
2619 }
2620
2621 impl TestValidator {
2622 pub fn random() -> Self {
2623 let account = Address::random();
2624 let commission = rand::thread_rng().gen_range(0..10000);
2625 Self::random_update_keys(account, commission)
2626 }
2627
2628 pub fn randomize_keys(&self) -> Self {
2629 Self::random_update_keys(self.account, self.commission)
2630 }
2631
2632 pub fn random_update_keys(account: Address, commission: u16) -> Self {
2633 let mut rng = &mut rand::thread_rng();
2634 let mut seed = [0u8; 32];
2635 rng.fill_bytes(&mut seed);
2636 let bls_key_pair = BLSKeyPair::generate(&mut rng);
2637 let bls_sig = sign_address_bls(&bls_key_pair, account);
2638 let schnorr_key_pair = StateKeyPair::generate_from_seed_indexed(seed, 0);
2639 let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, account);
2640 Self {
2641 account,
2642 bls_vk: bls_key_pair.ver_key().to_affine().into(),
2643 schnorr_vk: schnorr_key_pair.ver_key().to_affine().into(),
2644 commission,
2645 bls_sig: bls_sig.into(),
2646 schnorr_sig: StateSignatureSol::from(schnorr_sig).into(),
2647 }
2648 }
2649 }
2650
2651 impl From<&TestValidator> for ValidatorRegistered {
2652 fn from(value: &TestValidator) -> Self {
2653 Self {
2654 account: value.account,
2655 blsVk: value.bls_vk,
2656 schnorrVk: value.schnorr_vk,
2657 commission: value.commission,
2658 }
2659 }
2660 }
2661
2662 impl From<&TestValidator> for ValidatorRegisteredV2 {
2663 fn from(value: &TestValidator) -> Self {
2664 Self {
2665 account: value.account,
2666 blsVK: value.bls_vk,
2667 schnorrVK: value.schnorr_vk,
2668 commission: value.commission,
2669 blsSig: value.bls_sig.into(),
2670 schnorrSig: value.schnorr_sig.clone(),
2671 metadataUri: "dummy-meta".to_string(),
2672 }
2673 }
2674 }
2675
2676 impl From<&TestValidator> for ConsensusKeysUpdated {
2677 fn from(value: &TestValidator) -> Self {
2678 Self {
2679 account: value.account,
2680 blsVK: value.bls_vk,
2681 schnorrVK: value.schnorr_vk,
2682 }
2683 }
2684 }
2685
2686 impl From<&TestValidator> for ConsensusKeysUpdatedV2 {
2687 fn from(value: &TestValidator) -> Self {
2688 Self {
2689 account: value.account,
2690 blsVK: value.bls_vk,
2691 schnorrVK: value.schnorr_vk,
2692 blsSig: value.bls_sig.into(),
2693 schnorrSig: value.schnorr_sig.clone(),
2694 }
2695 }
2696 }
2697
2698 impl From<&TestValidator> for ValidatorExit {
2699 fn from(value: &TestValidator) -> Self {
2700 Self {
2701 validator: value.account,
2702 }
2703 }
2704 }
2705
2706 impl Validator<BLSPubKey> {
2707 pub fn mock() -> Validator<BLSPubKey> {
2708 let val = TestValidator::random();
2709 let rng = &mut rand::thread_rng();
2710 let mut seed = [1u8; 32];
2711 rng.fill_bytes(&mut seed);
2712 let mut validator_stake = alloy::primitives::U256::from(0);
2713 let mut delegators = HashMap::new();
2714 for _i in 0..=5000 {
2715 let stake: u64 = rng.gen_range(0..10000);
2716 delegators.insert(Address::random(), alloy::primitives::U256::from(stake));
2717 validator_stake += alloy::primitives::U256::from(stake);
2718 }
2719
2720 let stake_table_key = val.bls_vk.into();
2721 let state_ver_key = val.schnorr_vk.into();
2722
2723 Validator {
2724 account: val.account,
2725 stake_table_key,
2726 state_ver_key,
2727 stake: validator_stake,
2728 commission: val.commission,
2729 delegators,
2730 }
2731 }
2732 }
2733}
2734
2735#[cfg(test)]
2736mod tests {
2737
2738 use alloy::{primitives::Address, rpc::types::Log};
2739 use hotshot_contract_adapter::stake_table::{sign_address_bls, StakeTableContractVersion};
2740 use hotshot_types::signature_key::BLSKeyPair;
2741 use pretty_assertions::assert_matches;
2742 use rstest::rstest;
2743
2744 use super::*;
2745 use crate::{v0::impls::testing::*, L1ClientOptions};
2746
2747 #[test_log::test]
2748 fn test_from_l1_events() -> anyhow::Result<()> {
2749 let val_1 = TestValidator::random();
2751 let val_1_new_keys = val_1.randomize_keys();
2752 let val_2 = TestValidator::random();
2753 let val_2_new_keys = val_2.randomize_keys();
2754 let delegator = Address::random();
2755 let mut events: Vec<StakeTableEvent> = [
2756 ValidatorRegistered::from(&val_1).into(),
2757 ValidatorRegisteredV2::from(&val_2).into(),
2758 Delegated {
2759 delegator,
2760 validator: val_1.account,
2761 amount: U256::from(10),
2762 }
2763 .into(),
2764 ConsensusKeysUpdated::from(&val_1_new_keys).into(),
2765 ConsensusKeysUpdatedV2::from(&val_2_new_keys).into(),
2766 Undelegated {
2767 delegator,
2768 validator: val_1.account,
2769 amount: U256::from(7),
2770 }
2771 .into(),
2772 Delegated {
2774 delegator,
2775 validator: val_1.account,
2776 amount: U256::from(5),
2777 }
2778 .into(),
2779 Delegated {
2781 delegator: Address::random(),
2782 validator: val_2.account,
2783 amount: U256::from(3),
2784 }
2785 .into(),
2786 ]
2787 .to_vec();
2788
2789 let validators_set = validator_set_from_l1_events(events.iter().cloned())?;
2790 let st = validators_set.active_validators;
2791 let st_val_1 = st.get(&val_1.account).unwrap();
2792 assert_eq!(st_val_1.stake, U256::from(8));
2794 assert_eq!(st_val_1.commission, val_1.commission);
2795 assert_eq!(st_val_1.delegators.len(), 1);
2796 assert_eq!(*st_val_1.delegators.get(&delegator).unwrap(), U256::from(8));
2798
2799 let st_val_2 = st.get(&val_2.account).unwrap();
2800 assert_eq!(st_val_2.stake, U256::from(3));
2801 assert_eq!(st_val_2.commission, val_2.commission);
2802 assert_eq!(st_val_2.delegators.len(), 1);
2803
2804 events.push(ValidatorExit::from(&val_1).into());
2805
2806 let validator_set = validator_set_from_l1_events(events.iter().cloned())?;
2807 let st = validator_set.active_validators;
2808 assert_eq!(st.get(&val_1.account), None);
2810
2811 let st_val_2 = st.get(&val_2.account).unwrap();
2813 assert_eq!(st_val_2.stake, U256::from(3));
2814 assert_eq!(st_val_2.commission, val_2.commission);
2815 assert_eq!(st_val_2.delegators.len(), 1);
2816
2817 events.push(ValidatorExit::from(&val_2).into());
2819
2820 assert!(validator_set_from_l1_events(events.iter().cloned()).is_err());
2822
2823 Ok(())
2824 }
2825
2826 #[test]
2827 fn test_from_l1_events_failures() -> anyhow::Result<()> {
2828 let val = TestValidator::random();
2829 let delegator = Address::random();
2830
2831 let register: StakeTableEvent = ValidatorRegistered::from(&val).into();
2832 let register_v2: StakeTableEvent = ValidatorRegisteredV2::from(&val).into();
2833 let delegate: StakeTableEvent = Delegated {
2834 delegator,
2835 validator: val.account,
2836 amount: U256::from(10),
2837 }
2838 .into();
2839 let key_update: StakeTableEvent = ConsensusKeysUpdated::from(&val).into();
2840 let key_update_v2: StakeTableEvent = ConsensusKeysUpdatedV2::from(&val).into();
2841 let undelegate: StakeTableEvent = Undelegated {
2842 delegator,
2843 validator: val.account,
2844 amount: U256::from(7),
2845 }
2846 .into();
2847
2848 let exit: StakeTableEvent = ValidatorExit::from(&val).into();
2849
2850 let cases = [
2851 vec![exit],
2852 vec![undelegate.clone()],
2853 vec![delegate.clone()],
2854 vec![key_update],
2855 vec![key_update_v2],
2856 vec![register.clone(), register.clone()],
2857 vec![register_v2.clone(), register_v2.clone()],
2858 vec![register.clone(), register_v2.clone()],
2859 vec![register_v2.clone(), register.clone()],
2860 vec![
2861 register,
2862 delegate.clone(),
2863 undelegate.clone(),
2864 undelegate.clone(),
2865 ],
2866 vec![register_v2, delegate, undelegate.clone(), undelegate],
2867 ];
2868
2869 for events in cases.iter() {
2870 let res = validators_from_l1_events(events.iter().cloned());
2874 assert!(
2875 res.is_err(),
2876 "events {res:?}, not a valid sequence of events"
2877 );
2878 }
2879 Ok(())
2880 }
2881
2882 #[test]
2883 fn test_validators_selection() {
2884 let mut validators = IndexMap::new();
2885 let mut highest_stake = alloy::primitives::U256::ZERO;
2886
2887 for _i in 0..3000 {
2888 let validator = Validator::mock();
2889 validators.insert(validator.account, validator.clone());
2890
2891 if validator.stake > highest_stake {
2892 highest_stake = validator.stake;
2893 }
2894 }
2895
2896 let minimum_stake = highest_stake / U256::from(VID_TARGET_TOTAL_STAKE);
2897
2898 let selected_validators =
2899 select_active_validator_set(&validators).expect("Failed to select validators");
2900 assert!(
2901 selected_validators.len() <= 100,
2902 "validators len is {}, expected at most 100",
2903 selected_validators.len()
2904 );
2905
2906 let mut selected_validators_highest_stake = alloy::primitives::U256::ZERO;
2907 for (address, validator) in &selected_validators {
2909 assert!(
2910 validator.stake >= minimum_stake,
2911 "Validator {:?} has stake below minimum: {}",
2912 address,
2913 validator.stake
2914 );
2915
2916 if validator.stake > selected_validators_highest_stake {
2917 selected_validators_highest_stake = validator.stake;
2918 }
2919 }
2920 }
2921
2922 #[rstest::rstest]
2925 fn test_regression_non_unique_bls_keys_not_discarded(
2926 #[values(StakeTableContractVersion::V1, StakeTableContractVersion::V2)]
2927 version: StakeTableContractVersion,
2928 ) {
2929 let val = TestValidator::random();
2930 let register: StakeTableEvent = match version {
2931 StakeTableContractVersion::V1 => ValidatorRegistered::from(&val).into(),
2932 StakeTableContractVersion::V2 => ValidatorRegisteredV2::from(&val).into(),
2933 };
2934 let delegate: StakeTableEvent = Delegated {
2935 delegator: Address::random(),
2936 validator: val.account,
2937 amount: U256::from(10),
2938 }
2939 .into();
2940
2941 assert!(
2943 validator_set_from_l1_events(vec![register.clone(), delegate.clone()].into_iter())
2944 .is_ok()
2945 );
2946
2947 let key_update = ConsensusKeysUpdated::from(&val).into();
2949 let err = validator_set_from_l1_events(vec![register, delegate, key_update].into_iter())
2950 .unwrap_err();
2951
2952 let bls: BLSPubKey = val.bls_vk.into();
2953 assert!(matches!(err, StakeTableError::BlsKeyAlreadyUsed(addr) if addr == bls.to_string()));
2954 }
2955
2956 #[test]
2959 fn test_regression_reregister_eth_account() {
2960 let val1 = TestValidator::random();
2961 let val2 = val1.randomize_keys();
2962 let account = val1.account;
2963
2964 let register1 = ValidatorRegisteredV2::from(&val1).into();
2965 let deregister1 = ValidatorExit::from(&val1).into();
2966 let register2 = ValidatorRegisteredV2::from(&val2).into();
2967 let events = [register1, deregister1, register2];
2968 let error = validators_from_l1_events(events.iter().cloned()).unwrap_err();
2969 assert_matches!(error, StakeTableError::ValidatorAlreadyExited(addr) if addr == account);
2970 }
2971
2972 #[test]
2973 fn test_display_log() {
2974 let serialized = r#"{"address":"0x0000000000000000000000000000000000000069",
2975 "topics":["0x0000000000000000000000000000000000000000000000000000000000000069"],
2976 "data":"0x69",
2977 "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2978 "blockNumber":"0x69","blockTimestamp":"0x69",
2979 "transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2980 "transactionIndex":"0x69","logIndex":"0x70","removed":false}"#;
2981 let log: Log = serde_json::from_str(serialized).unwrap();
2982 assert_eq!(
2983 log.display(),
2984 "Log(block=105,index=112,\
2985 transaction_hash=0x0000000000000000000000000000000000000000000000000000000000000069)"
2986 )
2987 }
2988
2989 #[rstest]
2990 #[case::v1(StakeTableContractVersion::V1)]
2991 #[case::v2(StakeTableContractVersion::V2)]
2992 fn test_register_validator(#[case] version: StakeTableContractVersion) {
2993 let mut state = StakeTableState::default();
2994 let validator = TestValidator::random();
2995
2996 let event = match version {
2997 StakeTableContractVersion::V1 => StakeTableEvent::Register((&validator).into()),
2998 StakeTableContractVersion::V2 => StakeTableEvent::RegisterV2((&validator).into()),
2999 };
3000
3001 state.apply_event(event).unwrap().unwrap();
3002
3003 let stored = state.validators.get(&validator.account).unwrap();
3004 assert_eq!(stored.account, validator.account);
3005 }
3006
3007 #[rstest]
3008 #[case::v1(StakeTableContractVersion::V1)]
3009 #[case::v2(StakeTableContractVersion::V2)]
3010 fn test_validator_already_registered(#[case] version: StakeTableContractVersion) {
3011 let mut stake_table_state = StakeTableState::default();
3012
3013 let test_validator = TestValidator::random();
3014
3015 match version {
3017 StakeTableContractVersion::V1 => {
3018 stake_table_state.apply_event(StakeTableEvent::Register((&test_validator).into()))
3019 },
3020 StakeTableContractVersion::V2 => {
3021 stake_table_state.apply_event(StakeTableEvent::RegisterV2((&test_validator).into()))
3022 },
3023 }
3024 .unwrap()
3025 .unwrap(); let v1_already_registered_result = stake_table_state
3029 .clone()
3030 .apply_event(StakeTableEvent::Register((&test_validator).into()));
3031
3032 pretty_assertions::assert_matches!(
3033 v1_already_registered_result, Err(StakeTableError::AlreadyRegistered(account))
3034 if account == test_validator.account,
3035 "Expected AlreadyRegistered error. version ={version:?} result={v1_already_registered_result:?}",
3036 );
3037
3038 let v2_already_registered_result = stake_table_state
3040 .clone()
3041 .apply_event(StakeTableEvent::RegisterV2((&test_validator).into()));
3042
3043 pretty_assertions::assert_matches!(
3044 v2_already_registered_result,
3045 Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
3046 "Expected AlreadyRegistered error. version ={version:?} result={v2_already_registered_result:?}",
3047
3048 );
3049 }
3050
3051 #[test]
3052 fn test_register_validator_v2_auth_fails() {
3053 let mut state = StakeTableState::default();
3054 let mut val = TestValidator::random();
3055 val.bls_sig = Default::default();
3056 let event = StakeTableEvent::RegisterV2((&val).into());
3057
3058 let result = state.apply_event(event);
3059 assert!(matches!(
3060 result,
3061 Err(StakeTableError::AuthenticationFailed(_))
3062 ));
3063 }
3064
3065 #[rstest]
3066 #[case::v1(StakeTableContractVersion::V1)]
3067 #[case::v2(StakeTableContractVersion::V2)]
3068 fn test_deregister_validator(#[case] version: StakeTableContractVersion) {
3069 let mut state = StakeTableState::default();
3070 let val = TestValidator::random();
3071
3072 let reg = StakeTableEvent::Register((&val).into());
3073 state.apply_event(reg).unwrap().unwrap();
3074
3075 let dereg = match version {
3076 StakeTableContractVersion::V1 => StakeTableEvent::Deregister((&val).into()),
3077 StakeTableContractVersion::V2 => StakeTableEvent::DeregisterV2(ValidatorExitV2 {
3078 validator: val.account,
3079 unlocksAt: U256::from(1000u64),
3080 }),
3081 };
3082 state.apply_event(dereg).unwrap().unwrap();
3083 assert!(!state.validators.contains_key(&val.account));
3084 }
3085
3086 #[rstest]
3087 #[case::v1(StakeTableContractVersion::V1)]
3088 #[case::v2(StakeTableContractVersion::V2)]
3089 fn test_delegate_and_undelegate(#[case] version: StakeTableContractVersion) {
3090 let mut state = StakeTableState::default();
3091 let val = TestValidator::random();
3092 state
3093 .apply_event(StakeTableEvent::Register((&val).into()))
3094 .unwrap()
3095 .unwrap();
3096
3097 let delegator = Address::random();
3098 let amount = U256::from(1000);
3099 let delegate_event = StakeTableEvent::Delegate(Delegated {
3100 delegator,
3101 validator: val.account,
3102 amount,
3103 });
3104 state.apply_event(delegate_event).unwrap().unwrap();
3105
3106 let validator = state.validators.get(&val.account).unwrap();
3107 assert_eq!(validator.delegators.get(&delegator).cloned(), Some(amount));
3108
3109 let undelegate_event = match version {
3110 StakeTableContractVersion::V1 => StakeTableEvent::Undelegate(Undelegated {
3111 delegator,
3112 validator: val.account,
3113 amount,
3114 }),
3115 StakeTableContractVersion::V2 => StakeTableEvent::UndelegateV2(UndelegatedV2 {
3116 delegator,
3117 validator: val.account,
3118 amount,
3119 unlocksAt: U256::from(2000u64),
3120 undelegationId: 1,
3121 }),
3122 };
3123 state.apply_event(undelegate_event).unwrap().unwrap();
3124 let validator = state.validators.get(&val.account).unwrap();
3125 assert!(!validator.delegators.contains_key(&delegator));
3126 }
3127
3128 #[rstest]
3129 #[case::v1(StakeTableContractVersion::V1)]
3130 #[case::v2(StakeTableContractVersion::V2)]
3131 fn test_key_update_event(#[case] version: StakeTableContractVersion) {
3132 let mut state = StakeTableState::default();
3133 let val = TestValidator::random();
3134
3135 state
3137 .apply_event(StakeTableEvent::Register((&val).into()))
3138 .unwrap()
3139 .unwrap();
3140
3141 let new_keys = val.randomize_keys();
3142
3143 let event = match version {
3144 StakeTableContractVersion::V1 => StakeTableEvent::KeyUpdate((&new_keys).into()),
3145 StakeTableContractVersion::V2 => StakeTableEvent::KeyUpdateV2((&new_keys).into()),
3146 };
3147
3148 state.apply_event(event).unwrap().unwrap();
3149
3150 let updated = state.validators.get(&val.account).unwrap();
3151 assert_eq!(updated.stake_table_key, new_keys.bls_vk.into());
3152 assert_eq!(updated.state_ver_key, new_keys.schnorr_vk.into());
3153 }
3154
3155 #[test]
3156 fn test_duplicate_bls_key() {
3157 let mut state = StakeTableState::default();
3158 let val = TestValidator::random();
3159 let event1 = StakeTableEvent::Register((&val).into());
3160 let mut val2 = TestValidator::random();
3161 val2.bls_vk = val.bls_vk;
3162 val2.account = Address::random();
3163
3164 let event2 = StakeTableEvent::Register((&val2).into());
3165 state.apply_event(event1).unwrap().unwrap();
3166 let result = state.apply_event(event2);
3167
3168 let expected_bls_key = BLSPubKey::from(val.bls_vk).to_string();
3169
3170 assert_matches!(
3171 result,
3172 Err(StakeTableError::BlsKeyAlreadyUsed(key))
3173 if key == expected_bls_key,
3174 "Expected BlsKeyAlreadyUsed({expected_bls_key}), but got: {result:?}",
3175 );
3176 }
3177
3178 #[test]
3179 fn test_duplicate_schnorr_key() {
3180 let mut state = StakeTableState::default();
3181 let val = TestValidator::random();
3182 let event1 = StakeTableEvent::Register((&val).into());
3183 let mut val2 = TestValidator::random();
3184 val2.schnorr_vk = val.schnorr_vk;
3185 val2.account = Address::random();
3186 val2.bls_vk = val2.randomize_keys().bls_vk;
3187
3188 let event2 = StakeTableEvent::Register((&val2).into());
3189 state.apply_event(event1).unwrap().unwrap();
3190 let result = state.apply_event(event2);
3191
3192 let schnorr: SchnorrPubKey = val.schnorr_vk.into();
3193 assert_matches!(
3194 result,
3195 Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(key)))
3196 if key == schnorr.to_string(),
3197 "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
3198
3199 );
3200 }
3201
3202 #[test]
3203 fn test_duplicate_schnorr_key_v2_during_update() {
3204 let mut state = StakeTableState::default();
3205
3206 let val1 = TestValidator::random();
3207
3208 let mut rng = &mut rand::thread_rng();
3209 let bls_key_pair = BLSKeyPair::generate(&mut rng);
3210
3211 let val2 = TestValidator {
3212 account: val1.account,
3213 bls_vk: bls_key_pair.ver_key().to_affine().into(),
3214 schnorr_vk: val1.schnorr_vk,
3215 commission: val1.commission,
3216 bls_sig: sign_address_bls(&bls_key_pair, val1.account).into(),
3217 schnorr_sig: val1.clone().schnorr_sig,
3218 };
3219 let event1 = StakeTableEvent::RegisterV2((&val1).into());
3220 let event2 = StakeTableEvent::KeyUpdateV2((&val2).into());
3221
3222 state.apply_event(event1).unwrap().unwrap();
3223 let result = state.apply_event(event2);
3224
3225 let schnorr: SchnorrPubKey = val1.schnorr_vk.into();
3226 assert_matches!(
3227 result,
3228 Err(StakeTableError::SchnorrKeyAlreadyUsed(key))
3229 if key == schnorr.to_string(),
3230 "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
3231 );
3232 }
3233
3234 #[test]
3235 fn test_register_and_deregister_validator() {
3236 let mut state = StakeTableState::default();
3237 let validator = TestValidator::random();
3238 let event = StakeTableEvent::Register((&validator).into());
3239 state.apply_event(event).unwrap().unwrap();
3240
3241 let deregister_event = StakeTableEvent::Deregister((&validator).into());
3242 assert!(state.apply_event(deregister_event).unwrap().is_ok());
3243 }
3244
3245 #[test]
3246 fn test_commission_validation_exceeds_basis_points() {
3247 let validator = TestValidator::random();
3249 let mut stake_table = StakeTableState::default();
3250
3251 let registration_event = ValidatorRegistered::from(&validator).into();
3253 stake_table
3254 .apply_event(registration_event)
3255 .unwrap()
3256 .unwrap();
3257
3258 let valid_commission_event = CommissionUpdated {
3260 validator: validator.account,
3261 timestamp: Default::default(),
3262 oldCommission: 0,
3263 newCommission: COMMISSION_BASIS_POINTS, }
3265 .into();
3266 stake_table
3267 .apply_event(valid_commission_event)
3268 .unwrap()
3269 .unwrap();
3270
3271 let invalid_commission = COMMISSION_BASIS_POINTS + 1;
3272 let invalid_commission_event = CommissionUpdated {
3273 validator: validator.account,
3274 timestamp: Default::default(),
3275 oldCommission: 0,
3276 newCommission: invalid_commission,
3277 }
3278 .into();
3279
3280 let err = stake_table
3281 .apply_event(invalid_commission_event)
3282 .unwrap_err();
3283
3284 assert_matches!(
3285 err,
3286 StakeTableError::InvalidCommission(addr, invalid_commission)
3287 if addr == addr && invalid_commission == invalid_commission);
3288 }
3289
3290 #[test]
3291 fn test_delegate_zero_amount_is_rejected() {
3292 let mut state = StakeTableState::default();
3293 let validator = TestValidator::random();
3294 let account = validator.account;
3295 state
3296 .apply_event(StakeTableEvent::Register((&validator).into()))
3297 .unwrap()
3298 .unwrap();
3299
3300 let delegator = Address::random();
3301 let amount = U256::ZERO;
3302 let event = StakeTableEvent::Delegate(Delegated {
3303 delegator,
3304 validator: account,
3305 amount,
3306 });
3307 let result = state.apply_event(event);
3308
3309 assert_matches!(
3310 result,
3311 Err(StakeTableError::ZeroDelegatorStake(addr))
3312 if addr == delegator,
3313 "delegator stake is zero"
3314
3315 );
3316 }
3317
3318 #[test]
3319 fn test_undelegate_more_than_stake_fails() {
3320 let mut state = StakeTableState::default();
3321 let validator = TestValidator::random();
3322 let account = validator.account;
3323 state
3324 .apply_event(StakeTableEvent::Register((&validator).into()))
3325 .unwrap()
3326 .unwrap();
3327
3328 let delegator = Address::random();
3329 let event = StakeTableEvent::Delegate(Delegated {
3330 delegator,
3331 validator: account,
3332 amount: U256::from(10u64),
3333 });
3334 state.apply_event(event).unwrap().unwrap();
3335
3336 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3337 delegator,
3338 validator: account,
3339 amount: U256::from(20u64),
3340 }));
3341 assert_matches!(
3342 result,
3343 Err(StakeTableError::InsufficientStake),
3344 "Expected InsufficientStake error, got: {result:?}",
3345 );
3346 }
3347
3348 #[test]
3349 fn test_apply_event_does_not_modify_state_on_error() {
3350 let mut state = StakeTableState::default();
3351 let validator = TestValidator::random();
3352 let delegator = Address::random();
3353
3354 state
3355 .apply_event(StakeTableEvent::Register((&validator).into()))
3356 .unwrap()
3357 .unwrap();
3358
3359 let state_before = state.clone();
3361 let result = state.apply_event(StakeTableEvent::Register((&validator).into()));
3362 assert_matches!(result, Err(StakeTableError::AlreadyRegistered(_)));
3363 assert_eq!(
3364 state, state_before,
3365 "State should not change on AlreadyRegistered error"
3366 );
3367
3368 let state_before = state.clone();
3370 let mut validator2 = TestValidator::random();
3371 validator2.bls_vk = validator.bls_vk; let result = state.apply_event(StakeTableEvent::Register((&validator2).into()));
3373 assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
3374 assert_eq!(
3375 state, state_before,
3376 "State should not change on BlsKeyAlreadyUsed error"
3377 );
3378
3379 let state_before = state.clone();
3381 let nonexistent_validator = TestValidator::random();
3382 let result =
3383 state.apply_event(StakeTableEvent::Deregister((&nonexistent_validator).into()));
3384 assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
3385 assert_eq!(
3386 state, state_before,
3387 "State should not change on ValidatorNotFound error"
3388 );
3389
3390 let state_before = state.clone();
3392 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3393 delegator: Address::random(),
3394 validator: Address::random(),
3395 amount: U256::from(100u64),
3396 }));
3397 assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
3398 assert_eq!(
3399 state, state_before,
3400 "State should not change on ValidatorNotFound error for Undelegate"
3401 );
3402
3403 state
3404 .apply_event(StakeTableEvent::Delegate(Delegated {
3405 delegator,
3406 validator: validator.account,
3407 amount: U256::from(100u64),
3408 }))
3409 .unwrap()
3410 .unwrap();
3411
3412 let state_before = state.clone();
3414 let non_existent_delegator = Address::random();
3415 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3416 delegator: non_existent_delegator,
3417 validator: validator.account,
3418 amount: U256::from(50u64),
3419 }));
3420 assert_matches!(result, Err(StakeTableError::DelegatorNotFound(_)));
3421 assert_eq!(
3422 state, state_before,
3423 "State should not change on DelegatorNotFound error"
3424 );
3425
3426 let state_before = state.clone();
3428 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3429 delegator,
3430 validator: validator.account,
3431 amount: U256::from(200u64),
3432 }));
3433 assert_matches!(result, Err(StakeTableError::InsufficientStake));
3434 assert_eq!(
3435 state, state_before,
3436 "State should not change on InsufficientStake error"
3437 );
3438
3439 let validator2 = TestValidator::random();
3441 let delegator2 = Address::random();
3442
3443 state
3444 .apply_event(StakeTableEvent::Register((&validator2).into()))
3445 .unwrap()
3446 .unwrap();
3447
3448 state
3449 .apply_event(StakeTableEvent::Delegate(Delegated {
3450 delegator: delegator2,
3451 validator: validator2.account,
3452 amount: U256::from(50u64),
3453 }))
3454 .unwrap()
3455 .unwrap();
3456 let state_before = state.clone();
3457 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3458 delegator: delegator2,
3459 validator: validator2.account,
3460 amount: U256::from(100u64),
3461 }));
3462 assert_matches!(result, Err(StakeTableError::InsufficientStake));
3463 assert_eq!(state, state_before,);
3464
3465 let state_before = state.clone();
3467 let result = state.apply_event(StakeTableEvent::Delegate(Delegated {
3468 delegator: Address::random(),
3469 validator: validator.account,
3470 amount: U256::ZERO,
3471 }));
3472 assert_matches!(result, Err(StakeTableError::ZeroDelegatorStake(_)));
3473 assert_eq!(
3474 state, state_before,
3475 "State should not change on ZeroDelegatorStake error"
3476 );
3477 }
3478
3479 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3480 async fn test_decaf_stake_table() {
3481 let events_json =
3517 std::fs::read_to_string("../data/v3/decaf_stake_table_events.json").unwrap();
3518 let events: Vec<(EventKey, StakeTableEvent)> = serde_json::from_str(&events_json).unwrap();
3519
3520 let reconstructed_stake_table =
3522 validator_set_from_l1_events(events.into_iter().map(|(_, e)| e))
3523 .unwrap()
3524 .active_validators;
3525
3526 let stake_table_json =
3527 std::fs::read_to_string("../data/v3/decaf_stake_table.json").unwrap();
3528 let expected: IndexMap<Address, Validator<BLSPubKey>> =
3529 serde_json::from_str(&stake_table_json).unwrap();
3530
3531 assert_eq!(
3532 reconstructed_stake_table, expected,
3533 "Stake table reconstructed from events does not match the expected stake table "
3534 );
3535 }
3536
3537 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3538 #[should_panic]
3539 async fn test_large_max_events_range_panic() {
3540 let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
3542
3543 let l1 = L1ClientOptions {
3544 l1_events_max_retry_duration: Duration::from_secs(30),
3545 l1_events_max_block_range: 10_u64.pow(9),
3547 l1_retry_delay: Duration::from_secs(1),
3548 ..Default::default()
3549 }
3550 .connect(vec!["https://ethereum-sepolia.publicnode.com"
3551 .parse()
3552 .unwrap()])
3553 .expect("unable to construct l1 client");
3554
3555 let latest_block = l1.provider.get_block_number().await.unwrap();
3556 let _events = Fetcher::fetch_events_from_contract(
3557 l1,
3558 contract_address.parse().unwrap(),
3559 None,
3560 latest_block,
3561 )
3562 .await
3563 .unwrap();
3564 }
3565
3566 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3567 async fn sanity_check_block_reward_v3() {
3568 let initial_supply = U256::from_str("10000000000000000000000000000").unwrap();
3570
3571 let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
3572 .checked_div(U256::from(COMMISSION_BASIS_POINTS))
3573 .unwrap();
3574
3575 println!("Calculated reward: {reward}");
3576 assert!(reward > U256::ZERO);
3577 }
3578
3579 #[test]
3580 fn sanity_check_p_and_rp() {
3581 let total_stake = BigDecimal::from_str("1000").unwrap();
3582 let total_supply = BigDecimal::from_str("10000").unwrap(); let (p, rp) =
3585 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3586
3587 assert!(p > BigDecimal::from(0));
3588 assert!(p < BigDecimal::from(1));
3589 assert!(rp > BigDecimal::from(0));
3590 }
3591
3592 #[test]
3593 fn test_p_out_of_range() {
3594 let total_stake = BigDecimal::from_str("1000").unwrap();
3595 let total_supply = BigDecimal::from_str("500").unwrap(); let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3598 assert!(result.is_err());
3599 }
3600
3601 #[test]
3602 fn test_zero_total_supply() {
3603 let total_stake = BigDecimal::from_str("1000").unwrap();
3604 let total_supply = BigDecimal::from(0);
3605
3606 let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3607 assert!(result.is_err());
3608 }
3609
3610 #[test]
3611 fn test_valid_p_and_rp() {
3612 let total_stake = BigDecimal::from_str("5000").unwrap();
3613 let total_supply = BigDecimal::from_str("10000").unwrap();
3614
3615 let (p, rp) =
3616 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3617
3618 assert_eq!(p, BigDecimal::from_str("0.5").unwrap());
3619 assert!(rp > BigDecimal::from_str("0.0").unwrap());
3620 }
3621
3622 #[test]
3623 fn test_very_small_p() {
3624 let total_stake = BigDecimal::from_str("1").unwrap(); let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap(); let (p, rp) =
3628 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3629
3630 assert!(p > BigDecimal::from_str("0").unwrap());
3631 assert!(p < BigDecimal::from_str("1e-18").unwrap()); assert!(rp > BigDecimal::from(0));
3633 }
3634
3635 #[test]
3636 fn test_p_very_close_to_one() {
3637 let total_stake = BigDecimal::from_str("9999999999999999999999999999").unwrap();
3638 let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap();
3639
3640 let (p, rp) =
3641 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3642
3643 assert!(p < BigDecimal::from(1));
3644 assert!(p > BigDecimal::from_str("0.999999999999999999999999999").unwrap());
3645 assert!(rp > BigDecimal::from(0));
3646 }
3647
3648 #[test]
3652 fn test_reward_rate_rp() {
3653 let test_cases = [
3654 ("0.0000", "0.2121"), ("0.0050", "0.2121"), ("0.0100", "0.2121"), ("0.0250", "0.1342"), ("0.0500", "0.0949"), ("0.1000", "0.0671"), ("0.2500", "0.0424"), ("0.5000", "0.0300"), ("0.7500", "0.0245"), ("1.0000", "0.0212"), ];
3666
3667 let tolerance = BigDecimal::from_str("0.0001").unwrap();
3668
3669 let total_supply = BigDecimal::from_u32(10_000).unwrap();
3670
3671 for (p, rp) in test_cases {
3672 let p = BigDecimal::from_str(p).unwrap();
3673 let expected_rp = BigDecimal::from_str(rp).unwrap();
3674
3675 let total_stake = &p * &total_supply;
3676
3677 let (computed_p, computed_rp) =
3678 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3679
3680 assert!(
3681 (&computed_p - &p).abs() < tolerance,
3682 "p mismatch: got {computed_p}, expected {p}"
3683 );
3684
3685 assert!(
3686 (&computed_rp - &expected_rp).abs() < tolerance,
3687 "R(p) mismatch for p={p}: got {computed_rp}, expected {expected_rp}"
3688 );
3689 }
3690 }
3691
3692 #[tokio::test(flavor = "multi_thread")]
3693 async fn test_dynamic_block_reward_with_expected_values() {
3694 let total_supply = U256::from_str("10000000000000000000000000000").unwrap();
3696 let total_supply_bd = BigDecimal::from_str(&total_supply.to_string()).unwrap();
3697
3698 let test_cases = [
3699 ("0.0000", "0.2121", 1, "0"), ("0.0050", "0.2121", 1, "3362823439878234"), ("0.0100", "0.2121", 1, "6725646879756468"), ("0.0250", "0.1342", 1, "10638635210553018"), ("0.0500", "0.0949", 1, "15046296296296296"), ("0.1000", "0.0671", 1, "21277270421106037"), ("0.2500", "0.0424", 1, "33612379502790461"), ("0.5000", "0.0300", 1, "47564687975646879"), ("0.7500", "0.0245", 1, "58266742770167427"), ("1.0000", "0.0212", 1, "67224759005580923"), ("0.0000", "0.2121", 2000, "0"), ("0.0050", "0.2121", 2000, "672564687975646880"), ("0.0100", "0.2121", 2000, "1345129375951293760"), ("0.0250", "0.1342", 2000, "2127727042110603754"), ("0.0500", "0.0949", 2000, "3009259259259259259"), ("0.1000", "0.0671", 2000, "4255454084221207509"), ("0.2500", "0.0424", 2000, "6722475900558092339"), ("0.5000", "0.0300", 2000, "9512937595129375951"), ("0.7500", "0.0245", 2000, "11653348554033485540"), ("1.0000", "0.0212", 2000, "13444951801116184678"), ("0.0000", "0.2121", 10000, "0"), ("0.0050", "0.2121", 10000, "3362823439878234400"), ("0.0100", "0.2121", 10000, "6725646879756468800"), ("0.0250", "0.1342", 10000, "10638635210553018770"), ("0.0500", "0.0949", 10000, "15046296296296296295"), ("0.1000", "0.0671", 10000, "21277270421106037545"), ("0.2500", "0.0424", 10000, "33612379502790461695"), ("0.5000", "0.0300", 10000, "47564687975646879755"), ("0.7500", "0.0245", 10000, "58266742770167427700"), ("1.0000", "0.0212", 10000, "67224759005580923390"), ];
3733
3734 let tolerance = U256::from(100_000_000_000_000_000u128); for (p, rp, avg_block_time_ms, expected_reward) in test_cases {
3737 let p = BigDecimal::from_str(p).unwrap();
3738 let total_stake_bd = (&p * &total_supply_bd).round(0);
3739 println!("total_stake_bd={total_stake_bd}");
3740
3741 let total_stake = U256::from_str(&total_stake_bd.to_plain_string()).unwrap();
3742 let expected_reward = U256::from_str(expected_reward).unwrap();
3743
3744 let epoch = EpochNumber::new(0);
3745 let actual_reward = EpochCommittees::compute_block_reward(
3746 &epoch,
3747 total_supply,
3748 total_stake,
3749 avg_block_time_ms,
3750 )
3751 .unwrap()
3752 .0;
3753
3754 let diff = if actual_reward > expected_reward {
3755 actual_reward - expected_reward
3756 } else {
3757 expected_reward - actual_reward
3758 };
3759
3760 assert!(
3761 diff <= tolerance,
3762 "Reward mismatch for p = {p}, R(p) = {rp}, block_time = {avg_block_time_ms}: \
3763 expected = {expected_reward}, actual = {actual_reward}, diff = {diff}"
3764 );
3765 }
3766 }
3767}