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