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() -> Self {
249 Self {
250 validators: IndexMap::new(),
251 validator_exits: HashSet::new(),
252 used_bls_keys: HashSet::new(),
253 used_schnorr_keys: HashSet::new(),
254 }
255 }
256
257 pub fn validators(&self) -> &ValidatorMap {
258 &self.validators
259 }
260
261 pub fn into_validators(self) -> ValidatorMap {
262 self.validators
263 }
264
265 pub fn apply_event(&mut self, event: StakeTableEvent) -> ApplyEventResult<()> {
271 match event {
272 StakeTableEvent::Register(ValidatorRegistered {
273 account,
274 blsVk,
275 schnorrVk,
276 commission,
277 }) => {
278 let stake_table_key: BLSPubKey = blsVk.into();
279 let state_ver_key: SchnorrPubKey = schnorrVk.into();
280
281 if self.validator_exits.contains(&account) {
282 return Err(StakeTableError::ValidatorAlreadyExited(account));
283 }
284
285 let entry = self.validators.entry(account);
286 if let indexmap::map::Entry::Occupied(_) = entry {
287 return Err(StakeTableError::AlreadyRegistered(account));
288 }
289
290 if self.used_bls_keys.contains(&stake_table_key) {
292 return Err(StakeTableError::BlsKeyAlreadyUsed(
293 stake_table_key.to_string(),
294 ));
295 }
296
297 if self.used_schnorr_keys.contains(&state_ver_key) {
299 return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
300 state_ver_key.to_string(),
301 )));
302 }
303
304 self.used_bls_keys.insert(stake_table_key);
306 self.used_schnorr_keys.insert(state_ver_key.clone());
307
308 entry.or_insert(Validator {
309 account,
310 stake_table_key,
311 state_ver_key,
312 stake: U256::ZERO,
313 commission,
314 delegators: HashMap::new(),
315 });
316 },
317
318 StakeTableEvent::RegisterV2(reg) => {
319 reg.authenticate()
322 .map_err(|e| StakeTableError::AuthenticationFailed(e.to_string()))?;
323
324 let ValidatorRegisteredV2 {
325 account,
326 blsVK,
327 schnorrVK,
328 commission,
329 ..
330 } = reg;
331
332 let stake_table_key: BLSPubKey = blsVK.into();
333 let state_ver_key: SchnorrPubKey = schnorrVK.into();
334
335 if self.validator_exits.contains(&account) {
337 return Err(StakeTableError::ValidatorAlreadyExited(account));
338 }
339
340 let entry = self.validators.entry(account);
341 if let indexmap::map::Entry::Occupied(_) = entry {
342 return Err(StakeTableError::AlreadyRegistered(account));
343 }
344
345 if self.used_bls_keys.contains(&stake_table_key) {
347 return Err(StakeTableError::BlsKeyAlreadyUsed(
348 stake_table_key.to_string(),
349 ));
350 }
351
352 if self.used_schnorr_keys.contains(&state_ver_key) {
354 return Err(StakeTableError::SchnorrKeyAlreadyUsed(
355 state_ver_key.to_string(),
356 ));
357 }
358
359 self.used_bls_keys.insert(stake_table_key);
361 self.used_schnorr_keys.insert(state_ver_key.clone());
362
363 entry.or_insert(Validator {
364 account,
365 stake_table_key,
366 state_ver_key,
367 stake: U256::ZERO,
368 commission,
369 delegators: HashMap::new(),
370 });
371 },
372
373 StakeTableEvent::Deregister(ValidatorExit { validator })
374 | StakeTableEvent::DeregisterV2(ValidatorExitV2 { validator, .. }) => {
375 if !self.validators.contains_key(&validator) {
376 return Err(StakeTableError::ValidatorNotFound(validator));
377 }
378
379 self.validator_exits.insert(validator);
381 self.validators.shift_remove(&validator);
382 },
383
384 StakeTableEvent::Delegate(delegated) => {
385 let Delegated {
386 delegator,
387 validator,
388 amount,
389 } = delegated;
390
391 if amount.is_zero() {
393 return Err(StakeTableError::ZeroDelegatorStake(delegator));
394 }
395
396 let val = self
397 .validators
398 .get_mut(&validator)
399 .ok_or(StakeTableError::ValidatorNotFound(validator))?;
400
401 val.stake = val.stake.checked_add(amount).unwrap_or_else(|| {
404 panic!(
405 "validator stake overflow: validator={validator}, stake={}, \
406 amount={amount}",
407 val.stake
408 )
409 });
410 val.delegators
413 .entry(delegator)
414 .and_modify(|stake| {
415 *stake = stake.checked_add(amount).unwrap_or_else(|| {
416 panic!(
417 "delegator stake overflow: delegator={delegator}, stake={stake}, \
418 amount={amount}"
419 )
420 });
421 })
422 .or_insert(amount);
423 },
424
425 StakeTableEvent::Undelegate(Undelegated {
426 delegator,
427 validator,
428 amount,
429 })
430 | StakeTableEvent::UndelegateV2(UndelegatedV2 {
431 delegator,
432 validator,
433 amount,
434 ..
435 }) => {
436 let val = self
437 .validators
438 .get_mut(&validator)
439 .ok_or(StakeTableError::ValidatorNotFound(validator))?;
440
441 if val.stake < amount {
442 tracing::warn!("validator_stake={}, amount={amount}", val.stake);
443 return Err(StakeTableError::InsufficientStake);
444 }
445
446 let delegator_stake = val
447 .delegators
448 .get_mut(&delegator)
449 .ok_or(StakeTableError::DelegatorNotFound(delegator))?;
450
451 if *delegator_stake < amount {
452 tracing::warn!("delegator_stake={delegator_stake}, amount={amount}");
453 return Err(StakeTableError::InsufficientStake);
454 }
455
456 let new_delegator_stake = delegator_stake.checked_sub(amount).unwrap();
458
459 val.stake = val.stake.checked_sub(amount).unwrap();
462
463 if new_delegator_stake.is_zero() {
464 val.delegators.remove(&delegator);
465 } else {
466 *delegator_stake = new_delegator_stake;
467 }
468 },
469
470 StakeTableEvent::KeyUpdate(update) => {
471 let ConsensusKeysUpdated {
472 account,
473 blsVK,
474 schnorrVK,
475 } = update;
476
477 let stake_table_key: BLSPubKey = blsVK.into();
478 let state_ver_key: SchnorrPubKey = schnorrVK.into();
479
480 if !self.validators.contains_key(&account) {
481 return Err(StakeTableError::ValidatorNotFound(account));
482 }
483
484 if self.used_bls_keys.contains(&stake_table_key) {
485 return Err(StakeTableError::BlsKeyAlreadyUsed(
486 stake_table_key.to_string(),
487 ));
488 }
489
490 if self.used_schnorr_keys.contains(&state_ver_key) {
493 return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
494 state_ver_key.to_string(),
495 )));
496 }
497
498 self.used_bls_keys.insert(stake_table_key);
500 self.used_schnorr_keys.insert(state_ver_key.clone());
501 let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
503 panic!("validator {account} must exist after contains_key check")
504 });
505 validator.stake_table_key = stake_table_key;
506 validator.state_ver_key = state_ver_key;
507 },
508
509 StakeTableEvent::KeyUpdateV2(update) => {
510 update
513 .authenticate()
514 .map_err(|e| StakeTableError::AuthenticationFailed(e.to_string()))?;
515
516 let ConsensusKeysUpdatedV2 {
517 account,
518 blsVK,
519 schnorrVK,
520 ..
521 } = update;
522
523 let stake_table_key: BLSPubKey = blsVK.into();
524 let state_ver_key: SchnorrPubKey = schnorrVK.into();
525
526 if !self.validators.contains_key(&account) {
527 return Err(StakeTableError::ValidatorNotFound(account));
528 }
529
530 if self.used_bls_keys.contains(&stake_table_key) {
532 return Err(StakeTableError::BlsKeyAlreadyUsed(
533 stake_table_key.to_string(),
534 ));
535 }
536
537 if self.used_schnorr_keys.contains(&state_ver_key) {
539 return Err(StakeTableError::SchnorrKeyAlreadyUsed(
540 state_ver_key.to_string(),
541 ));
542 }
543
544 self.used_bls_keys.insert(stake_table_key);
546 self.used_schnorr_keys.insert(state_ver_key.clone());
547
548 let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
550 panic!("validator {account} must exist after contains_key check")
551 });
552 validator.stake_table_key = stake_table_key;
553 validator.state_ver_key = state_ver_key;
554 },
555
556 StakeTableEvent::CommissionUpdate(CommissionUpdated {
557 validator,
558 newCommission,
559 ..
560 }) => {
561 if newCommission > COMMISSION_BASIS_POINTS {
564 return Err(StakeTableError::InvalidCommission(validator, newCommission));
565 }
566
567 let val = self
571 .validators
572 .get_mut(&validator)
573 .ok_or(StakeTableError::ValidatorNotFound(validator))?;
574 val.commission = newCommission;
575 },
576 }
577
578 Ok(Ok(()))
579 }
580}
581
582pub fn validators_from_l1_events<I: Iterator<Item = StakeTableEvent>>(
583 events: I,
584) -> Result<(ValidatorMap, StakeTableHash), StakeTableError> {
585 let mut state = StakeTableState::new();
586 for event in events {
587 match state.apply_event(event.clone()) {
588 Ok(Ok(())) => {
589 },
591 Ok(Err(expected_err)) => {
592 tracing::warn!("Expected error while applying event {event:?}: {expected_err}");
594 },
595 Err(err) => {
596 tracing::error!("Fatal error in applying event {event:?}: {err}");
597 return Err(err);
598 },
599 }
600 }
601 let commit = state.commit();
602 Ok((state.into_validators(), commit))
603}
604
605pub(crate) fn select_active_validator_set(
610 validators: &ValidatorMap,
611) -> Result<ValidatorMap, StakeTableError> {
612 let total_validators = validators.len();
613
614 let valid_validators: ValidatorMap = validators
616 .iter()
617 .filter(|(address, validator)| {
618 if validator.delegators.is_empty() {
619 tracing::info!("Validator {address:?} does not have any delegator");
620 return false;
621 }
622
623 if validator.stake.is_zero() {
624 tracing::info!("Validator {address:?} does not have any stake");
625 return false;
626 }
627
628 true
629 })
630 .map(|(addr, val)| (*addr, val.clone()))
631 .collect();
632
633 tracing::debug!(
634 total_validators,
635 filtered = valid_validators.len(),
636 "Filtered out invalid validators"
637 );
638
639 if valid_validators.is_empty() {
640 tracing::warn!("Validator selection failed: no validators passed minimum criteria");
641 return Err(StakeTableError::NoValidValidators);
642 }
643
644 let maximum_stake = valid_validators.values().map(|v| v.stake).max().unwrap();
645
646 let minimum_stake = maximum_stake
647 .checked_div(U256::from(VID_TARGET_TOTAL_STAKE))
648 .ok_or_else(|| {
649 tracing::error!("Overflow while calculating minimum stake threshold");
650 StakeTableError::MinimumStakeOverflow
651 })?;
652
653 let mut valid_stakers: Vec<_> = valid_validators
654 .iter()
655 .filter(|(_, v)| v.stake >= minimum_stake)
656 .map(|(addr, v)| (*addr, v.stake))
657 .collect();
658
659 tracing::info!(
660 count = valid_stakers.len(),
661 "Number of validators above minimum stake threshold"
662 );
663
664 valid_stakers.sort_by_key(|(_, stake)| std::cmp::Reverse(*stake));
666
667 if valid_stakers.len() > 100 {
668 valid_stakers.truncate(100);
669 }
670
671 let selected_addresses: HashSet<_> = valid_stakers.iter().map(|(addr, _)| *addr).collect();
672 let selected_validators: ValidatorMap = valid_validators
673 .into_iter()
674 .filter(|(address, _)| selected_addresses.contains(address))
675 .collect();
676
677 tracing::info!(
678 final_count = selected_validators.len(),
679 "Selected active validator set"
680 );
681
682 Ok(selected_validators)
683}
684
685#[derive(Clone, Debug)]
686pub struct ValidatorSet {
687 pub all_validators: ValidatorMap,
688 pub active_validators: ValidatorMap,
689 pub stake_table_hash: Option<StakeTableHash>,
690}
691
692pub(crate) fn validator_set_from_l1_events<I: Iterator<Item = StakeTableEvent>>(
694 events: I,
695) -> Result<ValidatorSet, StakeTableError> {
696 let (all_validators, stake_table_hash) = validators_from_l1_events(events)?;
697 let active_validators = select_active_validator_set(&all_validators)?;
698
699 let validator_set = ValidatorSet {
700 all_validators,
701 active_validators,
702 stake_table_hash: Some(stake_table_hash),
703 };
704
705 Ok(validator_set)
706}
707
708impl std::fmt::Debug for StakeTableEvent {
709 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710 match self {
711 StakeTableEvent::Register(event) => write!(f, "Register({:?})", event.account),
712 StakeTableEvent::RegisterV2(event) => write!(f, "RegisterV2({:?})", event.account),
713 StakeTableEvent::Deregister(event) => write!(f, "Deregister({:?})", event.validator),
714 StakeTableEvent::DeregisterV2(event) => {
715 write!(f, "DeregisterV2({:?})", event.validator)
716 },
717 StakeTableEvent::Delegate(event) => write!(f, "Delegate({:?})", event.delegator),
718 StakeTableEvent::Undelegate(event) => write!(f, "Undelegate({:?})", event.delegator),
719 StakeTableEvent::UndelegateV2(event) => {
720 write!(f, "UndelegateV2({:?})", event.delegator)
721 },
722 StakeTableEvent::KeyUpdate(event) => write!(f, "KeyUpdate({:?})", event.account),
723 StakeTableEvent::KeyUpdateV2(event) => write!(f, "KeyUpdateV2({:?})", event.account),
724 StakeTableEvent::CommissionUpdate(event) => {
725 write!(f, "CommissionUpdate({:?})", event.validator)
726 },
727 }
728 }
729}
730
731#[derive(Clone, derive_more::derive::Debug)]
732pub struct EpochCommittees {
734 non_epoch_committee: NonEpochCommittee,
736 state: HashMap<Epoch, EpochCommittee>,
738 all_validators: BTreeMap<Epoch, ValidatorMap>,
740 randomized_committees: BTreeMap<Epoch, RandomizedCommittee<StakeTableEntry<PubKey>>>,
742 da_committees: BTreeMap<u64, DaCommittee>,
744 first_epoch: Option<Epoch>,
745 epoch_height: u64,
746 fixed_block_reward: Option<RewardAmount>,
749 fetcher: Arc<Fetcher>,
750}
751
752#[derive(Debug, Clone)]
753struct DaCommittee {
754 committee: Vec<PeerConfig<SeqTypes>>,
755 indexed_committee: HashMap<PubKey, PeerConfig<SeqTypes>>,
756}
757
758impl Fetcher {
759 pub fn new(
760 peers: Arc<dyn StateCatchup>,
761 persistence: Arc<Mutex<dyn MembershipPersistence>>,
762 l1_client: L1Client,
763 chain_config: ChainConfig,
764 ) -> Self {
765 Self {
766 peers,
767 persistence,
768 l1_client,
769 chain_config: Arc::new(Mutex::new(chain_config)),
770 update_task: StakeTableUpdateTask(Mutex::new(None)).into(),
771 initial_supply: Arc::new(RwLock::new(None)),
772 }
773 }
774
775 pub async fn spawn_update_loop(&self) {
776 let mut update_task = self.update_task.0.lock().await;
777 if update_task.is_none() {
778 *update_task = Some(spawn(self.update_loop()));
779 }
780 }
781
782 fn update_loop(&self) -> impl Future<Output = ()> {
787 let span = tracing::warn_span!("Stake table update loop");
788 let self_clone = self.clone();
789 let state = self.l1_client.state.clone();
790 let l1_retry = self.l1_client.options().l1_retry_delay;
791 let update_delay = self.l1_client.options().stake_table_update_interval;
792 let chain_config = self.chain_config.clone();
793
794 async move {
795 let stake_contract_address = loop {
800 let contract = chain_config.lock().await.stake_table_contract;
801 match contract {
802 Some(addr) => break addr,
803 None => {
804 tracing::debug!(
805 "Stake table contract address not found. Retrying in {l1_retry:?}...",
806 );
807 },
808 }
809 sleep(l1_retry).await;
810 };
811
812 loop {
814 let finalized_block = loop {
815 let last_finalized = state.lock().await.last_finalized;
816 if let Some(block) = last_finalized {
817 break block;
818 }
819 tracing::debug!("Finalized block not yet available. Retrying in {l1_retry:?}",);
820 sleep(l1_retry).await;
821 };
822
823 tracing::debug!("Attempting to fetch stake table at L1 block {finalized_block:?}",);
824
825 loop {
826 match self_clone
827 .fetch_and_store_stake_table_events(stake_contract_address, finalized_block)
828 .await
829 {
830 Ok(events) => {
831 tracing::info!(
832 "Successfully fetched and stored stake table events at \
833 block={finalized_block:?}"
834 );
835 tracing::debug!("events={events:?}");
836 break;
837 },
838 Err(e) => {
839 tracing::error!(
840 "Error fetching stake table at block {finalized_block:?}. err= \
841 {e:#}",
842 );
843 sleep(l1_retry).await;
844 },
845 }
846 }
847
848 tracing::debug!("Waiting {update_delay:?} before next stake table update...",);
849 sleep(update_delay).await;
850 }
851 }
852 .instrument(span)
853 }
854
855 pub async fn fetch_and_store_stake_table_events(
862 &self,
863 contract: Address,
864 to_block: u64,
865 ) -> anyhow::Result<Vec<(EventKey, StakeTableEvent)>> {
866 let (read_l1_offset, persistence_events) = {
867 let persistence_lock = self.persistence.lock().await;
868 persistence_lock.load_events(to_block).await?
869 };
870
871 tracing::info!("loaded events from storage to_block={to_block:?}");
872
873 if let Some(EventsPersistenceRead::Complete) = read_l1_offset {
876 return Ok(persistence_events);
877 }
878
879 let from_block = read_l1_offset
880 .map(|read| match read {
881 EventsPersistenceRead::UntilL1Block(block) => Ok(block + 1),
882 EventsPersistenceRead::Complete => Err(anyhow::anyhow!(
883 "Unexpected state. offset is complete after returning early"
884 )),
885 })
886 .transpose()?;
887
888 ensure!(
889 Some(to_block) >= from_block,
890 "to_block {to_block:?} is less than from_block {from_block:?}"
891 );
892
893 tracing::info!(%to_block, from_block = ?from_block, "Fetching events from contract");
894
895 let contract_events = Self::fetch_events_from_contract(
896 self.l1_client.clone(),
897 contract,
898 from_block,
899 to_block,
900 )
901 .await?;
902
903 if !contract_events.is_empty() {
905 tracing::info!(
906 "storing {} new events in storage to_block={to_block:?}",
907 contract_events.len()
908 );
909 {
910 let persistence_lock = self.persistence.lock().await;
911 persistence_lock
912 .store_events(to_block, contract_events.clone())
913 .await
914 .inspect_err(|e| tracing::error!("failed to store events. err={e}"))?;
915 }
916 }
917
918 let mut events = match from_block {
919 Some(_) => persistence_events
920 .into_iter()
921 .chain(contract_events)
922 .collect(),
923 None => contract_events,
924 };
925
926 let len_before_dedup = events.len();
931 events.dedup();
932 let len_after_dedup = events.len();
933 if len_before_dedup != len_after_dedup {
934 tracing::warn!("Duplicate events found and removed. This should not normally happen.")
935 }
936
937 Ok(events)
938 }
939
940 fn validate_event(event: &StakeTableV2Events, log: &Log) -> Result<bool, StakeTableError> {
947 match event {
948 StakeTableV2Events::ValidatorRegisteredV2(evt) => {
949 if let Err(err) = evt.authenticate() {
950 tracing::warn!(
951 %err,
952 "Failed to authenticate ValidatorRegisteredV2 event: {}",
953 log.display()
954 );
955 return Ok(false);
956 }
957 },
958 StakeTableV2Events::ConsensusKeysUpdatedV2(evt) => {
959 if let Err(err) = evt.authenticate() {
960 tracing::warn!(
961 %err,
962 "Failed to authenticate ConsensusKeysUpdatedV2 event: {}",
963 log.display()
964 );
965 return Ok(false);
966 }
967 },
968 StakeTableV2Events::CommissionUpdated(CommissionUpdated {
969 validator,
970 newCommission,
971 ..
972 }) => {
973 if *newCommission > COMMISSION_BASIS_POINTS {
974 return Err(StakeTableError::InvalidCommission(
975 *validator,
976 *newCommission,
977 ));
978 }
979 },
980 _ => {},
981 }
982
983 Ok(true)
984 }
985
986 fn block_range_chunks(
988 from_block: u64,
989 to_block: u64,
990 chunk_size: u64,
991 ) -> impl Iterator<Item = (u64, u64)> {
992 let mut start = from_block;
993 let end = to_block;
994 std::iter::from_fn(move || {
995 let chunk_end = min(start + chunk_size - 1, end);
996 if chunk_end < start {
997 return None;
998 }
999 let chunk = (start, chunk_end);
1000 start = chunk_end + 1;
1001 Some(chunk)
1002 })
1003 }
1004
1005 pub async fn fetch_events_from_contract(
1007 l1_client: L1Client,
1008 contract: Address,
1009 from_block: Option<u64>,
1010 to_block: u64,
1011 ) -> Result<Vec<(EventKey, StakeTableEvent)>, StakeTableError> {
1012 let stake_table_contract = StakeTableV2::new(contract, l1_client.provider.clone());
1013 let max_retry_duration = l1_client.options().l1_events_max_retry_duration;
1014 let retry_delay = l1_client.options().l1_retry_delay;
1015 let from_block = match from_block {
1018 Some(block) => block,
1019 None => {
1020 let start = Instant::now();
1021 loop {
1022 match stake_table_contract.initializedAtBlock().call().await {
1023 Ok(init_block) => break init_block.to::<u64>(),
1024 Err(err) => {
1025 if start.elapsed() >= max_retry_duration {
1026 panic!(
1027 "Failed to retrieve initial block after `{}`: {err}",
1028 format_duration(max_retry_duration)
1029 );
1030 }
1031 tracing::warn!(%err, "Failed to retrieve initial block, retrying...");
1032 sleep(retry_delay).await;
1033 },
1034 }
1035 }
1036 },
1037 };
1038
1039 let chunk_size = l1_client.options().l1_events_max_block_range;
1043 let chunks = Self::block_range_chunks(from_block, to_block, chunk_size);
1044
1045 let mut events = vec![];
1046
1047 for (from, to) in chunks {
1048 let provider = l1_client.provider.clone();
1049
1050 tracing::debug!(from, to, "fetch all stake table events in range");
1051 let logs: Vec<Log> = retry(
1054 retry_delay,
1055 max_retry_duration,
1056 "stake table events fetch",
1057 move || {
1058 let provider = provider.clone();
1059
1060 Box::pin(async move {
1061 let filter = Filter::new()
1062 .events([
1063 ValidatorRegistered::SIGNATURE,
1064 ValidatorRegisteredV2::SIGNATURE,
1065 ValidatorExit::SIGNATURE,
1066 ValidatorExitV2::SIGNATURE,
1067 Delegated::SIGNATURE,
1068 Undelegated::SIGNATURE,
1069 UndelegatedV2::SIGNATURE,
1070 ConsensusKeysUpdated::SIGNATURE,
1071 ConsensusKeysUpdatedV2::SIGNATURE,
1072 CommissionUpdated::SIGNATURE,
1073 ])
1074 .address(contract)
1075 .from_block(from)
1076 .to_block(to);
1077 provider.get_logs(&filter).await
1078 })
1079 },
1080 )
1081 .await;
1082
1083 let chunk_events = logs
1084 .into_iter()
1085 .filter_map(|log| {
1086 let event =
1087 StakeTableV2Events::decode_raw_log(log.topics(), &log.data().data).ok()?;
1088 match Self::validate_event(&event, &log) {
1089 Ok(true) => Some(Ok((event, log))),
1090 Ok(false) => None,
1091 Err(e) => Some(Err(e)),
1092 }
1093 })
1094 .collect::<Result<Vec<_>, _>>()?;
1095
1096 events.extend(chunk_events);
1097 }
1098
1099 sort_stake_table_events(events).map_err(Into::into)
1100 }
1101
1102 pub async fn fetch_all_validators_from_contract(
1104 l1_client: L1Client,
1105 contract: Address,
1106 to_block: u64,
1107 ) -> anyhow::Result<(ValidatorMap, StakeTableHash)> {
1108 let events = Self::fetch_events_from_contract(l1_client, contract, None, to_block).await?;
1109
1110 validators_from_l1_events(events.into_iter().map(|(_, e)| e))
1112 .context("failed to construct validators set from l1 events")
1113 }
1114
1115 pub async fn fetch_fixed_block_reward(&self) -> Result<RewardAmount, FetchRewardError> {
1119 let initial_supply = *self.initial_supply.read().await;
1121 let initial_supply = match initial_supply {
1122 Some(supply) => supply,
1123 None => self.fetch_and_update_initial_supply().await?,
1124 };
1125
1126 let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
1127 .checked_div(U256::from(COMMISSION_BASIS_POINTS))
1128 .ok_or(FetchRewardError::DivisionByZero(
1129 "COMMISSION_BASIS_POINTS is zero",
1130 ))?;
1131
1132 Ok(RewardAmount(reward))
1133 }
1134
1135 pub async fn fetch_and_update_initial_supply(&self) -> Result<U256, FetchRewardError> {
1152 tracing::info!("Fetching token initial supply");
1153 let chain_config = *self.chain_config.lock().await;
1154
1155 let stake_table_contract = chain_config
1156 .stake_table_contract
1157 .ok_or(FetchRewardError::MissingStakeTableContract)?;
1158
1159 let provider = self.l1_client.provider.clone();
1160 let stake_table = StakeTableV2::new(stake_table_contract, provider.clone());
1161
1162 let stake_table_init_block = stake_table
1166 .initializedAtBlock()
1167 .block(BlockId::finalized())
1168 .call()
1169 .await
1170 .map_err(FetchRewardError::ContractCall)?
1171 .to::<u64>();
1172
1173 tracing::info!("stake table init block ={stake_table_init_block}");
1174
1175 let token_address = stake_table
1176 .token()
1177 .block(BlockId::finalized())
1178 .call()
1179 .await
1180 .map_err(FetchRewardError::TokenAddressFetch)?;
1181
1182 let token = EspToken::new(token_address, provider.clone());
1183
1184 let init_logs = token
1191 .Initialized_filter()
1192 .from_block(0u64)
1193 .to_block(BlockNumberOrTag::Finalized)
1194 .query()
1195 .await;
1196
1197 let init_log = match init_logs {
1198 Ok(init_logs) => {
1199 if init_logs.is_empty() {
1200 tracing::error!(
1201 "Token Initialized event logs are empty. This should never happen"
1202 );
1203 return Err(FetchRewardError::MissingInitializedEvent);
1204 }
1205
1206 let (_, init_log) = init_logs[0].clone();
1207
1208 tracing::debug!(tx_hash = ?init_log.transaction_hash, "Found token `Initialized` event");
1209 init_log
1210 },
1211 Err(err) => {
1212 tracing::warn!(
1213 "RPC returned error {err:?}. will fallback to scanning over fixed block range"
1214 );
1215 self.scan_token_contract_initialized_event_log(stake_table_init_block, token)
1216 .await?
1217 },
1218 };
1219
1220 let tx_hash =
1222 init_log
1223 .transaction_hash
1224 .ok_or_else(|| FetchRewardError::MissingTransactionHash {
1225 init_log: init_log.clone().into(),
1226 })?;
1227
1228 let init_tx = provider
1230 .get_transaction_receipt(tx_hash)
1231 .await
1232 .map_err(FetchRewardError::Rpc)?
1233 .ok_or_else(|| FetchRewardError::MissingTransactionReceipt {
1234 tx_hash: tx_hash.to_string(),
1235 })?;
1236
1237 let mint_transfer = init_tx.decoded_log::<EspToken::Transfer>().ok_or(
1238 FetchRewardError::DecodeTransferLog {
1239 tx_hash: tx_hash.to_string(),
1240 },
1241 )?;
1242
1243 tracing::debug!("mint transfer event ={mint_transfer:?}");
1244 if mint_transfer.from != Address::ZERO {
1245 return Err(FetchRewardError::InvalidMintFromAddress);
1246 }
1247
1248 let initial_supply = mint_transfer.value;
1249
1250 tracing::info!("Initial token amount: {} ESP", format_ether(initial_supply));
1251
1252 let mut writer = self.initial_supply.write().await;
1253 *writer = Some(initial_supply);
1254
1255 Ok(initial_supply)
1256 }
1257
1258 pub async fn scan_token_contract_initialized_event_log(
1266 &self,
1267 stake_table_init_block: u64,
1268 token: EspTokenInstance<L1Provider>,
1269 ) -> Result<Log, FetchRewardError> {
1270 let max_events_range = self.l1_client.options().l1_events_max_block_range;
1271 const MAX_BLOCKS_SCANNED: u64 = 200_000;
1272 let mut total_scanned = 0;
1273
1274 let mut from_block = stake_table_init_block.saturating_sub(max_events_range);
1275 let mut to_block = stake_table_init_block;
1276
1277 loop {
1278 if total_scanned >= MAX_BLOCKS_SCANNED {
1279 tracing::error!(
1280 total_scanned,
1281 "Exceeded maximum scan range while searching for token Initialized event"
1282 );
1283 return Err(FetchRewardError::ExceededMaxScanRange(MAX_BLOCKS_SCANNED));
1284 }
1285
1286 let init_logs = token
1287 .Initialized_filter()
1288 .from_block(from_block)
1289 .to_block(to_block)
1290 .query()
1291 .await
1292 .map_err(FetchRewardError::ScanQueryFailed)?;
1293
1294 if !init_logs.is_empty() {
1295 let (_, init_log) = init_logs[0].clone();
1296 tracing::info!(
1297 from_block,
1298 tx_hash = ?init_log.transaction_hash,
1299 "Found token Initialized event during scan"
1300 );
1301 return Ok(init_log);
1302 }
1303
1304 total_scanned += max_events_range;
1305 from_block = from_block.saturating_sub(max_events_range);
1306 to_block = to_block.saturating_sub(max_events_range);
1307 }
1308 }
1309
1310 pub async fn update_chain_config(&self, header: &Header) -> anyhow::Result<()> {
1311 let chain_config = self.get_chain_config(header).await?;
1312 *self.chain_config.lock().await = chain_config;
1314
1315 Ok(())
1316 }
1317
1318 pub async fn fetch(&self, epoch: Epoch, header: &Header) -> anyhow::Result<ValidatorSet> {
1319 let chain_config = *self.chain_config.lock().await;
1320 let Some(address) = chain_config.stake_table_contract else {
1321 bail!("No stake table contract address found in Chain config");
1322 };
1323
1324 let Some(l1_finalized_block_info) = header.l1_finalized() else {
1325 bail!(
1326 "The epoch root for epoch {epoch} is missing the L1 finalized block info. This is \
1327 a fatal error. Consensus is blocked and will not recover."
1328 );
1329 };
1330
1331 let events = match self
1332 .fetch_and_store_stake_table_events(address, l1_finalized_block_info.number())
1333 .await
1334 .map_err(GetStakeTablesError::L1ClientFetchError)
1335 {
1336 Ok(events) => events,
1337 Err(e) => {
1338 bail!("failed to fetch stake table events {e:?}");
1339 },
1340 };
1341
1342 match validator_set_from_l1_events(events.into_iter().map(|(_, e)| e)) {
1343 Ok(res) => Ok(res),
1344 Err(e) => {
1345 bail!("failed to construct stake table {e:?}");
1346 },
1347 }
1348 }
1349
1350 pub(crate) async fn get_chain_config(&self, header: &Header) -> anyhow::Result<ChainConfig> {
1353 let chain_config = self.chain_config.lock().await;
1354 let peers = self.peers.clone();
1355 let header_cf = header.chain_config();
1356 if chain_config.commit() == header_cf.commit() {
1357 return Ok(*chain_config);
1358 }
1359
1360 let cf = match header_cf.resolve() {
1361 Some(cf) => cf,
1362 None => peers
1363 .fetch_chain_config(header_cf.commit())
1364 .await
1365 .inspect_err(|err| {
1366 tracing::error!("failed to get chain_config from peers. err: {err:?}");
1367 })?,
1368 };
1369
1370 Ok(cf)
1371 }
1372
1373 #[cfg(any(test, feature = "testing"))]
1374 pub fn mock() -> Self {
1375 use crate::{mock, v0_1::NoStorage};
1376 let chain_config = ChainConfig::default();
1377 let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
1378 .expect("Failed to create L1 client");
1379
1380 let peers = Arc::new(mock::MockStateCatchup::default());
1381 let persistence = NoStorage;
1382
1383 Self::new(peers, Arc::new(Mutex::new(persistence)), l1, chain_config)
1384 }
1385}
1386
1387async fn retry<F, T, E>(
1388 retry_delay: Duration,
1389 max_duration: Duration,
1390 operation_name: &str,
1391 mut operation: F,
1392) -> T
1393where
1394 F: FnMut() -> BoxFuture<'static, Result<T, E>>,
1395 E: std::fmt::Display,
1396{
1397 let start = Instant::now();
1398 loop {
1399 match operation().await {
1400 Ok(result) => return result,
1401 Err(err) => {
1402 if start.elapsed() >= max_duration {
1403 panic!(
1404 r#"
1405 Failed to complete operation `{operation_name}` after `{}`.
1406 error: {err}
1407
1408
1409 This might be caused by:
1410 - The current block range being too large for your RPC provider.
1411 - The event query returning more data than your RPC allows as
1412 some RPC providers limit the number of events returned.
1413 - RPC provider outage
1414
1415 Suggested solution:
1416 - Reduce the value of the environment variable
1417 `ESPRESSO_SEQUENCER_L1_EVENTS_MAX_BLOCK_RANGE` to query smaller ranges.
1418 - Add multiple RPC providers
1419 - Use a different RPC provider with higher rate limits."#,
1420 format_duration(max_duration)
1421 );
1422 }
1423 tracing::warn!(%err, "Retrying `{operation_name}` after error");
1424 sleep(retry_delay).await;
1425 },
1426 }
1427 }
1428}
1429
1430#[derive(Clone, Debug)]
1432struct NonEpochCommittee {
1433 eligible_leaders: Vec<PeerConfig<SeqTypes>>,
1437
1438 stake_table: Vec<PeerConfig<SeqTypes>>,
1440
1441 da_committee: DaCommittee,
1442
1443 indexed_stake_table: HashMap<PubKey, PeerConfig<SeqTypes>>,
1445}
1446
1447#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
1449pub struct EpochCommittee {
1450 eligible_leaders: Vec<PeerConfig<SeqTypes>>,
1454 stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>>,
1456 validators: ValidatorMap,
1457 address_mapping: HashMap<BLSPubKey, Address>,
1458 block_reward: Option<RewardAmount>,
1459 stake_table_hash: Option<StakeTableHash>,
1460 header: Option<Header>,
1461}
1462
1463impl EpochCommittees {
1464 pub fn first_epoch(&self) -> Option<Epoch> {
1465 self.first_epoch
1466 }
1467
1468 pub fn fetcher(&self) -> &Fetcher {
1469 &self.fetcher
1470 }
1471
1472 pub fn fixed_block_reward(&self) -> Option<RewardAmount> {
1473 self.fixed_block_reward
1474 }
1475
1476 async fn fetch_and_update_fixed_block_reward(
1481 membership: Arc<RwLock<Self>>,
1482 epoch: EpochNumber,
1483 ) -> anyhow::Result<RewardAmount> {
1484 let membership_reader = membership.upgradable_read().await;
1485 let fetcher = membership_reader.fetcher.clone();
1486 match membership_reader.fixed_block_reward {
1487 Some(reward) => Ok(reward),
1488 None => {
1489 tracing::warn!(%epoch,
1490 "Block reward is None. attempting to fetch it from L1",
1491
1492 );
1493 let block_reward = fetcher
1494 .fetch_fixed_block_reward()
1495 .await
1496 .inspect_err(|err| {
1497 tracing::error!(?epoch, ?err, "failed to fetch block_reward");
1498 })?;
1499 let mut writer = RwLockUpgradableReadGuard::upgrade(membership_reader).await;
1500 writer.fixed_block_reward = Some(block_reward);
1501 Ok(block_reward)
1502 },
1503 }
1504 }
1505
1506 pub fn compute_block_reward(
1507 epoch: &EpochNumber,
1508 total_supply: U256,
1509 total_stake: U256,
1510 avg_block_time_ms: u64,
1511 ) -> anyhow::Result<RewardAmount> {
1512 let total_stake_bd = BigDecimal::from_str(&total_stake.to_string())?;
1514 let total_supply_bd = BigDecimal::from_str(&(total_supply.to_string()))?;
1515
1516 tracing::debug!(?epoch, "total_stake={total_stake}");
1517 tracing::debug!(?epoch, "total_supply_bd={total_supply_bd}");
1518
1519 let (proportion, reward_rate) =
1520 calculate_proportion_staked_and_reward_rate(&total_stake_bd, &total_supply_bd)?;
1521 let inflation_rate = proportion * reward_rate;
1522
1523 tracing::debug!(?epoch, "inflation_rate={inflation_rate:?}");
1524
1525 let blocks_per_year = MILLISECONDS_PER_YEAR
1526 .checked_div(avg_block_time_ms.into())
1527 .context("avg_block_time_ms is zero")?;
1528
1529 tracing::debug!(?epoch, "blocks_per_year={blocks_per_year:?}");
1530
1531 ensure!(!blocks_per_year.is_zero(), "blocks per year is zero");
1532 let block_reward = (total_supply_bd * inflation_rate) / blocks_per_year;
1533
1534 let block_reward_u256 = U256::from_str(&block_reward.round(0).to_string())?;
1535
1536 Ok(block_reward_u256.into())
1537 }
1538
1539 pub async fn fetch_and_calculate_block_reward(
1549 current_epoch: Epoch,
1550 coordinator: EpochMembershipCoordinator<SeqTypes>,
1551 ) -> anyhow::Result<RewardAmount> {
1552 let membership_read = coordinator.membership().read().await;
1553 let fixed_block_reward = membership_read.fixed_block_reward;
1554
1555 let committee = membership_read
1556 .state
1557 .get(¤t_epoch)
1558 .context(format!("committee not found for epoch={current_epoch:?}"))?
1559 .clone();
1560
1561 if let Some(reward) = committee.block_reward {
1563 return Ok(reward);
1564 }
1565
1566 let first_epoch = *membership_read.first_epoch().context(format!(
1567 "First epoch not initialized (current_epoch={current_epoch})"
1568 ))?;
1569
1570 drop(membership_read);
1571
1572 if *current_epoch <= first_epoch + 1 {
1573 bail!(
1574 "epoch is in first two epochs: current_epoch={current_epoch}, \
1575 first_epoch={first_epoch}"
1576 );
1577 }
1578
1579 let header = match committee.header.clone() {
1580 Some(header) => header,
1581 None => {
1582 let root_epoch = current_epoch.checked_sub(2).context(format!(
1583 "Epoch calculation underflow (current_epoch={current_epoch})"
1584 ))?;
1585
1586 tracing::info!(?root_epoch, "catchup epoch root header");
1587
1588 let membership = coordinator.membership();
1589 let leaf = Self::get_epoch_root(membership.clone(), EpochNumber::new(root_epoch))
1590 .await
1591 .with_context(|| {
1592 format!("Failed to get epoch root for root_epoch={root_epoch}")
1593 })?;
1594 leaf.block_header().clone()
1595 },
1596 };
1597
1598 if header.version() <= EpochVersion::version() {
1599 return fixed_block_reward.context(format!(
1600 "Fixed block reward not found for current_epoch={current_epoch}"
1601 ));
1602 }
1603
1604 let prev_epoch_u64 = current_epoch.checked_sub(1).context(format!(
1605 "Underflow: cannot compute previous epoch when current_epoch={current_epoch}"
1606 ))?;
1607
1608 let prev_epoch = EpochNumber::new(prev_epoch_u64);
1609
1610 if *prev_epoch > first_epoch + 1 {
1613 if let Err(err) = coordinator.stake_table_for_epoch(Some(prev_epoch)).await {
1614 tracing::info!("failed to get membership for epoch={prev_epoch:?}: {err:#}");
1615
1616 coordinator
1617 .wait_for_catchup(prev_epoch)
1618 .await
1619 .context(format!("failed to catch up for epoch={prev_epoch}"))?;
1620 }
1621 }
1622
1623 let membership_read = coordinator.membership().read().await;
1624
1625 membership_read
1626 .calculate_dynamic_block_reward(¤t_epoch, &header, &committee.validators)
1627 .await
1628 .with_context(|| {
1629 format!("dynamic block reward calculation failed for epoch={current_epoch}")
1630 })?
1631 .with_context(|| format!("dynamic block reward returned None. epoch={current_epoch}"))
1632 }
1633
1634 async fn calculate_dynamic_block_reward(
1641 &self,
1642 epoch: &Epoch,
1643 header: &Header,
1644 validators: &ValidatorMap,
1645 ) -> anyhow::Result<Option<RewardAmount>> {
1646 let epoch_height = self.epoch_height;
1647 let current_epoch = epoch_from_block_number(header.height(), epoch_height);
1648 let previous_epoch = current_epoch
1649 .checked_sub(1)
1650 .context("underflow: cannot get previous epoch when current_epoch is 0")?;
1651 tracing::debug!(?epoch, "previous_epoch={previous_epoch:?}");
1652
1653 let first_epoch = *self.first_epoch().context("first epoch is None")?;
1654
1655 if previous_epoch > first_epoch + 1
1658 && !self.has_stake_table(EpochNumber::new(previous_epoch))
1659 {
1660 tracing::warn!(?previous_epoch, "missing stake table for previous epoch");
1661 return Ok(None);
1662 }
1663
1664 let fetcher = self.fetcher.clone();
1665
1666 let previous_reward_distributed = header
1667 .total_reward_distributed()
1668 .context("Invalid block header: missing total_reward_distributed field")?;
1669
1670 let total_stake: U256 = validators.values().map(|v| v.stake).sum();
1672 let initial_supply = *fetcher.initial_supply.read().await;
1673 let initial_supply = match initial_supply {
1674 Some(supply) => supply,
1675 None => fetcher.fetch_and_update_initial_supply().await?,
1676 };
1677 let total_supply = initial_supply
1678 .checked_add(previous_reward_distributed.0)
1679 .context("initial_supply + previous_reward_distributed overflow")?;
1680
1681 let curr_ts = header.timestamp_millis_internal();
1683 tracing::debug!(?epoch, "curr_ts={curr_ts:?}");
1684
1685 let average_block_time_ms = if previous_epoch <= first_epoch + 1 {
1689 ASSUMED_BLOCK_TIME_SECONDS as u64 * 1000 } else {
1691 let next_epoch = epoch
1695 .checked_sub(1)
1696 .context("underflow: cannot get next epoch when epoch is 0")?;
1697 let prev_ts = match self.get_header(EpochNumber::new(next_epoch)) {
1698 Some(header) => header.timestamp_millis_internal(),
1699 None => {
1700 tracing::info!(
1701 "Calculating rewards for epoch {}, we have no root leaf header for epoch \
1702 - 1. Fetching from peers",
1703 epoch
1704 );
1705
1706 let root_height = header.height().checked_sub(epoch_height).context(
1707 "Epoch height is greater than block height. cannot compute previous epoch \
1708 root height",
1709 )?;
1710
1711 let prev_stake_table = self
1712 .get_stake_table(&Some(EpochNumber::new(previous_epoch)))
1713 .context("Stake table not found")?
1714 .into();
1715
1716 let success_threshold =
1717 self.success_threshold(Some(EpochNumber::new(previous_epoch)));
1718
1719 fetcher
1720 .peers
1721 .fetch_leaf(root_height, prev_stake_table, success_threshold)
1722 .await
1723 .context("Epoch root leaf not found")?
1724 .block_header()
1725 .timestamp_millis_internal()
1726 },
1727 };
1728
1729 let time_diff = curr_ts.checked_sub(prev_ts).context(
1730 "Current timestamp is earlier than previous. underflow in block time calculation",
1731 )?;
1732
1733 time_diff
1734 .checked_div(epoch_height)
1735 .context("Epoch height is zero. cannot compute average block time")?
1736 };
1737 tracing::info!(?epoch, %total_supply, %total_stake, %average_block_time_ms,
1738 "dynamic block reward parameters");
1739
1740 let block_reward =
1741 Self::compute_block_reward(epoch, total_supply, total_stake, average_block_time_ms)?;
1742
1743 Ok(Some(block_reward))
1744 }
1745
1746 pub fn epoch_block_reward(&self, epoch: EpochNumber) -> Option<RewardAmount> {
1748 self.state
1749 .get(&epoch)
1750 .and_then(|committee| committee.block_reward)
1751 }
1752 fn insert_committee(
1758 &mut self,
1759 epoch: EpochNumber,
1760 validators: ValidatorMap,
1761 block_reward: Option<RewardAmount>,
1762 hash: Option<StakeTableHash>,
1763 header: Option<Header>,
1764 ) {
1765 let mut address_mapping = HashMap::new();
1766 let stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>> = validators
1767 .values()
1768 .map(|v| {
1769 address_mapping.insert(v.stake_table_key, v.account);
1770 (
1771 v.stake_table_key,
1772 PeerConfig {
1773 stake_table_entry: BLSPubKey::stake_table_entry(
1774 &v.stake_table_key,
1775 v.stake,
1776 ),
1777 state_ver_key: v.state_ver_key.clone(),
1778 },
1779 )
1780 })
1781 .collect();
1782
1783 let eligible_leaders: Vec<PeerConfig<SeqTypes>> =
1784 stake_table.iter().map(|(_, l)| l.clone()).collect();
1785
1786 self.state.insert(
1787 epoch,
1788 EpochCommittee {
1789 eligible_leaders,
1790 stake_table,
1791 validators,
1792 address_mapping,
1793 block_reward,
1794 stake_table_hash: hash,
1795 header,
1796 },
1797 );
1798 }
1799
1800 pub fn active_validators(&self, epoch: &Epoch) -> anyhow::Result<ValidatorMap> {
1801 Ok(self
1802 .state
1803 .get(epoch)
1804 .context("state for found")?
1805 .validators
1806 .clone())
1807 }
1808
1809 pub fn address(&self, epoch: &Epoch, bls_key: BLSPubKey) -> anyhow::Result<Address> {
1810 let mapping = self
1811 .state
1812 .get(epoch)
1813 .context("state for found")?
1814 .address_mapping
1815 .clone();
1816
1817 Ok(*mapping.get(&bls_key).context(format!(
1818 "failed to get ethereum address for bls key {bls_key}. epoch={epoch}"
1819 ))?)
1820 }
1821
1822 pub fn get_validator_config(
1823 &self,
1824 epoch: &Epoch,
1825 key: BLSPubKey,
1826 ) -> anyhow::Result<Validator<BLSPubKey>> {
1827 let address = self.address(epoch, key)?;
1828 let validators = self.active_validators(epoch)?;
1829 validators
1830 .get(&address)
1831 .context("validator not found")
1832 .cloned()
1833 }
1834
1835 pub fn new_stake(
1837 committee_members: Vec<PeerConfig<SeqTypes>>,
1840 da_members: Vec<PeerConfig<SeqTypes>>,
1841 fixed_block_reward: Option<RewardAmount>,
1842 fetcher: Fetcher,
1843 epoch_height: u64,
1844 ) -> Self {
1845 let stake_table: Vec<_> = committee_members
1847 .iter()
1848 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
1849 .cloned()
1850 .collect();
1851
1852 let eligible_leaders = stake_table.clone();
1853 let da_members: Vec<_> = da_members
1855 .iter()
1856 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
1857 .cloned()
1858 .collect();
1859
1860 let indexed_stake_table: HashMap<PubKey, _> = stake_table
1862 .iter()
1863 .map(|peer_config| {
1864 (
1865 PubKey::public_key(&peer_config.stake_table_entry),
1866 peer_config.clone(),
1867 )
1868 })
1869 .collect();
1870
1871 let indexed_da_members: HashMap<PubKey, _> = da_members
1873 .iter()
1874 .map(|peer_config| {
1875 (
1876 PubKey::public_key(&peer_config.stake_table_entry),
1877 peer_config.clone(),
1878 )
1879 })
1880 .collect();
1881
1882 let da_committee = DaCommittee {
1883 committee: da_members,
1884 indexed_committee: indexed_da_members,
1885 };
1886
1887 let members = NonEpochCommittee {
1888 eligible_leaders,
1889 stake_table,
1890 indexed_stake_table,
1891 da_committee,
1892 };
1893
1894 let mut map = HashMap::new();
1895 let epoch_committee = EpochCommittee {
1896 eligible_leaders: members.eligible_leaders.clone(),
1897 stake_table: members
1898 .stake_table
1899 .iter()
1900 .map(|x| (PubKey::public_key(&x.stake_table_entry), x.clone()))
1901 .collect(),
1902 validators: Default::default(),
1903 address_mapping: HashMap::new(),
1904 block_reward: Default::default(),
1905 stake_table_hash: None,
1906 header: None,
1907 };
1908 map.insert(Epoch::genesis(), epoch_committee.clone());
1909 map.insert(Epoch::genesis() + 1u64, epoch_committee.clone());
1911
1912 Self {
1913 non_epoch_committee: members,
1914 da_committees: BTreeMap::new(),
1915 state: map,
1916 all_validators: BTreeMap::new(),
1917 randomized_committees: BTreeMap::new(),
1918 first_epoch: None,
1919 fixed_block_reward,
1920 fetcher: Arc::new(fetcher),
1921 epoch_height,
1922 }
1923 }
1924
1925 pub async fn reload_stake(&mut self, limit: u64) {
1926 match self.fetcher.fetch_fixed_block_reward().await {
1927 Ok(block_reward) => {
1928 tracing::info!("Fetched block reward: {block_reward}");
1929 self.fixed_block_reward = Some(block_reward);
1930 },
1931 Err(err) => {
1932 tracing::warn!(
1933 "Failed to fetch the block reward when reloading the stake tables: {err}"
1934 );
1935 },
1936 }
1937
1938 let loaded_stake = match self
1940 .fetcher
1941 .persistence
1942 .lock()
1943 .await
1944 .load_latest_stake(limit)
1945 .await
1946 {
1947 Ok(Some(loaded)) => loaded,
1948 Ok(None) => {
1949 tracing::warn!("No stake table history found in persistence!");
1950 return;
1951 },
1952 Err(e) => {
1953 tracing::error!("Failed to load stake table history from persistence: {e}");
1954 return;
1955 },
1956 };
1957
1958 for (epoch, (stake_table, block_reward), stake_table_hash) in loaded_stake {
1959 self.insert_committee(epoch, stake_table, block_reward, stake_table_hash, None);
1960 }
1961 }
1962
1963 fn get_stake_table(&self, epoch: &Option<Epoch>) -> Option<Vec<PeerConfig<SeqTypes>>> {
1964 if let Some(epoch) = epoch {
1965 self.state
1966 .get(epoch)
1967 .map(|committee| committee.stake_table.clone().into_values().collect())
1968 } else {
1969 Some(self.non_epoch_committee.stake_table.clone())
1970 }
1971 }
1972
1973 fn get_da_committee(&self, epoch: Option<Epoch>) -> DaCommittee {
1974 if let Some(e) = epoch {
1975 self.da_committees
1977 .range((Bound::Included(&0), Bound::Included(&*e)))
1978 .last()
1979 .map(|(_, committee)| committee.clone())
1980 .unwrap_or(self.non_epoch_committee.da_committee.clone())
1981 } else {
1982 self.non_epoch_committee.da_committee.clone()
1983 }
1984 }
1985
1986 fn get_header(&self, epoch: Epoch) -> Option<&Header> {
1988 self.state
1989 .get(&epoch)
1990 .and_then(|committee| committee.header.as_ref())
1991 }
1992}
1993
1994pub fn calculate_proportion_staked_and_reward_rate(
2004 total_stake: &BigDecimal,
2005 total_supply: &BigDecimal,
2006) -> anyhow::Result<(BigDecimal, BigDecimal)> {
2007 if total_supply.is_zero() {
2008 return Err(anyhow::anyhow!("Total supply cannot be zero"));
2009 }
2010
2011 let proportion_staked = total_stake / total_supply;
2012
2013 if proportion_staked < BigDecimal::from(0) || proportion_staked > BigDecimal::from(1) {
2014 return Err(anyhow::anyhow!("Stake ratio p must be in the range [0, 1]"));
2015 }
2016
2017 let two = BigDecimal::from_u32(2).unwrap();
2018 let min_stake_ratio = BigDecimal::from_str("0.01")?;
2019 let numerator = BigDecimal::from_str("0.03")?;
2020
2021 let denominator = (&two * (&proportion_staked).max(&min_stake_ratio))
2022 .sqrt()
2023 .context("Failed to compute sqrt in R(p)")?;
2024
2025 let reward_rate = numerator / denominator;
2026
2027 tracing::debug!("rp={reward_rate}");
2028
2029 Ok((proportion_staked, reward_rate))
2030}
2031
2032#[derive(Error, Debug)]
2033enum GetStakeTablesError {
2035 #[error("Error fetching from L1: {0}")]
2036 L1ClientFetchError(anyhow::Error),
2037}
2038
2039#[derive(Error, Debug)]
2040#[error("Could not lookup leader")] pub struct LeaderLookupError;
2042
2043impl Membership<SeqTypes> for EpochCommittees {
2045 type Error = LeaderLookupError;
2046 type Storage = ();
2047 type StakeTableHash = StakeTableState;
2048
2049 fn new<I: NodeImplementation<SeqTypes>>(
2051 _committee_members: Vec<PeerConfig<SeqTypes>>,
2054 _da_members: Vec<PeerConfig<SeqTypes>>,
2055 _storage: Self::Storage,
2056 _network: Arc<<I as NodeImplementation<SeqTypes>>::Network>,
2057 _public_key: <SeqTypes as NodeType>::SignatureKey,
2058 _epoch_height: u64,
2059 ) -> Self {
2060 panic!("This function has been replaced with new_stake()");
2061 }
2062
2063 fn stake_table(&self, epoch: Option<Epoch>) -> HSStakeTable<SeqTypes> {
2065 self.get_stake_table(&epoch).unwrap_or_default().into()
2066 }
2067 fn da_stake_table(&self, epoch: Option<Epoch>) -> HSStakeTable<SeqTypes> {
2069 self.get_da_committee(epoch).committee.clone().into()
2070 }
2071
2072 fn committee_members(
2074 &self,
2075 _view_number: <SeqTypes as NodeType>::View,
2076 epoch: Option<Epoch>,
2077 ) -> BTreeSet<PubKey> {
2078 let stake_table = self.stake_table(epoch);
2079 stake_table
2080 .iter()
2081 .map(|x| PubKey::public_key(&x.stake_table_entry))
2082 .collect()
2083 }
2084
2085 fn da_committee_members(
2087 &self,
2088 _view_number: <SeqTypes as NodeType>::View,
2089 epoch: Option<Epoch>,
2090 ) -> BTreeSet<PubKey> {
2091 self.da_stake_table(epoch)
2092 .iter()
2093 .map(|peer_config| PubKey::public_key(&peer_config.stake_table_entry))
2094 .collect()
2095 }
2096
2097 fn stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> Option<PeerConfig<SeqTypes>> {
2099 if let Some(epoch) = epoch {
2101 self.state
2102 .get(&epoch)
2103 .and_then(|h| h.stake_table.get(pub_key))
2104 .cloned()
2105 } else {
2106 self.non_epoch_committee
2107 .indexed_stake_table
2108 .get(pub_key)
2109 .cloned()
2110 }
2111 }
2112
2113 fn da_stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> Option<PeerConfig<SeqTypes>> {
2115 self.get_da_committee(epoch)
2116 .indexed_committee
2117 .get(pub_key)
2118 .cloned()
2119 }
2120
2121 fn has_stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> bool {
2123 self.stake(pub_key, epoch)
2124 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
2125 .unwrap_or_default()
2126 }
2127
2128 fn has_da_stake(&self, pub_key: &PubKey, epoch: Option<Epoch>) -> bool {
2130 self.da_stake(pub_key, epoch)
2131 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
2132 .unwrap_or_default()
2133 }
2134
2135 fn lookup_leader(
2148 &self,
2149 view_number: <SeqTypes as NodeType>::View,
2150 epoch: Option<Epoch>,
2151 ) -> Result<PubKey, Self::Error> {
2152 match (self.first_epoch(), epoch) {
2153 (Some(first_epoch), Some(epoch)) => {
2154 if epoch < first_epoch {
2155 tracing::error!(
2156 "lookup_leader called with epoch {} before first epoch {}",
2157 epoch,
2158 first_epoch,
2159 );
2160 return Err(LeaderLookupError);
2161 }
2162 let Some(randomized_committee) = self.randomized_committees.get(&epoch) else {
2163 tracing::error!(
2164 "We are missing the randomized committee for epoch {}",
2165 epoch
2166 );
2167 return Err(LeaderLookupError);
2168 };
2169
2170 Ok(PubKey::public_key(&select_randomized_leader(
2171 randomized_committee,
2172 *view_number,
2173 )))
2174 },
2175 (_, None) => {
2176 let leaders = &self.non_epoch_committee.eligible_leaders;
2177
2178 let index = *view_number as usize % leaders.len();
2179 let res = leaders[index].clone();
2180 Ok(PubKey::public_key(&res.stake_table_entry))
2181 },
2182 (None, Some(epoch)) => {
2183 tracing::error!(
2184 "lookup_leader called with epoch {} but we don't have a first epoch",
2185 epoch,
2186 );
2187 Err(LeaderLookupError)
2188 },
2189 }
2190 }
2191
2192 fn total_nodes(&self, epoch: Option<Epoch>) -> usize {
2194 self.stake_table(epoch).len()
2195 }
2196
2197 fn da_total_nodes(&self, epoch: Option<Epoch>) -> usize {
2199 self.da_stake_table(epoch).len()
2200 }
2201
2202 async fn add_epoch_root(
2206 membership: Arc<RwLock<Self>>,
2207 block_header: Header,
2208 ) -> anyhow::Result<()> {
2209 let block_number = block_header.block_number();
2210
2211 let membership_reader = membership.read().await;
2212 let epoch_height = membership_reader.epoch_height;
2213
2214 let epoch =
2215 Epoch::new(epoch_from_block_number(block_number, membership_reader.epoch_height) + 2);
2216
2217 tracing::info!(?epoch, "adding epoch root. height={:?}", block_number);
2218
2219 if !is_epoch_root(block_number, epoch_height) {
2220 tracing::error!(
2221 "`add_epoch_root` was called with a block header that was not the root block for \
2222 an epoch. This should never happen. Header:\n\n{block_header:?}"
2223 );
2224 bail!(
2225 "Failed to add epoch root: block {block_number:?} is not a root block for an epoch"
2226 );
2227 }
2228
2229 let fetcher = membership_reader.fetcher.clone();
2230
2231 drop(membership_reader);
2232
2233 let version = block_header.version();
2234 fetcher.update_chain_config(&block_header).await?;
2236
2237 let mut block_reward = None;
2238 if version == EpochVersion::version() {
2241 let reward =
2242 Self::fetch_and_update_fixed_block_reward(membership.clone(), epoch).await?;
2243 block_reward = Some(reward);
2244 }
2245
2246 let epoch_committee = {
2247 let membership_reader = membership.read().await;
2248 membership_reader.state.get(&epoch).cloned()
2249 };
2250
2251 let (active_validators, all_validators, stake_table_hash) = match epoch_committee {
2257 Some(committee)
2258 if committee.block_reward.is_some()
2259 && committee.header.is_some()
2260 && committee.stake_table_hash.is_some() =>
2261 {
2262 tracing::info!(
2263 ?epoch,
2264 "committee already has block reward, header, and stake table hash; skipping \
2265 add_epoch_root"
2266 );
2267 return Ok(());
2268 },
2269
2270 Some(committee) => {
2271 if let Some(reward) = committee.block_reward {
2272 block_reward = Some(reward);
2273 }
2274
2275 if let Some(hash) = committee.stake_table_hash {
2276 (committee.validators.clone(), Default::default(), Some(hash))
2277 } else {
2278 tracing::info!(
2280 "Stake table hash missing for epoch {epoch}. recalculating by fetching \
2281 from l1."
2282 );
2283 let set = fetcher.fetch(epoch, &block_header).await?;
2284 (
2285 set.active_validators,
2286 set.all_validators,
2287 set.stake_table_hash,
2288 )
2289 }
2290 },
2291
2292 None => {
2293 tracing::info!("Stake table missing for epoch {epoch}. Fetching from L1.");
2294 let set = fetcher.fetch(epoch, &block_header).await?;
2295 (
2296 set.active_validators,
2297 set.all_validators,
2298 set.stake_table_hash,
2299 )
2300 },
2301 };
2302
2303 if block_reward.is_none() && version >= DrbAndHeaderUpgradeVersion::version() {
2307 tracing::info!(?epoch, "calculating dynamic block reward");
2308 let reader = membership.read().await;
2309 let reward = reader
2310 .calculate_dynamic_block_reward(&epoch, &block_header, &active_validators)
2311 .await?;
2312
2313 tracing::info!(?epoch, "calculated dynamic block reward = {reward:?}");
2314 block_reward = reward;
2315 }
2316
2317 let mut membership_writer = membership.write().await;
2318 membership_writer.insert_committee(
2319 epoch,
2320 active_validators.clone(),
2321 block_reward,
2322 stake_table_hash,
2323 Some(block_header.clone()),
2324 );
2325
2326 let previous_epoch = EpochNumber::new(epoch.saturating_sub(1));
2329 let previous_committee = membership_writer.state.get(&previous_epoch).cloned();
2330 membership_writer.all_validators =
2332 membership_writer.all_validators.split_off(&previous_epoch);
2333 let previous_validators = membership_writer.all_validators.remove(&previous_epoch);
2335 membership_writer
2336 .all_validators
2337 .insert(epoch, all_validators.clone());
2338 drop(membership_writer);
2339
2340 let persistence_lock = fetcher.persistence.lock().await;
2341
2342 let decided_hash = block_header.next_stake_table_hash();
2343
2344 if let Some(previous_committee) = previous_committee {
2351 if decided_hash.is_none() || decided_hash == previous_committee.stake_table_hash {
2352 if let Err(e) = persistence_lock
2353 .store_stake(
2354 previous_epoch,
2355 previous_committee.validators,
2356 previous_committee.block_reward,
2357 previous_committee.stake_table_hash,
2358 )
2359 .await
2360 {
2361 tracing::error!(
2362 ?e,
2363 ?previous_epoch,
2364 "`add_epoch_root`, error storing stake table"
2365 );
2366 }
2367
2368 if let Some(previous_validators) = previous_validators {
2369 if let Err(e) = persistence_lock
2370 .store_all_validators(previous_epoch, previous_validators)
2371 .await
2372 {
2373 tracing::error!(
2374 ?e,
2375 ?epoch,
2376 "`add_epoch_root`, error storing all validators"
2377 );
2378 }
2379 }
2380 } else {
2381 panic!(
2382 "The decided block header's `next_stake_table_hash` does not match the hash \
2383 of the stake table we have. This is an unrecoverable error likely due to \
2384 issues with the your L1 RPC provider. Decided:\n\n{:?}Actual:\n\n{:?}",
2385 decided_hash, previous_committee.stake_table_hash
2386 );
2387 }
2388 }
2389
2390 Ok(())
2391 }
2392
2393 fn has_stake_table(&self, epoch: Epoch) -> bool {
2394 self.state.contains_key(&epoch)
2395 }
2396
2397 fn has_randomized_stake_table(&self, epoch: Epoch) -> anyhow::Result<bool> {
2409 let Some(first_epoch) = self.first_epoch else {
2410 bail!(
2411 "Called has_randomized_stake_table with epoch {} but first_epoch is None",
2412 epoch
2413 );
2414 };
2415 ensure!(
2416 epoch >= first_epoch,
2417 "Called has_randomized_stake_table with epoch {} but first_epoch is {}",
2418 epoch,
2419 first_epoch
2420 );
2421 Ok(self.randomized_committees.contains_key(&epoch))
2422 }
2423
2424 async fn get_epoch_root(membership: Arc<RwLock<Self>>, epoch: Epoch) -> anyhow::Result<Leaf2> {
2425 let membership_reader = membership.read().await;
2426 let block_height = root_block_in_epoch(*epoch, membership_reader.epoch_height);
2427 let peers = membership_reader.fetcher.peers.clone();
2428 let stake_table = membership_reader.stake_table(Some(epoch)).clone();
2429 let success_threshold = membership_reader.success_threshold(Some(epoch));
2430 drop(membership_reader);
2431
2432 let leaf: Leaf2 = peers
2434 .fetch_leaf(block_height, stake_table.clone(), success_threshold)
2435 .await?;
2436
2437 Ok(leaf)
2438 }
2439
2440 async fn get_epoch_drb(
2441 membership: Arc<RwLock<Self>>,
2442 epoch: Epoch,
2443 ) -> anyhow::Result<DrbResult> {
2444 let membership_reader = membership.read().await;
2445 let peers = membership_reader.fetcher.peers.clone();
2446
2447 if let Some(randomized_committee) = membership_reader.randomized_committees.get(&epoch) {
2449 return Ok(randomized_committee.drb_result());
2450 }
2451
2452 let previous_epoch = match epoch.checked_sub(1) {
2454 Some(epoch) => EpochNumber::new(epoch),
2455 None => {
2456 return membership_reader
2457 .randomized_committees
2458 .get(&epoch)
2459 .map(|committee| committee.drb_result())
2460 .context(format!("Missing randomized committee for epoch {epoch}"))
2461 },
2462 };
2463
2464 let stake_table = membership_reader.stake_table(Some(previous_epoch)).clone();
2465 let success_threshold = membership_reader.success_threshold(Some(previous_epoch));
2466
2467 let block_height =
2468 transition_block_for_epoch(*previous_epoch, membership_reader.epoch_height);
2469
2470 drop(membership_reader);
2471
2472 tracing::debug!(
2473 "Getting DRB for epoch {}, block height {}",
2474 epoch,
2475 block_height
2476 );
2477 let drb_leaf = peers
2478 .try_fetch_leaf(1, block_height, stake_table, success_threshold)
2479 .await?;
2480
2481 let Some(drb) = drb_leaf.next_drb_result else {
2482 tracing::error!(
2483 "We received a leaf that should contain a DRB result, but the DRB result is \
2484 missing: {:?}",
2485 drb_leaf
2486 );
2487
2488 bail!("DRB leaf is missing the DRB result.");
2489 };
2490
2491 Ok(drb)
2492 }
2493
2494 fn add_drb_result(&mut self, epoch: Epoch, drb: DrbResult) {
2495 tracing::info!("Adding DRB result {drb:?} to epoch {epoch}");
2496
2497 let Some(raw_stake_table) = self.state.get(&epoch) else {
2498 tracing::error!(
2499 "add_drb_result({epoch}, {drb:?}) was called, but we do not yet have the stake \
2500 table for epoch {epoch}"
2501 );
2502 return;
2503 };
2504
2505 let leaders = raw_stake_table
2506 .eligible_leaders
2507 .clone()
2508 .into_iter()
2509 .map(|peer_config| peer_config.stake_table_entry)
2510 .collect::<Vec<_>>();
2511 let randomized_committee = generate_stake_cdf(leaders, drb);
2512
2513 self.randomized_committees
2514 .insert(epoch, randomized_committee);
2515 }
2516
2517 fn set_first_epoch(&mut self, epoch: Epoch, initial_drb_result: DrbResult) {
2518 self.first_epoch = Some(epoch);
2519
2520 let epoch_committee = self.state.get(&Epoch::genesis()).unwrap().clone();
2521 self.state.insert(epoch, epoch_committee.clone());
2522 self.state.insert(epoch + 1, epoch_committee);
2523 self.add_drb_result(epoch, initial_drb_result);
2524 self.add_drb_result(epoch + 1, initial_drb_result);
2525 }
2526
2527 fn first_epoch(&self) -> Option<<SeqTypes as NodeType>::Epoch> {
2528 self.first_epoch
2529 }
2530
2531 fn stake_table_hash(&self, epoch: Epoch) -> Option<StakeTableHash> {
2532 let committee = self.state.get(&epoch)?;
2533 committee.stake_table_hash
2534 }
2535
2536 fn add_da_committee(&mut self, first_epoch: u64, committee: Vec<PeerConfig<SeqTypes>>) {
2537 let indexed_committee: HashMap<PubKey, _> = committee
2538 .iter()
2539 .map(|peer_config| {
2540 (
2541 PubKey::public_key(&peer_config.stake_table_entry),
2542 peer_config.clone(),
2543 )
2544 })
2545 .collect();
2546
2547 let da_committee = DaCommittee {
2548 committee,
2549 indexed_committee,
2550 };
2551
2552 self.da_committees.insert(first_epoch, da_committee);
2553 }
2554}
2555
2556#[cfg(any(test, feature = "testing"))]
2557impl super::v0_3::StakeTable {
2558 pub fn mock(n: u64) -> Self {
2560 [..n]
2561 .iter()
2562 .map(|_| PeerConfig::default())
2563 .collect::<Vec<PeerConfig<SeqTypes>>>()
2564 .into()
2565 }
2566}
2567
2568#[cfg(any(test, feature = "testing"))]
2569impl DAMembers {
2570 pub fn mock(n: u64) -> Self {
2572 [..n]
2573 .iter()
2574 .map(|_| PeerConfig::default())
2575 .collect::<Vec<PeerConfig<SeqTypes>>>()
2576 .into()
2577 }
2578}
2579
2580#[cfg(any(test, feature = "testing"))]
2581pub mod testing {
2582 use alloy::primitives::Bytes;
2583 use hotshot_contract_adapter::{
2584 sol_types::{EdOnBN254PointSol, G1PointSol, G2PointSol},
2585 stake_table::{sign_address_bls, sign_address_schnorr, StateSignatureSol},
2586 };
2587 use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
2588 use rand::{Rng as _, RngCore as _};
2589
2590 use super::*;
2591
2592 #[derive(Debug, Clone)]
2595 pub struct TestValidator {
2596 pub account: Address,
2597 pub bls_vk: G2PointSol,
2598 pub schnorr_vk: EdOnBN254PointSol,
2599 pub commission: u16,
2600 pub bls_sig: G1PointSol,
2601 pub schnorr_sig: Bytes,
2602 }
2603
2604 impl TestValidator {
2605 pub fn random() -> Self {
2606 let account = Address::random();
2607 let commission = rand::thread_rng().gen_range(0..10000);
2608 Self::random_update_keys(account, commission)
2609 }
2610
2611 pub fn randomize_keys(&self) -> Self {
2612 Self::random_update_keys(self.account, self.commission)
2613 }
2614
2615 pub fn random_update_keys(account: Address, commission: u16) -> Self {
2616 let mut rng = &mut rand::thread_rng();
2617 let mut seed = [0u8; 32];
2618 rng.fill_bytes(&mut seed);
2619 let bls_key_pair = BLSKeyPair::generate(&mut rng);
2620 let bls_sig = sign_address_bls(&bls_key_pair, account);
2621 let schnorr_key_pair = StateKeyPair::generate_from_seed_indexed(seed, 0);
2622 let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, account);
2623 Self {
2624 account,
2625 bls_vk: bls_key_pair.ver_key().to_affine().into(),
2626 schnorr_vk: schnorr_key_pair.ver_key().to_affine().into(),
2627 commission,
2628 bls_sig: bls_sig.into(),
2629 schnorr_sig: StateSignatureSol::from(schnorr_sig).into(),
2630 }
2631 }
2632 }
2633
2634 impl From<&TestValidator> for ValidatorRegistered {
2635 fn from(value: &TestValidator) -> Self {
2636 Self {
2637 account: value.account,
2638 blsVk: value.bls_vk,
2639 schnorrVk: value.schnorr_vk,
2640 commission: value.commission,
2641 }
2642 }
2643 }
2644
2645 impl From<&TestValidator> for ValidatorRegisteredV2 {
2646 fn from(value: &TestValidator) -> Self {
2647 Self {
2648 account: value.account,
2649 blsVK: value.bls_vk,
2650 schnorrVK: value.schnorr_vk,
2651 commission: value.commission,
2652 blsSig: value.bls_sig.into(),
2653 schnorrSig: value.schnorr_sig.clone(),
2654 metadataUri: "dummy-meta".to_string(),
2655 }
2656 }
2657 }
2658
2659 impl From<&TestValidator> for ConsensusKeysUpdated {
2660 fn from(value: &TestValidator) -> Self {
2661 Self {
2662 account: value.account,
2663 blsVK: value.bls_vk,
2664 schnorrVK: value.schnorr_vk,
2665 }
2666 }
2667 }
2668
2669 impl From<&TestValidator> for ConsensusKeysUpdatedV2 {
2670 fn from(value: &TestValidator) -> Self {
2671 Self {
2672 account: value.account,
2673 blsVK: value.bls_vk,
2674 schnorrVK: value.schnorr_vk,
2675 blsSig: value.bls_sig.into(),
2676 schnorrSig: value.schnorr_sig.clone(),
2677 }
2678 }
2679 }
2680
2681 impl From<&TestValidator> for ValidatorExit {
2682 fn from(value: &TestValidator) -> Self {
2683 Self {
2684 validator: value.account,
2685 }
2686 }
2687 }
2688
2689 impl Validator<BLSPubKey> {
2690 pub fn mock() -> Validator<BLSPubKey> {
2691 let val = TestValidator::random();
2692 let rng = &mut rand::thread_rng();
2693 let mut seed = [1u8; 32];
2694 rng.fill_bytes(&mut seed);
2695 let mut validator_stake = alloy::primitives::U256::from(0);
2696 let mut delegators = HashMap::new();
2697 for _i in 0..=5000 {
2698 let stake: u64 = rng.gen_range(0..10000);
2699 delegators.insert(Address::random(), alloy::primitives::U256::from(stake));
2700 validator_stake += alloy::primitives::U256::from(stake);
2701 }
2702
2703 let stake_table_key = val.bls_vk.into();
2704 let state_ver_key = val.schnorr_vk.into();
2705
2706 Validator {
2707 account: val.account,
2708 stake_table_key,
2709 state_ver_key,
2710 stake: validator_stake,
2711 commission: val.commission,
2712 delegators,
2713 }
2714 }
2715 }
2716}
2717
2718#[cfg(test)]
2719mod tests {
2720
2721 use alloy::{primitives::Address, rpc::types::Log};
2722 use hotshot_contract_adapter::stake_table::{sign_address_bls, StakeTableContractVersion};
2723 use hotshot_types::signature_key::BLSKeyPair;
2724 use pretty_assertions::assert_matches;
2725 use rstest::rstest;
2726
2727 use super::*;
2728 use crate::{v0::impls::testing::*, L1ClientOptions};
2729
2730 #[test_log::test]
2731 fn test_from_l1_events() -> anyhow::Result<()> {
2732 let val_1 = TestValidator::random();
2734 let val_1_new_keys = val_1.randomize_keys();
2735 let val_2 = TestValidator::random();
2736 let val_2_new_keys = val_2.randomize_keys();
2737 let delegator = Address::random();
2738 let mut events: Vec<StakeTableEvent> = [
2739 ValidatorRegistered::from(&val_1).into(),
2740 ValidatorRegisteredV2::from(&val_2).into(),
2741 Delegated {
2742 delegator,
2743 validator: val_1.account,
2744 amount: U256::from(10),
2745 }
2746 .into(),
2747 ConsensusKeysUpdated::from(&val_1_new_keys).into(),
2748 ConsensusKeysUpdatedV2::from(&val_2_new_keys).into(),
2749 Undelegated {
2750 delegator,
2751 validator: val_1.account,
2752 amount: U256::from(7),
2753 }
2754 .into(),
2755 Delegated {
2757 delegator,
2758 validator: val_1.account,
2759 amount: U256::from(5),
2760 }
2761 .into(),
2762 Delegated {
2764 delegator: Address::random(),
2765 validator: val_2.account,
2766 amount: U256::from(3),
2767 }
2768 .into(),
2769 ]
2770 .to_vec();
2771
2772 let validators_set = validator_set_from_l1_events(events.iter().cloned())?;
2773 let st = validators_set.active_validators;
2774 let st_val_1 = st.get(&val_1.account).unwrap();
2775 assert_eq!(st_val_1.stake, U256::from(8));
2777 assert_eq!(st_val_1.commission, val_1.commission);
2778 assert_eq!(st_val_1.delegators.len(), 1);
2779 assert_eq!(*st_val_1.delegators.get(&delegator).unwrap(), U256::from(8));
2781
2782 let st_val_2 = st.get(&val_2.account).unwrap();
2783 assert_eq!(st_val_2.stake, U256::from(3));
2784 assert_eq!(st_val_2.commission, val_2.commission);
2785 assert_eq!(st_val_2.delegators.len(), 1);
2786
2787 events.push(ValidatorExit::from(&val_1).into());
2788
2789 let validator_set = validator_set_from_l1_events(events.iter().cloned())?;
2790 let st = validator_set.active_validators;
2791 assert_eq!(st.get(&val_1.account), None);
2793
2794 let st_val_2 = st.get(&val_2.account).unwrap();
2796 assert_eq!(st_val_2.stake, U256::from(3));
2797 assert_eq!(st_val_2.commission, val_2.commission);
2798 assert_eq!(st_val_2.delegators.len(), 1);
2799
2800 events.push(ValidatorExit::from(&val_2).into());
2802
2803 assert!(validator_set_from_l1_events(events.iter().cloned()).is_err());
2805
2806 Ok(())
2807 }
2808
2809 #[test]
2810 fn test_from_l1_events_failures() -> anyhow::Result<()> {
2811 let val = TestValidator::random();
2812 let delegator = Address::random();
2813
2814 let register: StakeTableEvent = ValidatorRegistered::from(&val).into();
2815 let register_v2: StakeTableEvent = ValidatorRegisteredV2::from(&val).into();
2816 let delegate: StakeTableEvent = Delegated {
2817 delegator,
2818 validator: val.account,
2819 amount: U256::from(10),
2820 }
2821 .into();
2822 let key_update: StakeTableEvent = ConsensusKeysUpdated::from(&val).into();
2823 let key_update_v2: StakeTableEvent = ConsensusKeysUpdatedV2::from(&val).into();
2824 let undelegate: StakeTableEvent = Undelegated {
2825 delegator,
2826 validator: val.account,
2827 amount: U256::from(7),
2828 }
2829 .into();
2830
2831 let exit: StakeTableEvent = ValidatorExit::from(&val).into();
2832
2833 let cases = [
2834 vec![exit],
2835 vec![undelegate.clone()],
2836 vec![delegate.clone()],
2837 vec![key_update],
2838 vec![key_update_v2],
2839 vec![register.clone(), register.clone()],
2840 vec![register_v2.clone(), register_v2.clone()],
2841 vec![register.clone(), register_v2.clone()],
2842 vec![register_v2.clone(), register.clone()],
2843 vec![
2844 register,
2845 delegate.clone(),
2846 undelegate.clone(),
2847 undelegate.clone(),
2848 ],
2849 vec![register_v2, delegate, undelegate.clone(), undelegate],
2850 ];
2851
2852 for events in cases.iter() {
2853 let res = validators_from_l1_events(events.iter().cloned());
2857 assert!(
2858 res.is_err(),
2859 "events {res:?}, not a valid sequence of events"
2860 );
2861 }
2862 Ok(())
2863 }
2864
2865 #[test]
2866 fn test_validators_selection() {
2867 let mut validators = IndexMap::new();
2868 let mut highest_stake = alloy::primitives::U256::ZERO;
2869
2870 for _i in 0..3000 {
2871 let validator = Validator::mock();
2872 validators.insert(validator.account, validator.clone());
2873
2874 if validator.stake > highest_stake {
2875 highest_stake = validator.stake;
2876 }
2877 }
2878
2879 let minimum_stake = highest_stake / U256::from(VID_TARGET_TOTAL_STAKE);
2880
2881 let selected_validators =
2882 select_active_validator_set(&validators).expect("Failed to select validators");
2883 assert!(
2884 selected_validators.len() <= 100,
2885 "validators len is {}, expected at most 100",
2886 selected_validators.len()
2887 );
2888
2889 let mut selected_validators_highest_stake = alloy::primitives::U256::ZERO;
2890 for (address, validator) in &selected_validators {
2892 assert!(
2893 validator.stake >= minimum_stake,
2894 "Validator {:?} has stake below minimum: {}",
2895 address,
2896 validator.stake
2897 );
2898
2899 if validator.stake > selected_validators_highest_stake {
2900 selected_validators_highest_stake = validator.stake;
2901 }
2902 }
2903 }
2904
2905 #[rstest::rstest]
2908 fn test_regression_non_unique_bls_keys_not_discarded(
2909 #[values(StakeTableContractVersion::V1, StakeTableContractVersion::V2)]
2910 version: StakeTableContractVersion,
2911 ) {
2912 let val = TestValidator::random();
2913 let register: StakeTableEvent = match version {
2914 StakeTableContractVersion::V1 => ValidatorRegistered::from(&val).into(),
2915 StakeTableContractVersion::V2 => ValidatorRegisteredV2::from(&val).into(),
2916 };
2917 let delegate: StakeTableEvent = Delegated {
2918 delegator: Address::random(),
2919 validator: val.account,
2920 amount: U256::from(10),
2921 }
2922 .into();
2923
2924 assert!(
2926 validator_set_from_l1_events(vec![register.clone(), delegate.clone()].into_iter())
2927 .is_ok()
2928 );
2929
2930 let key_update = ConsensusKeysUpdated::from(&val).into();
2932 let err = validator_set_from_l1_events(vec![register, delegate, key_update].into_iter())
2933 .unwrap_err();
2934
2935 let bls: BLSPubKey = val.bls_vk.into();
2936 assert!(matches!(err, StakeTableError::BlsKeyAlreadyUsed(addr) if addr == bls.to_string()));
2937 }
2938
2939 #[test]
2942 fn test_regression_reregister_eth_account() {
2943 let val1 = TestValidator::random();
2944 let val2 = val1.randomize_keys();
2945 let account = val1.account;
2946
2947 let register1 = ValidatorRegisteredV2::from(&val1).into();
2948 let deregister1 = ValidatorExit::from(&val1).into();
2949 let register2 = ValidatorRegisteredV2::from(&val2).into();
2950 let events = [register1, deregister1, register2];
2951 let error = validators_from_l1_events(events.iter().cloned()).unwrap_err();
2952 assert_matches!(error, StakeTableError::ValidatorAlreadyExited(addr) if addr == account);
2953 }
2954
2955 #[test]
2956 fn test_display_log() {
2957 let serialized = r#"{"address":"0x0000000000000000000000000000000000000069",
2958 "topics":["0x0000000000000000000000000000000000000000000000000000000000000069"],
2959 "data":"0x69",
2960 "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2961 "blockNumber":"0x69","blockTimestamp":"0x69",
2962 "transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2963 "transactionIndex":"0x69","logIndex":"0x70","removed":false}"#;
2964 let log: Log = serde_json::from_str(serialized).unwrap();
2965 assert_eq!(
2966 log.display(),
2967 "Log(block=105,index=112,\
2968 transaction_hash=0x0000000000000000000000000000000000000000000000000000000000000069)"
2969 )
2970 }
2971
2972 #[rstest]
2973 #[case::v1(StakeTableContractVersion::V1)]
2974 #[case::v2(StakeTableContractVersion::V2)]
2975 fn test_register_validator(#[case] version: StakeTableContractVersion) {
2976 let mut state = StakeTableState::new();
2977 let validator = TestValidator::random();
2978
2979 let event = match version {
2980 StakeTableContractVersion::V1 => StakeTableEvent::Register((&validator).into()),
2981 StakeTableContractVersion::V2 => StakeTableEvent::RegisterV2((&validator).into()),
2982 };
2983
2984 state.apply_event(event).unwrap().unwrap();
2985
2986 let stored = state.validators.get(&validator.account).unwrap();
2987 assert_eq!(stored.account, validator.account);
2988 }
2989
2990 #[rstest]
2991 #[case::v1(StakeTableContractVersion::V1)]
2992 #[case::v2(StakeTableContractVersion::V2)]
2993 fn test_validator_already_registered(#[case] version: StakeTableContractVersion) {
2994 let mut stake_table_state = StakeTableState::new();
2995
2996 let test_validator = TestValidator::random();
2997
2998 match version {
3000 StakeTableContractVersion::V1 => {
3001 stake_table_state.apply_event(StakeTableEvent::Register((&test_validator).into()))
3002 },
3003 StakeTableContractVersion::V2 => {
3004 stake_table_state.apply_event(StakeTableEvent::RegisterV2((&test_validator).into()))
3005 },
3006 }
3007 .unwrap()
3008 .unwrap(); let v1_already_registered_result = stake_table_state
3012 .clone()
3013 .apply_event(StakeTableEvent::Register((&test_validator).into()));
3014
3015 pretty_assertions::assert_matches!(
3016 v1_already_registered_result, Err(StakeTableError::AlreadyRegistered(account))
3017 if account == test_validator.account,
3018 "Expected AlreadyRegistered error. version ={version:?} result={v1_already_registered_result:?}",
3019 );
3020
3021 let v2_already_registered_result = stake_table_state
3023 .clone()
3024 .apply_event(StakeTableEvent::RegisterV2((&test_validator).into()));
3025
3026 pretty_assertions::assert_matches!(
3027 v2_already_registered_result,
3028 Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
3029 "Expected AlreadyRegistered error. version ={version:?} result={v2_already_registered_result:?}",
3030
3031 );
3032 }
3033
3034 #[test]
3035 fn test_register_validator_v2_auth_fails() {
3036 let mut state = StakeTableState::new();
3037 let mut val = TestValidator::random();
3038 val.bls_sig = Default::default();
3039 let event = StakeTableEvent::RegisterV2((&val).into());
3040
3041 let result = state.apply_event(event);
3042 assert!(matches!(
3043 result,
3044 Err(StakeTableError::AuthenticationFailed(_))
3045 ));
3046 }
3047
3048 #[rstest]
3049 #[case::v1(StakeTableContractVersion::V1)]
3050 #[case::v2(StakeTableContractVersion::V2)]
3051 fn test_deregister_validator(#[case] version: StakeTableContractVersion) {
3052 let mut state = StakeTableState::new();
3053 let val = TestValidator::random();
3054
3055 let reg = StakeTableEvent::Register((&val).into());
3056 state.apply_event(reg).unwrap().unwrap();
3057
3058 let dereg = match version {
3059 StakeTableContractVersion::V1 => StakeTableEvent::Deregister((&val).into()),
3060 StakeTableContractVersion::V2 => StakeTableEvent::DeregisterV2(ValidatorExitV2 {
3061 validator: val.account,
3062 unlocksAt: U256::from(1000u64),
3063 }),
3064 };
3065 state.apply_event(dereg).unwrap().unwrap();
3066 assert!(!state.validators.contains_key(&val.account));
3067 }
3068
3069 #[rstest]
3070 #[case::v1(StakeTableContractVersion::V1)]
3071 #[case::v2(StakeTableContractVersion::V2)]
3072 fn test_delegate_and_undelegate(#[case] version: StakeTableContractVersion) {
3073 let mut state = StakeTableState::new();
3074 let val = TestValidator::random();
3075 state
3076 .apply_event(StakeTableEvent::Register((&val).into()))
3077 .unwrap()
3078 .unwrap();
3079
3080 let delegator = Address::random();
3081 let amount = U256::from(1000);
3082 let delegate_event = StakeTableEvent::Delegate(Delegated {
3083 delegator,
3084 validator: val.account,
3085 amount,
3086 });
3087 state.apply_event(delegate_event).unwrap().unwrap();
3088
3089 let validator = state.validators.get(&val.account).unwrap();
3090 assert_eq!(validator.delegators.get(&delegator).cloned(), Some(amount));
3091
3092 let undelegate_event = match version {
3093 StakeTableContractVersion::V1 => StakeTableEvent::Undelegate(Undelegated {
3094 delegator,
3095 validator: val.account,
3096 amount,
3097 }),
3098 StakeTableContractVersion::V2 => StakeTableEvent::UndelegateV2(UndelegatedV2 {
3099 delegator,
3100 validator: val.account,
3101 amount,
3102 unlocksAt: U256::from(2000u64),
3103 undelegationId: 1,
3104 }),
3105 };
3106 state.apply_event(undelegate_event).unwrap().unwrap();
3107 let validator = state.validators.get(&val.account).unwrap();
3108 assert!(!validator.delegators.contains_key(&delegator));
3109 }
3110
3111 #[rstest]
3112 #[case::v1(StakeTableContractVersion::V1)]
3113 #[case::v2(StakeTableContractVersion::V2)]
3114 fn test_key_update_event(#[case] version: StakeTableContractVersion) {
3115 let mut state = StakeTableState::new();
3116 let val = TestValidator::random();
3117
3118 state
3120 .apply_event(StakeTableEvent::Register((&val).into()))
3121 .unwrap()
3122 .unwrap();
3123
3124 let new_keys = val.randomize_keys();
3125
3126 let event = match version {
3127 StakeTableContractVersion::V1 => StakeTableEvent::KeyUpdate((&new_keys).into()),
3128 StakeTableContractVersion::V2 => StakeTableEvent::KeyUpdateV2((&new_keys).into()),
3129 };
3130
3131 state.apply_event(event).unwrap().unwrap();
3132
3133 let updated = state.validators.get(&val.account).unwrap();
3134 assert_eq!(updated.stake_table_key, new_keys.bls_vk.into());
3135 assert_eq!(updated.state_ver_key, new_keys.schnorr_vk.into());
3136 }
3137
3138 #[test]
3139 fn test_duplicate_bls_key() {
3140 let mut state = StakeTableState::new();
3141 let val = TestValidator::random();
3142 let event1 = StakeTableEvent::Register((&val).into());
3143 let mut val2 = TestValidator::random();
3144 val2.bls_vk = val.bls_vk;
3145 val2.account = Address::random();
3146
3147 let event2 = StakeTableEvent::Register((&val2).into());
3148 state.apply_event(event1).unwrap().unwrap();
3149 let result = state.apply_event(event2);
3150
3151 let expected_bls_key = BLSPubKey::from(val.bls_vk).to_string();
3152
3153 assert_matches!(
3154 result,
3155 Err(StakeTableError::BlsKeyAlreadyUsed(key))
3156 if key == expected_bls_key,
3157 "Expected BlsKeyAlreadyUsed({expected_bls_key}), but got: {result:?}",
3158 );
3159 }
3160
3161 #[test]
3162 fn test_duplicate_schnorr_key() {
3163 let mut state = StakeTableState::new();
3164 let val = TestValidator::random();
3165 let event1 = StakeTableEvent::Register((&val).into());
3166 let mut val2 = TestValidator::random();
3167 val2.schnorr_vk = val.schnorr_vk;
3168 val2.account = Address::random();
3169 val2.bls_vk = val2.randomize_keys().bls_vk;
3170
3171 let event2 = StakeTableEvent::Register((&val2).into());
3172 state.apply_event(event1).unwrap().unwrap();
3173 let result = state.apply_event(event2);
3174
3175 let schnorr: SchnorrPubKey = val.schnorr_vk.into();
3176 assert_matches!(
3177 result,
3178 Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(key)))
3179 if key == schnorr.to_string(),
3180 "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
3181
3182 );
3183 }
3184
3185 #[test]
3186 fn test_duplicate_schnorr_key_v2_during_update() {
3187 let mut state = StakeTableState::new();
3188
3189 let val1 = TestValidator::random();
3190
3191 let mut rng = &mut rand::thread_rng();
3192 let bls_key_pair = BLSKeyPair::generate(&mut rng);
3193
3194 let val2 = TestValidator {
3195 account: val1.account,
3196 bls_vk: bls_key_pair.ver_key().to_affine().into(),
3197 schnorr_vk: val1.schnorr_vk,
3198 commission: val1.commission,
3199 bls_sig: sign_address_bls(&bls_key_pair, val1.account).into(),
3200 schnorr_sig: val1.clone().schnorr_sig,
3201 };
3202 let event1 = StakeTableEvent::RegisterV2((&val1).into());
3203 let event2 = StakeTableEvent::KeyUpdateV2((&val2).into());
3204
3205 state.apply_event(event1).unwrap().unwrap();
3206 let result = state.apply_event(event2);
3207
3208 let schnorr: SchnorrPubKey = val1.schnorr_vk.into();
3209 assert_matches!(
3210 result,
3211 Err(StakeTableError::SchnorrKeyAlreadyUsed(key))
3212 if key == schnorr.to_string(),
3213 "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
3214 );
3215 }
3216
3217 #[test]
3218 fn test_register_and_deregister_validator() {
3219 let mut state = StakeTableState::new();
3220 let validator = TestValidator::random();
3221 let event = StakeTableEvent::Register((&validator).into());
3222 state.apply_event(event).unwrap().unwrap();
3223
3224 let deregister_event = StakeTableEvent::Deregister((&validator).into());
3225 assert!(state.apply_event(deregister_event).unwrap().is_ok());
3226 }
3227
3228 #[test]
3229 fn test_commission_validation_exceeds_basis_points() {
3230 let validator = TestValidator::random();
3232 let mut stake_table = StakeTableState::new();
3233
3234 let registration_event = ValidatorRegistered::from(&validator).into();
3236 stake_table
3237 .apply_event(registration_event)
3238 .unwrap()
3239 .unwrap();
3240
3241 let valid_commission_event = CommissionUpdated {
3243 validator: validator.account,
3244 timestamp: Default::default(),
3245 oldCommission: 0,
3246 newCommission: COMMISSION_BASIS_POINTS, }
3248 .into();
3249 stake_table
3250 .apply_event(valid_commission_event)
3251 .unwrap()
3252 .unwrap();
3253
3254 let invalid_commission = COMMISSION_BASIS_POINTS + 1;
3255 let invalid_commission_event = CommissionUpdated {
3256 validator: validator.account,
3257 timestamp: Default::default(),
3258 oldCommission: 0,
3259 newCommission: invalid_commission,
3260 }
3261 .into();
3262
3263 let err = stake_table
3264 .apply_event(invalid_commission_event)
3265 .unwrap_err();
3266
3267 assert_matches!(
3268 err,
3269 StakeTableError::InvalidCommission(addr, invalid_commission)
3270 if addr == addr && invalid_commission == invalid_commission);
3271 }
3272
3273 #[test]
3274 fn test_delegate_zero_amount_is_rejected() {
3275 let mut state = StakeTableState::new();
3276 let validator = TestValidator::random();
3277 let account = validator.account;
3278 state
3279 .apply_event(StakeTableEvent::Register((&validator).into()))
3280 .unwrap()
3281 .unwrap();
3282
3283 let delegator = Address::random();
3284 let amount = U256::ZERO;
3285 let event = StakeTableEvent::Delegate(Delegated {
3286 delegator,
3287 validator: account,
3288 amount,
3289 });
3290 let result = state.apply_event(event);
3291
3292 assert_matches!(
3293 result,
3294 Err(StakeTableError::ZeroDelegatorStake(addr))
3295 if addr == delegator,
3296 "delegator stake is zero"
3297
3298 );
3299 }
3300
3301 #[test]
3302 fn test_undelegate_more_than_stake_fails() {
3303 let mut state = StakeTableState::new();
3304 let validator = TestValidator::random();
3305 let account = validator.account;
3306 state
3307 .apply_event(StakeTableEvent::Register((&validator).into()))
3308 .unwrap()
3309 .unwrap();
3310
3311 let delegator = Address::random();
3312 let event = StakeTableEvent::Delegate(Delegated {
3313 delegator,
3314 validator: account,
3315 amount: U256::from(10u64),
3316 });
3317 state.apply_event(event).unwrap().unwrap();
3318
3319 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3320 delegator,
3321 validator: account,
3322 amount: U256::from(20u64),
3323 }));
3324 assert_matches!(
3325 result,
3326 Err(StakeTableError::InsufficientStake),
3327 "Expected InsufficientStake error, got: {result:?}",
3328 );
3329 }
3330
3331 #[test]
3332 fn test_apply_event_does_not_modify_state_on_error() {
3333 let mut state = StakeTableState::new();
3334 let validator = TestValidator::random();
3335 let delegator = Address::random();
3336
3337 state
3338 .apply_event(StakeTableEvent::Register((&validator).into()))
3339 .unwrap()
3340 .unwrap();
3341
3342 let state_before = state.clone();
3344 let result = state.apply_event(StakeTableEvent::Register((&validator).into()));
3345 assert_matches!(result, Err(StakeTableError::AlreadyRegistered(_)));
3346 assert_eq!(
3347 state, state_before,
3348 "State should not change on AlreadyRegistered error"
3349 );
3350
3351 let state_before = state.clone();
3353 let mut validator2 = TestValidator::random();
3354 validator2.bls_vk = validator.bls_vk; let result = state.apply_event(StakeTableEvent::Register((&validator2).into()));
3356 assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
3357 assert_eq!(
3358 state, state_before,
3359 "State should not change on BlsKeyAlreadyUsed error"
3360 );
3361
3362 let state_before = state.clone();
3364 let nonexistent_validator = TestValidator::random();
3365 let result =
3366 state.apply_event(StakeTableEvent::Deregister((&nonexistent_validator).into()));
3367 assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
3368 assert_eq!(
3369 state, state_before,
3370 "State should not change on ValidatorNotFound error"
3371 );
3372
3373 let state_before = state.clone();
3375 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3376 delegator: Address::random(),
3377 validator: Address::random(),
3378 amount: U256::from(100u64),
3379 }));
3380 assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
3381 assert_eq!(
3382 state, state_before,
3383 "State should not change on ValidatorNotFound error for Undelegate"
3384 );
3385
3386 state
3387 .apply_event(StakeTableEvent::Delegate(Delegated {
3388 delegator,
3389 validator: validator.account,
3390 amount: U256::from(100u64),
3391 }))
3392 .unwrap()
3393 .unwrap();
3394
3395 let state_before = state.clone();
3397 let non_existent_delegator = Address::random();
3398 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3399 delegator: non_existent_delegator,
3400 validator: validator.account,
3401 amount: U256::from(50u64),
3402 }));
3403 assert_matches!(result, Err(StakeTableError::DelegatorNotFound(_)));
3404 assert_eq!(
3405 state, state_before,
3406 "State should not change on DelegatorNotFound error"
3407 );
3408
3409 let state_before = state.clone();
3411 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3412 delegator,
3413 validator: validator.account,
3414 amount: U256::from(200u64),
3415 }));
3416 assert_matches!(result, Err(StakeTableError::InsufficientStake));
3417 assert_eq!(
3418 state, state_before,
3419 "State should not change on InsufficientStake error"
3420 );
3421
3422 let validator2 = TestValidator::random();
3424 let delegator2 = Address::random();
3425
3426 state
3427 .apply_event(StakeTableEvent::Register((&validator2).into()))
3428 .unwrap()
3429 .unwrap();
3430
3431 state
3432 .apply_event(StakeTableEvent::Delegate(Delegated {
3433 delegator: delegator2,
3434 validator: validator2.account,
3435 amount: U256::from(50u64),
3436 }))
3437 .unwrap()
3438 .unwrap();
3439 let state_before = state.clone();
3440 let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
3441 delegator: delegator2,
3442 validator: validator2.account,
3443 amount: U256::from(100u64),
3444 }));
3445 assert_matches!(result, Err(StakeTableError::InsufficientStake));
3446 assert_eq!(state, state_before,);
3447
3448 let state_before = state.clone();
3450 let result = state.apply_event(StakeTableEvent::Delegate(Delegated {
3451 delegator: Address::random(),
3452 validator: validator.account,
3453 amount: U256::ZERO,
3454 }));
3455 assert_matches!(result, Err(StakeTableError::ZeroDelegatorStake(_)));
3456 assert_eq!(
3457 state, state_before,
3458 "State should not change on ZeroDelegatorStake error"
3459 );
3460 }
3461
3462 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3463 async fn test_decaf_stake_table() {
3464 let events_json =
3500 std::fs::read_to_string("../data/v3/decaf_stake_table_events.json").unwrap();
3501 let events: Vec<(EventKey, StakeTableEvent)> = serde_json::from_str(&events_json).unwrap();
3502
3503 let reconstructed_stake_table =
3505 validator_set_from_l1_events(events.into_iter().map(|(_, e)| e))
3506 .unwrap()
3507 .active_validators;
3508
3509 let stake_table_json =
3510 std::fs::read_to_string("../data/v3/decaf_stake_table.json").unwrap();
3511 let expected: IndexMap<Address, Validator<BLSPubKey>> =
3512 serde_json::from_str(&stake_table_json).unwrap();
3513
3514 assert_eq!(
3515 reconstructed_stake_table, expected,
3516 "Stake table reconstructed from events does not match the expected stake table "
3517 );
3518 }
3519
3520 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3521 #[should_panic]
3522 async fn test_large_max_events_range_panic() {
3523 let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
3525
3526 let l1 = L1ClientOptions {
3527 l1_events_max_retry_duration: Duration::from_secs(30),
3528 l1_events_max_block_range: 10_u64.pow(9),
3530 l1_retry_delay: Duration::from_secs(1),
3531 ..Default::default()
3532 }
3533 .connect(vec!["https://ethereum-sepolia.publicnode.com"
3534 .parse()
3535 .unwrap()])
3536 .expect("unable to construct l1 client");
3537
3538 let latest_block = l1.provider.get_block_number().await.unwrap();
3539 let _events = Fetcher::fetch_events_from_contract(
3540 l1,
3541 contract_address.parse().unwrap(),
3542 None,
3543 latest_block,
3544 )
3545 .await
3546 .unwrap();
3547 }
3548
3549 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3550 async fn sanity_check_block_reward_v3() {
3551 let initial_supply = U256::from_str("10000000000000000000000000000").unwrap();
3553
3554 let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
3555 .checked_div(U256::from(COMMISSION_BASIS_POINTS))
3556 .unwrap();
3557
3558 println!("Calculated reward: {reward}");
3559 assert!(reward > U256::ZERO);
3560 }
3561
3562 #[test]
3563 fn sanity_check_p_and_rp() {
3564 let total_stake = BigDecimal::from_str("1000").unwrap();
3565 let total_supply = BigDecimal::from_str("10000").unwrap(); let (p, rp) =
3568 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3569
3570 assert!(p > BigDecimal::from(0));
3571 assert!(p < BigDecimal::from(1));
3572 assert!(rp > BigDecimal::from(0));
3573 }
3574
3575 #[test]
3576 fn test_p_out_of_range() {
3577 let total_stake = BigDecimal::from_str("1000").unwrap();
3578 let total_supply = BigDecimal::from_str("500").unwrap(); let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3581 assert!(result.is_err());
3582 }
3583
3584 #[test]
3585 fn test_zero_total_supply() {
3586 let total_stake = BigDecimal::from_str("1000").unwrap();
3587 let total_supply = BigDecimal::from(0);
3588
3589 let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3590 assert!(result.is_err());
3591 }
3592
3593 #[test]
3594 fn test_valid_p_and_rp() {
3595 let total_stake = BigDecimal::from_str("5000").unwrap();
3596 let total_supply = BigDecimal::from_str("10000").unwrap();
3597
3598 let (p, rp) =
3599 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3600
3601 assert_eq!(p, BigDecimal::from_str("0.5").unwrap());
3602 assert!(rp > BigDecimal::from_str("0.0").unwrap());
3603 }
3604
3605 #[test]
3606 fn test_very_small_p() {
3607 let total_stake = BigDecimal::from_str("1").unwrap(); let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap(); let (p, rp) =
3611 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3612
3613 assert!(p > BigDecimal::from_str("0").unwrap());
3614 assert!(p < BigDecimal::from_str("1e-18").unwrap()); assert!(rp > BigDecimal::from(0));
3616 }
3617
3618 #[test]
3619 fn test_p_very_close_to_one() {
3620 let total_stake = BigDecimal::from_str("9999999999999999999999999999").unwrap();
3621 let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap();
3622
3623 let (p, rp) =
3624 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3625
3626 assert!(p < BigDecimal::from(1));
3627 assert!(p > BigDecimal::from_str("0.999999999999999999999999999").unwrap());
3628 assert!(rp > BigDecimal::from(0));
3629 }
3630
3631 #[test]
3635 fn test_reward_rate_rp() {
3636 let test_cases = [
3637 ("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"), ];
3649
3650 let tolerance = BigDecimal::from_str("0.0001").unwrap();
3651
3652 let total_supply = BigDecimal::from_u32(10_000).unwrap();
3653
3654 for (p, rp) in test_cases {
3655 let p = BigDecimal::from_str(p).unwrap();
3656 let expected_rp = BigDecimal::from_str(rp).unwrap();
3657
3658 let total_stake = &p * &total_supply;
3659
3660 let (computed_p, computed_rp) =
3661 calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3662
3663 assert!(
3664 (&computed_p - &p).abs() < tolerance,
3665 "p mismatch: got {computed_p}, expected {p}"
3666 );
3667
3668 assert!(
3669 (&computed_rp - &expected_rp).abs() < tolerance,
3670 "R(p) mismatch for p={p}: got {computed_rp}, expected {expected_rp}"
3671 );
3672 }
3673 }
3674
3675 #[tokio::test(flavor = "multi_thread")]
3676 async fn test_dynamic_block_reward_with_expected_values() {
3677 let total_supply = U256::from_str("10000000000000000000000000000").unwrap();
3679 let total_supply_bd = BigDecimal::from_str(&total_supply.to_string()).unwrap();
3680
3681 let test_cases = [
3682 ("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"), ];
3716
3717 let tolerance = U256::from(100_000_000_000_000_000u128); for (p, rp, avg_block_time_ms, expected_reward) in test_cases {
3720 let p = BigDecimal::from_str(p).unwrap();
3721 let total_stake_bd = (&p * &total_supply_bd).round(0);
3722 println!("total_stake_bd={total_stake_bd}");
3723
3724 let total_stake = U256::from_str(&total_stake_bd.to_plain_string()).unwrap();
3725 let expected_reward = U256::from_str(expected_reward).unwrap();
3726
3727 let epoch = EpochNumber::new(0);
3728 let actual_reward = EpochCommittees::compute_block_reward(
3729 &epoch,
3730 total_supply,
3731 total_stake,
3732 avg_block_time_ms,
3733 )
3734 .unwrap()
3735 .0;
3736
3737 let diff = if actual_reward > expected_reward {
3738 actual_reward - expected_reward
3739 } else {
3740 expected_reward - actual_reward
3741 };
3742
3743 assert!(
3744 diff <= tolerance,
3745 "Reward mismatch for p = {p}, R(p) = {rp}, block_time = {avg_block_time_ms}: \
3746 expected = {expected_reward}, actual = {actual_reward}, diff = {diff}"
3747 );
3748 }
3749 }
3750}