1use std::{
2 collections::{BTreeMap, HashMap},
3 ops::Bound,
4 sync::Arc,
5 time::Duration,
6};
7
8use alloy::primitives::{Address, U256};
9use anyhow::{Context, bail};
10use async_lock::Mutex as AsyncMutex;
11use committable::Commitment;
12use hotshot_types::{
13 PeerConfig, PeerConnectInfo,
14 data::{BlockNumber, EpochNumber, ViewNumber},
15 drb::{
16 DrbResult, INITIAL_DRB_RESULT,
17 election::{RandomizedCommittee, generate_stake_cdf, select_randomized_leader},
18 },
19 epoch_membership::EpochMembershipCoordinator,
20 signature_key::BLSPubKey,
21 stake_table::StakeTableEntry,
22 traits::{
23 election::{GenesisStakeTables, Membership, MembershipSnapshot},
24 signature_key::{SignatureKey as _, StakeTableEntryType},
25 },
26 utils::{epoch_from_block_number, root_block_in_epoch, transition_block_for_epoch},
27};
28#[cfg(feature = "node")]
29use hotshot_types::{traits::block_contents::BlockHeader, utils::is_epoch_root};
30use indexmap::IndexMap;
31use parking_lot::RwLock;
32use thiserror::Error;
33use tracing::{debug, error, info, warn};
34#[cfg(feature = "node")]
35use versions::DRB_AND_HEADER_UPGRADE_VERSION;
36use versions::EPOCH_VERSION;
37
38use super::{
39 AuthenticatedValidatorMap, RegisteredValidatorMap, StakeTableHash, StakeTableState,
40 compute_block_reward,
41};
42use crate::{
43 Header, Leaf2, PubKey, SeqTypes,
44 traits::StateCatchup,
45 v0_3::{ASSUMED_BLOCK_TIME_SECONDS, AuthenticatedValidator, Fetcher, RewardAmount},
46};
47
48pub const RECENT_STAKE_TABLES_LIMIT: u64 = 20;
56
57const DEFAULT_STORAGE_READ_TIMEOUT: Duration = Duration::from_secs(60);
65
66#[derive(Clone, Debug)]
68pub struct EpochCommittees {
69 inner: Arc<RwLock<Inner>>,
70 fetcher: Arc<Fetcher>,
71 #[cfg_attr(not(feature = "node"), allow(dead_code))]
72 update_fixed_block_reward_lock: Arc<AsyncMutex<()>>,
73 load_from_storage_lock: Arc<AsyncMutex<()>>,
74 storage_read_timeout: Duration,
75 epoch_height: BlockNumber,
76}
77
78#[derive(Debug)]
79struct Inner {
80 genesis: Arc<GenesisStakeTables<SeqTypes>>,
84
85 genesis_da_committee: Arc<DaCommittee>,
87
88 snapshots: BTreeMap<EpochNumber, EpochSnapshot>,
92
93 #[cfg_attr(not(feature = "node"), allow(dead_code))]
95 all_validators: BTreeMap<EpochNumber, RegisteredValidatorMap>,
96
97 da_committees: BTreeMap<EpochNumber, Arc<DaCommittee>>,
101
102 prune_cutoff: EpochNumber,
106
107 first_epoch: EpochNumber,
108
109 fixed_block_reward: Option<RewardAmount>,
113}
114
115#[derive(Clone, Debug)]
116struct DaCommittee {
117 committee: Vec<PeerConfig<SeqTypes>>,
118 indexed_committee: HashMap<PubKey, PeerConfig<SeqTypes>>,
119}
120
121#[derive(Debug)]
123struct EpochCommittee {
124 eligible_leaders: Vec<PeerConfig<SeqTypes>>,
129 stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>>,
131 validators: AuthenticatedValidatorMap,
132 address_mapping: HashMap<BLSPubKey, Address>,
133 block_reward: Option<RewardAmount>,
134 stake_table_hash: Option<StakeTableHash>,
135 header: Option<Header>,
136}
137
138impl EpochCommittee {
139 fn new(
140 validators: AuthenticatedValidatorMap,
141 block_reward: Option<RewardAmount>,
142 hash: Option<StakeTableHash>,
143 header: Option<Header>,
144 ) -> Self {
145 let mut address_mapping = HashMap::new();
146 let stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>> = validators
147 .values()
148 .map(|v| {
149 let key = *v.stake_table_key();
150 address_mapping.insert(key, v.account);
151 (
152 key,
153 PeerConfig {
154 stake_table_entry: BLSPubKey::stake_table_entry(&key, v.stake),
155 state_ver_key: v.state_ver_key().clone(),
156 connect_info: v.x25519_key.and_then(|p| {
157 let a = v.p2p_addr.clone()?;
158 Some(PeerConnectInfo {
159 x25519_key: p,
160 p2p_addr: a,
161 })
162 }),
163 },
164 )
165 })
166 .collect();
167
168 let eligible_leaders: Vec<PeerConfig<SeqTypes>> =
169 stake_table.iter().map(|(_, l)| l.clone()).collect();
170
171 Self {
172 eligible_leaders,
173 stake_table,
174 validators,
175 address_mapping,
176 block_reward,
177 stake_table_hash: hash,
178 header,
179 }
180 }
181}
182
183impl EpochCommittees {
184 pub fn epoch_height(&self) -> BlockNumber {
185 self.epoch_height
186 }
187
188 pub fn first_epoch(&self) -> EpochNumber {
189 self.inner.read().first_epoch
190 }
191
192 pub fn fetcher(&self) -> &Fetcher {
193 &self.fetcher
194 }
195
196 pub fn fixed_block_reward(&self) -> Option<RewardAmount> {
197 self.inner.read().fixed_block_reward
198 }
199
200 pub fn latest_peer_config(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
205 let inner = self.inner.read();
206 for snap in inner.snapshots.values().rev() {
207 if let Some(cfg) = snap.inner.committee.stake_table.get(key) {
208 return Some(cfg.clone());
209 }
210 }
211 inner.genesis.stake(key)
212 }
213
214 pub fn latest_account(&self, key: &PubKey) -> Option<Address> {
215 let inner = self.inner.read();
216 inner
217 .snapshots
218 .values()
219 .rev()
220 .find_map(|snap| snap.inner.committee.address_mapping.get(key).copied())
221 }
222
223 #[cfg(feature = "node")]
228 async fn fetch_and_update_fixed_block_reward(
229 &self,
230 epoch: EpochNumber,
231 ) -> anyhow::Result<RewardAmount> {
232 let _guard = self.update_fixed_block_reward_lock.lock().await;
234
235 #[allow(clippy::significant_drop_in_scrutinee)]
244 if let Some(reward) = self.inner.read().fixed_block_reward {
245 Ok(reward)
246 } else {
247 warn!(%epoch,
248 "Block reward is None. attempting to fetch it from L1",
249 );
250 let block_reward =
251 self.fetcher
252 .fetch_fixed_block_reward()
253 .await
254 .inspect_err(|err| {
255 error!(?epoch, ?err, "failed to fetch block_reward");
256 })?;
257 self.inner.write().fixed_block_reward = Some(block_reward);
258 Ok(block_reward)
259 }
260 }
261
262 async fn calculate_dynamic_block_reward(
269 &self,
270 epoch: EpochNumber,
271 header: &Header,
272 validators: &AuthenticatedValidatorMap,
273 coordinator: &EpochMembershipCoordinator<SeqTypes>,
274 ) -> anyhow::Result<Option<RewardAmount>> {
275 let epoch_height = self.epoch_height;
276 let current_epoch = epoch_from_block_number(header.height(), *epoch_height);
277 let previous_epoch = current_epoch
278 .checked_sub(1)
279 .context("underflow: cannot get previous epoch when current_epoch is 0")?;
280 debug!(?epoch, "previous_epoch={previous_epoch:?}");
281
282 let first_epoch = *self.first_epoch();
283
284 if previous_epoch > first_epoch + 1 && self.snapshot(previous_epoch.into()).is_none() {
287 warn!(?previous_epoch, "missing stake table for previous epoch");
288 return Ok(None);
289 }
290
291 let previous_reward_distributed = header
292 .total_reward_distributed()
293 .context("Invalid block header: missing total_reward_distributed field")?;
294
295 let total_stake: U256 = validators.values().map(|v| v.stake).sum();
297 let initial_supply = self.fetcher.initial_supply_or_fetch().await?;
298 let total_supply = initial_supply
299 .checked_add(previous_reward_distributed.0)
300 .context("initial_supply + previous_reward_distributed overflow")?;
301
302 let curr_ts = header.timestamp_millis_internal();
304 debug!(?epoch, "curr_ts={curr_ts:?}");
305
306 let average_block_time_ms = if previous_epoch <= first_epoch + 1 {
310 ASSUMED_BLOCK_TIME_SECONDS as u64 * 1000 } else {
312 let next_epoch = epoch
316 .checked_sub(1)
317 .context("underflow: cannot get next epoch when epoch is 0")?;
318 let prev_ts = match self.map_header(next_epoch, |h| h.timestamp_millis_internal()) {
319 Some(ts) => ts,
320 None => {
321 info!(
322 "Calculating rewards for epoch {}, we have no root leaf header for epoch \
323 - 1. Fetching from peers",
324 epoch
325 );
326
327 let root_height = header.height().checked_sub(*epoch_height).context(
328 "Epoch height is greater than block height. cannot compute previous epoch \
329 root height",
330 )?;
331
332 self.fetcher
333 .peers
334 .fetch_leaf(coordinator.clone(), root_height)
335 .await
336 .context("Epoch root leaf not found")?
337 .block_header()
338 .timestamp_millis_internal()
339 },
340 };
341
342 let time_diff = curr_ts.checked_sub(prev_ts).context(
343 "Current timestamp is earlier than previous. underflow in block time calculation",
344 )?;
345
346 time_diff
347 .checked_div(*epoch_height)
348 .context("Epoch height is zero. cannot compute average block time")?
349 };
350 info!(?epoch, %total_supply, %total_stake, %average_block_time_ms,
351 "dynamic block reward parameters");
352
353 let block_reward =
354 compute_block_reward(epoch, total_supply, total_stake, average_block_time_ms)?;
355
356 Ok(Some(block_reward))
357 }
358
359 pub fn epoch_block_reward(&self, epoch: EpochNumber) -> Option<RewardAmount> {
361 self.inner
362 .read()
363 .epoch_committee(epoch)
364 .and_then(|committee| committee.block_reward)
365 }
366
367 pub fn get_validator_index(&self, epoch: EpochNumber, bls_key: &PubKey) -> Option<usize> {
372 self.inner
373 .read()
374 .epoch_committee(epoch)
375 .and_then(|committee| committee.stake_table.get_index_of(bls_key))
376 }
377
378 pub fn active_validators(&self, e: EpochNumber) -> anyhow::Result<AuthenticatedValidatorMap> {
379 self.inner.read().active_validators(e)
380 }
381
382 pub fn address(&self, e: EpochNumber, key: &BLSPubKey) -> anyhow::Result<Address> {
383 self.inner.read().address(e, key)
384 }
385
386 pub fn get_validator_config(
387 &self,
388 epoch: EpochNumber,
389 key: &BLSPubKey,
390 ) -> anyhow::Result<AuthenticatedValidator<BLSPubKey>> {
391 let inner = self.inner.read();
392 let address = inner.address(epoch, key)?;
393 let validators = inner.active_validators(epoch)?;
394 validators
395 .get(&address)
396 .context("validator not found")
397 .cloned()
398 }
399
400 pub fn new<B: Into<BlockNumber>>(
405 committee_members: Vec<PeerConfig<SeqTypes>>,
406 da_members: Vec<PeerConfig<SeqTypes>>,
407 fixed_block_reward: Option<RewardAmount>,
408 fetcher: Fetcher,
409 epoch_height: B,
410 first_epoch: EpochNumber,
411 ) -> Self {
412 let stake_table: Vec<_> = committee_members
414 .iter()
415 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
416 .cloned()
417 .collect();
418
419 let eligible_leaders = stake_table.clone();
420 let da_members: Vec<_> = da_members
422 .iter()
423 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
424 .cloned()
425 .collect();
426
427 let indexed_da_members: HashMap<PubKey, _> = da_members
429 .iter()
430 .map(|peer_config| {
431 (
432 PubKey::public_key(&peer_config.stake_table_entry),
433 peer_config.clone(),
434 )
435 })
436 .collect();
437
438 let genesis = Arc::new(GenesisStakeTables::new(
439 stake_table.clone(),
440 da_members.clone(),
441 ));
442 let da_committee = Arc::new(DaCommittee {
443 committee: da_members,
444 indexed_committee: indexed_da_members,
445 });
446
447 let epoch_committee = Arc::new(EpochCommittee {
448 eligible_leaders,
449 stake_table: stake_table
450 .iter()
451 .map(|x| (PubKey::public_key(&x.stake_table_entry), x.clone()))
452 .collect(),
453 validators: Default::default(),
454 address_mapping: HashMap::new(),
455 block_reward: Default::default(),
456 stake_table_hash: None,
457 header: None,
458 });
459
460 let mut snapshots = BTreeMap::new();
461 snapshots.insert(
462 EpochNumber::genesis(),
463 EpochSnapshot::new(
464 EpochNumber::genesis(),
465 first_epoch,
466 epoch_committee.clone(),
467 None,
468 da_committee.clone(),
469 ),
470 );
471 snapshots.insert(
473 EpochNumber::genesis() + 1u64,
474 EpochSnapshot::new(
475 EpochNumber::genesis() + 1u64,
476 first_epoch,
477 epoch_committee,
478 None,
479 da_committee.clone(),
480 ),
481 );
482
483 let committees = Self {
484 inner: Arc::new(RwLock::new(Inner {
485 genesis,
486 genesis_da_committee: da_committee,
487 da_committees: BTreeMap::new(),
488 snapshots,
489 all_validators: BTreeMap::new(),
490 prune_cutoff: EpochNumber::genesis(),
491 first_epoch,
492 fixed_block_reward,
493 })),
494 fetcher: Arc::new(fetcher),
495 update_fixed_block_reward_lock: Arc::new(AsyncMutex::new(())),
496 load_from_storage_lock: Arc::new(AsyncMutex::new(())),
497 storage_read_timeout: DEFAULT_STORAGE_READ_TIMEOUT,
498 epoch_height: epoch_height.into(),
499 };
500 committees.seed_first_epoch(first_epoch);
501 committees
502 }
503
504 pub fn with_storage_read_timeout(mut self, timeout: Duration) -> Self {
507 self.storage_read_timeout = timeout;
508 self
509 }
510
511 #[cfg(feature = "node")]
512 pub async fn reload_stake(&mut self, limit: u64) {
513 match self.fetcher.fetch_fixed_block_reward().await {
514 Ok(block_reward) => {
515 info!("Fetched block reward: {block_reward}");
516 self.inner.write().fixed_block_reward = Some(block_reward);
517 },
518 Err(err) => {
519 warn!("Failed to fetch the block reward when reloading the stake tables: {err}");
520 },
521 }
522
523 let loaded_stake = match self
525 .fetcher
526 .persistence
527 .lock()
528 .await
529 .load_latest_stake(limit)
530 .await
531 {
532 Ok(Some(loaded)) => loaded,
533 Ok(None) => {
534 warn!("No stake table history found in persistence!");
535 return;
536 },
537 Err(e) => {
538 error!("Failed to load stake table history from persistence: {e}");
539 return;
540 },
541 };
542
543 let first_epoch = self.inner.read().first_epoch;
544 for (epoch, (validators, block_reward), stake_table_hash) in loaded_stake {
545 if epoch <= first_epoch + 1 {
547 continue;
548 }
549 let committee = EpochCommittee::new(validators, block_reward, stake_table_hash, None);
550 self.inner
551 .write()
552 .put_epoch_committee(epoch, Arc::new(committee));
553 }
554 }
555
556 fn map_header<E, F, R>(&self, epoch: E, f: F) -> Option<R>
558 where
559 E: Into<EpochNumber>,
560 F: FnMut(&Header) -> R,
561 {
562 self.inner
563 .read()
564 .epoch_committee(epoch.into())
565 .and_then(|committee| committee.header.as_ref().map(f))
566 }
567
568 fn seed_first_epoch(&self, epoch: EpochNumber) {
570 let rand_comm = Arc::new(
571 self.randomized_committee(EpochNumber::genesis(), INITIAL_DRB_RESULT)
572 .expect("committee exist at genesis"),
573 );
574
575 let mut inner = self.inner.write();
576 let epoch_committee = inner
577 .epoch_committee(EpochNumber::genesis())
578 .expect("committee exists at genesis")
579 .clone();
580
581 inner.put_epoch_committee(epoch, epoch_committee.clone());
584 inner.put_randomized_committee(epoch, rand_comm.clone());
585 inner.put_epoch_committee(epoch + 1, epoch_committee);
586 inner.put_randomized_committee(epoch + 1, rand_comm);
587 }
588
589 fn randomized_committee(
590 &self,
591 epoch: EpochNumber,
592 drb: DrbResult,
593 ) -> Option<RandomizedCommittee<StakeTableEntry<PubKey>>> {
594 let inner = self.inner.read();
595 let Some(raw_stake_table) = inner.epoch_committee(epoch) else {
596 error!(
597 "randomized_committee({epoch}, {drb:?}) was called, but we do not yet have the \
598 stake table for epoch {epoch}"
599 );
600 return None;
601 };
602
603 let leaders = raw_stake_table
604 .eligible_leaders
605 .clone()
606 .into_iter()
607 .map(|peer_config| peer_config.stake_table_entry)
608 .collect::<Vec<_>>();
609
610 Some(generate_stake_cdf(leaders, drb))
611 }
612}
613
614pub async fn fetch_and_calculate_block_reward(
624 coordinator: EpochMembershipCoordinator<SeqTypes>,
625 current_epoch: EpochNumber,
626) -> anyhow::Result<RewardAmount> {
627 let committee;
628 let first_epoch;
629 let fixed_block_reward;
630 {
631 let membership = coordinator.membership().inner.read();
632 fixed_block_reward = membership.fixed_block_reward;
633
634 committee = membership
635 .epoch_committee(current_epoch)
636 .context(format!("committee not found for epoch={current_epoch:?}"))?
637 .clone();
638
639 if let Some(reward) = committee.block_reward {
641 return Ok(reward);
642 }
643
644 first_epoch = membership.first_epoch;
645 }
646
647 if *current_epoch <= *first_epoch + 1 {
648 bail!(
649 "epoch is in first two epochs: current_epoch={current_epoch}, \
650 first_epoch={first_epoch}"
651 );
652 }
653
654 let header = match committee.header.clone() {
655 Some(header) => header,
656 None => {
657 let root_epoch = current_epoch.checked_sub(2).context(format!(
658 "Epoch calculation underflow (current_epoch={current_epoch})"
659 ))?;
660
661 info!(?root_epoch, "catchup epoch root header");
662
663 let leaf = coordinator
664 .get_epoch_root(EpochNumber::new(root_epoch))
665 .await
666 .with_context(|| format!("Failed to get epoch root for root_epoch={root_epoch}"))?;
667 leaf.block_header().clone()
668 },
669 };
670
671 if header.version() <= EPOCH_VERSION {
672 return fixed_block_reward.context(format!(
673 "Fixed block reward not found for current_epoch={current_epoch}"
674 ));
675 }
676
677 let prev_epoch_u64 = current_epoch.checked_sub(1).context(format!(
678 "Underflow: cannot compute previous epoch when current_epoch={current_epoch}"
679 ))?;
680
681 let prev_epoch = EpochNumber::new(prev_epoch_u64);
682
683 if *prev_epoch > *first_epoch + 1
686 && let Err(err) = coordinator.stake_table_for_epoch(Some(prev_epoch))
687 {
688 info!("failed to get membership for epoch={prev_epoch:?}: {err:#}");
689
690 coordinator
691 .wait_for_catchup(prev_epoch)
692 .await
693 .context(format!("failed to catch up for epoch={prev_epoch}"))?;
694 }
695
696 coordinator
697 .membership()
698 .calculate_dynamic_block_reward(current_epoch, &header, &committee.validators, &coordinator)
699 .await
700 .with_context(|| {
701 format!("dynamic block reward calculation failed for epoch={current_epoch}")
702 })?
703 .with_context(|| format!("dynamic block reward returned None. epoch={current_epoch}"))
704}
705
706impl Membership<SeqTypes> for EpochCommittees {
707 type Error = EpochCommitteesError;
708 type Snapshot = EpochSnapshot;
709
710 fn snapshot(&self, epoch: EpochNumber) -> Option<Self::Snapshot> {
711 self.inner.read().snapshots.get(&epoch).cloned()
712 }
713
714 async fn load_stake_table(&self, epoch: EpochNumber) -> bool {
720 if self.inner.read().snapshots.contains_key(&epoch) {
721 return true;
722 }
723 let locks = tokio::time::timeout(self.storage_read_timeout, async {
724 let guard = self.load_from_storage_lock.lock().await;
726 if self.inner.read().snapshots.contains_key(&epoch) {
728 return None;
729 }
730 let persistence = self.fetcher.persistence.lock().await;
731 Some((guard, persistence))
732 })
733 .await;
734 let (_guard, persistence) = match locks {
735 Ok(Some(locks)) => locks,
736 Ok(None) => return true,
738 Err(_) => {
739 if self.inner.read().snapshots.contains_key(&epoch) {
741 return true;
742 }
743 warn!(
744 %epoch,
745 timeout = ?self.storage_read_timeout,
746 "timed out queueing for the storage locks; treating the epoch as not persisted"
747 );
748 return false;
749 },
750 };
751 if self.inner.read().snapshots.contains_key(&epoch) {
753 return true;
754 }
755 let read = tokio::time::timeout(self.storage_read_timeout, async {
757 let stake = match persistence.load_stake(epoch).await {
758 Ok(Some(stake)) => stake,
759 Ok(None) => return None,
760 Err(err) => {
761 warn!(%err, "failed to load stake table for epoch {epoch} from persistence");
762 return None;
763 },
764 };
765 let drb = match persistence.load_drb_result(epoch).await {
766 Ok(drb) => drb,
767 Err(err) => {
768 warn!(%err, "failed to load drb result for epoch {epoch} from persistence");
769 None
770 },
771 };
772 let header = match persistence.load_epoch_root(epoch).await {
773 Ok(header) => header,
774 Err(err) => {
775 warn!(%err, "failed to load epoch root for epoch {epoch} from persistence");
776 None
777 },
778 };
779 Some((stake, drb, header))
780 })
781 .await;
782 let ((validators, block_reward, stake_table_hash), drb, header) = match read {
783 Ok(Some(loaded)) => loaded,
784 Ok(None) => return false,
785 Err(_) => {
786 if self.inner.read().snapshots.contains_key(&epoch) {
787 return true;
788 }
789 warn!(
790 %epoch,
791 timeout = ?self.storage_read_timeout,
792 "storage reads for the stake table timed out; treating the epoch as not \
793 persisted"
794 );
795 return false;
796 },
797 };
798 drop(persistence);
799 info!(
800 %epoch,
801 has_drb = drb.is_some(),
802 has_header = header.is_some(),
803 "stake table loaded from persistence"
804 );
805 let committee = Arc::new(EpochCommittee::new(
806 validators,
807 block_reward,
808 stake_table_hash,
809 header,
810 ));
811 let randomized = drb.map(|drb| {
812 let leaders = committee
813 .eligible_leaders
814 .iter()
815 .map(|peer_config| peer_config.stake_table_entry.clone())
816 .collect();
817 Arc::new(generate_stake_cdf(leaders, drb))
818 });
819 let mut inner = self.inner.write();
820 inner.put_epoch_committee(epoch, committee);
821 if let Some(randomized) = randomized {
822 inner.put_randomized_committee(epoch, randomized);
823 }
824 true
825 }
826
827 fn genesis_stake_tables(&self) -> Arc<GenesisStakeTables<SeqTypes>> {
828 self.inner.read().genesis.clone()
829 }
830
831 #[cfg(not(feature = "node"))]
835 async fn add_epoch_root(
836 &self,
837 _block_header: Header,
838 _coordinator: &EpochMembershipCoordinator<SeqTypes>,
839 ) -> Result<(), Self::Error> {
840 unimplemented!("add_epoch_root requires the node feature");
842 }
843
844 #[cfg(feature = "node")]
848 async fn add_epoch_root(
849 &self,
850 block_header: Header,
851 coordinator: &EpochMembershipCoordinator<SeqTypes>,
852 ) -> Result<(), Self::Error> {
853 let block_number = block_header.block_number();
854
855 let epoch_height = *self.epoch_height;
856
857 let epoch = EpochNumber::new(epoch_from_block_number(block_number, epoch_height) + 2);
858
859 info!(?epoch, "adding epoch root. height={:?}", block_number);
860
861 if !is_epoch_root(block_number, epoch_height) {
862 error!(
863 "`add_epoch_root` was called with a block header that was not the root block for \
864 an epoch. This should never happen. Header:\n\n{block_header:?}"
865 );
866 return Err(Self::Error::NoRootBlock(block_number.into()));
867 }
868
869 if let Err(e) = self
877 .fetcher
878 .persistence
879 .lock()
880 .await
881 .store_epoch_root(epoch, block_header.clone())
882 .await
883 {
884 error!(?e, ?epoch, "`add_epoch_root`, error storing epoch root");
885 }
886
887 let version = block_header.version();
888 self.fetcher
890 .update_chain_config(&block_header)
891 .await
892 .map_err(Self::Error::Fetcher)?;
893
894 let mut block_reward = None;
895 if version == EPOCH_VERSION {
898 let reward = self
899 .fetch_and_update_fixed_block_reward(epoch)
900 .await
901 .map_err(Self::Error::Fetcher)?;
902 block_reward = Some(reward);
903 }
904
905 self.load_stake_table(epoch).await;
906
907 let epoch_committee = self.inner.read().epoch_committee(epoch).cloned();
908
909 let (active_validators, all_validators, stake_table_hash) = match epoch_committee {
918 Some(committee)
919 if committee.block_reward.is_some()
920 && committee.header.is_some()
921 && committee.stake_table_hash.is_some() =>
922 {
923 info!(
924 ?epoch,
925 "committee already has block reward, header, and stake table hash; skipping \
926 add_epoch_root"
927 );
928 return Ok(());
929 },
930
931 Some(committee) => {
932 if let Some(reward) = committee.block_reward {
933 block_reward = Some(reward);
934 }
935
936 if let Some(hash) = committee.stake_table_hash {
937 (committee.validators.clone(), Default::default(), Some(hash))
938 } else {
939 info!(
941 "Stake table hash missing for epoch {epoch}. recalculating by fetching \
942 from l1."
943 );
944 let set = self
945 .fetcher
946 .fetch(epoch, &block_header)
947 .await
948 .map_err(Self::Error::Fetcher)?;
949 (
950 set.active_validators,
951 set.all_validators,
952 set.stake_table_hash,
953 )
954 }
955 },
956
957 None => {
958 info!("Stake table missing for epoch {epoch}. Fetching from L1.");
959 let set = self
960 .fetcher
961 .fetch(epoch, &block_header)
962 .await
963 .map_err(Self::Error::Fetcher)?;
964 (
965 set.active_validators,
966 set.all_validators,
967 set.stake_table_hash,
968 )
969 },
970 };
971
972 if block_reward.is_none() && version >= DRB_AND_HEADER_UPGRADE_VERSION {
976 info!(?epoch, "calculating dynamic block reward");
977 let reward = self
978 .calculate_dynamic_block_reward(
979 epoch,
980 &block_header,
981 &active_validators,
982 coordinator,
983 )
984 .await
985 .map_err(Self::Error::Reward)?;
986
987 info!(?epoch, "calculated dynamic block reward = {reward:?}");
988 block_reward = reward;
989 }
990
991 let committee = EpochCommittee::new(
992 active_validators.clone(),
993 block_reward,
994 stake_table_hash,
995 Some(block_header.clone()),
996 );
997
998 let previous_epoch;
999 let previous_committee;
1000 let previous_validators;
1001 {
1002 let mut inner = self.inner.write();
1003 inner.put_epoch_committee(epoch, Arc::new(committee));
1004 previous_epoch = EpochNumber::new(epoch.saturating_sub(1));
1007 previous_committee = inner.epoch_committee(previous_epoch).cloned();
1008 inner.all_validators = inner.all_validators.split_off(&previous_epoch);
1010 previous_validators = inner.all_validators.remove(&previous_epoch);
1012 inner.all_validators.insert(epoch, all_validators.clone());
1013 inner.prune_epochs(epoch);
1014 }
1015
1016 let persistence_lock = self.fetcher.persistence.lock().await;
1017
1018 let decided_hash = block_header.next_stake_table_hash();
1019
1020 if let Some(previous_committee) = previous_committee {
1027 if decided_hash.is_none() || decided_hash == previous_committee.stake_table_hash {
1028 if let Err(e) = persistence_lock
1029 .store_stake(
1030 previous_epoch,
1031 previous_committee.validators.clone(),
1032 previous_committee.block_reward,
1033 previous_committee.stake_table_hash,
1034 )
1035 .await
1036 {
1037 error!(
1038 ?e,
1039 ?previous_epoch,
1040 "`add_epoch_root`, error storing stake table"
1041 );
1042 }
1043
1044 if let Some(previous_validators) = previous_validators
1045 && let Err(e) = persistence_lock
1046 .store_all_validators(previous_epoch, previous_validators)
1047 .await
1048 {
1049 error!(?e, ?epoch, "`add_epoch_root`, error storing all validators");
1050 }
1051 } else {
1052 panic!(
1053 "The decided block header's `next_stake_table_hash` does not match the hash \
1054 of the stake table we have. This is an unrecoverable error likely due to \
1055 issues with your L1 RPC provider. Decided:\n\n{:?}Actual:\n\n{:?}",
1056 decided_hash, previous_committee.stake_table_hash
1057 );
1058 }
1059 }
1060
1061 Ok(())
1062 }
1063
1064 async fn get_epoch_root(
1065 &self,
1066 epoch: EpochNumber,
1067 coordinator: &EpochMembershipCoordinator<SeqTypes>,
1068 ) -> Result<Leaf2, Self::Error> {
1069 let block_height = root_block_in_epoch(*epoch, *self.epoch_height());
1070 let peers = self.fetcher.peers.clone();
1071
1072 for retry in 0..3 {
1076 match peers
1077 .try_fetch_leaf(retry, coordinator.clone(), block_height)
1078 .await
1079 {
1080 Ok(leaf) => return Ok(leaf),
1081 Err(err) => {
1082 warn!(%epoch, block_height, retry, "failed to fetch epoch root leaf: {err:#}");
1083 },
1084 }
1085 }
1086 Err(Self::Error::Catchup(anyhow::anyhow!(
1087 "failed to fetch epoch root leaf for epoch {epoch} (block {block_height}) from peers"
1088 )))
1089 }
1090
1091 async fn get_epoch_drb(
1092 &self,
1093 epoch: EpochNumber,
1094 coordinator: &EpochMembershipCoordinator<SeqTypes>,
1095 ) -> Result<DrbResult, Self::Error> {
1096 let peers = self.fetcher.peers.clone();
1097
1098 if let Some(snap) = self.snapshot(epoch)
1100 && let Some(rand) = &snap.inner.randomized
1101 {
1102 return Ok(rand.drb_result());
1103 }
1104
1105 let previous_epoch = match epoch.checked_sub(1) {
1107 Some(epoch) => EpochNumber::new(epoch),
1108 None => {
1109 return self
1110 .snapshot(epoch)
1111 .and_then(|s| s.inner.randomized.as_ref().map(|r| r.drb_result()))
1112 .ok_or_else(|| {
1113 Self::Error::Message(format!(
1114 "Missing randomized committee for epoch {epoch}"
1115 ))
1116 });
1117 },
1118 };
1119
1120 let block_height = transition_block_for_epoch(*previous_epoch, *self.epoch_height());
1121
1122 debug!(
1123 "Getting DRB for epoch {}, block height {}",
1124 epoch, block_height
1125 );
1126 let drb_leaf = peers
1127 .try_fetch_leaf(1, coordinator.clone(), block_height)
1128 .await
1129 .map_err(Self::Error::Catchup)?;
1130
1131 let Some(drb) = drb_leaf.next_drb_result else {
1132 error!(
1133 "We received a leaf that should contain a DRB result, but the DRB result is \
1134 missing: {:?}",
1135 drb_leaf
1136 );
1137
1138 return Err(Self::Error::Message(
1139 "DRB leaf is missing the DRB result.".to_string(),
1140 ));
1141 };
1142
1143 Ok(drb)
1144 }
1145
1146 fn add_drb_result(&self, epoch: EpochNumber, drb: DrbResult) {
1147 info!("Adding DRB result {drb:?} to epoch {epoch}");
1148 if let Some(committee) = self.randomized_committee(epoch, drb) {
1149 self.inner
1150 .write()
1151 .put_randomized_committee(epoch, Arc::new(committee));
1152 }
1153 }
1154
1155 fn first_epoch(&self) -> EpochNumber {
1156 self.inner.read().first_epoch
1157 }
1158
1159 fn highest_known_epoch(&self) -> Option<EpochNumber> {
1160 self.inner.read().snapshots.keys().max().copied()
1161 }
1162
1163 fn add_da_committee(&self, first_epoch: EpochNumber, committee: Vec<PeerConfig<SeqTypes>>) {
1164 let indexed_committee: HashMap<PubKey, _> = committee
1165 .iter()
1166 .map(|peer_config| {
1167 (
1168 PubKey::public_key(&peer_config.stake_table_entry),
1169 peer_config.clone(),
1170 )
1171 })
1172 .collect();
1173
1174 let da_committee = Arc::new(DaCommittee {
1175 committee,
1176 indexed_committee,
1177 });
1178
1179 let mut inner = self.inner.write();
1180 inner
1181 .da_committees
1182 .insert(first_epoch, da_committee.clone());
1183
1184 let upper = inner
1190 .da_committees
1191 .range((Bound::Excluded(first_epoch), Bound::Unbounded))
1192 .next()
1193 .map(|(k, _)| *k);
1194
1195 let range = if let Some(u) = upper {
1196 (Bound::Included(first_epoch), Bound::Excluded(u))
1197 } else {
1198 (Bound::Included(first_epoch), Bound::Unbounded)
1199 };
1200
1201 let affected: Vec<EpochNumber> = inner.snapshots.range(range).map(|(k, _)| *k).collect();
1202 let first_epoch_field = inner.first_epoch;
1203
1204 for epoch in affected {
1205 let Some(existing) = inner.snapshots.get(&epoch) else {
1206 continue;
1207 };
1208 let new_snapshot = EpochSnapshot::new(
1209 epoch,
1210 first_epoch_field,
1211 existing.inner.committee.clone(),
1212 existing.inner.randomized.clone(),
1213 da_committee.clone(),
1214 );
1215 inner.snapshots.insert(epoch, new_snapshot);
1216 }
1217 }
1218}
1219
1220#[derive(Error, Debug)]
1221pub enum EpochCommitteesError {
1222 #[error("could not lookup leader")]
1223 LeaderLookupError,
1224
1225 #[error("block {0} is not the root block for an epoch")]
1226 NoRootBlock(BlockNumber),
1227
1228 #[error("fetcher error: {0}")]
1229 Fetcher(#[source] anyhow::Error),
1230
1231 #[error("{0}")]
1232 Message(String),
1233
1234 #[error("state catchup error: {0}")]
1235 Catchup(#[source] anyhow::Error),
1236
1237 #[error("reward calculation error: {0}")]
1238 Reward(#[source] anyhow::Error),
1239}
1240
1241impl Inner {
1242 fn resolve_da_committee(&self, epoch: Option<EpochNumber>) -> Arc<DaCommittee> {
1245 if let Some(e) = epoch {
1246 self.da_committees
1248 .range((Bound::Included(0.into()), Bound::Included(e)))
1249 .last()
1250 .map(|(_, committee)| committee.clone())
1251 .unwrap_or_else(|| self.genesis_da_committee.clone())
1252 } else {
1253 self.genesis_da_committee.clone()
1254 }
1255 }
1256
1257 fn epoch_committee(&self, e: EpochNumber) -> Option<&Arc<EpochCommittee>> {
1259 self.snapshots.get(&e).map(|s| &s.inner.committee)
1260 }
1261
1262 fn address(&self, e: EpochNumber, key: &BLSPubKey) -> anyhow::Result<Address> {
1263 self.epoch_committee(e)
1264 .context("state for found")?
1265 .address_mapping
1266 .get(key)
1267 .copied()
1268 .context(format!(
1269 "failed to get ethereum address for bls key {key}. epoch={e}"
1270 ))
1271 }
1272
1273 fn active_validators(&self, e: EpochNumber) -> anyhow::Result<AuthenticatedValidatorMap> {
1274 Ok(self
1275 .epoch_committee(e)
1276 .context("state not found")?
1277 .validators
1278 .clone())
1279 }
1280
1281 fn put_epoch_committee(&mut self, epoch: EpochNumber, committee: Arc<EpochCommittee>) {
1284 let randomized = self
1285 .snapshots
1286 .get(&epoch)
1287 .and_then(|s| s.inner.randomized.clone());
1288 let da_committee = self.resolve_da_committee(Some(epoch));
1289 let first_epoch = self.first_epoch;
1290 self.snapshots.insert(
1291 epoch,
1292 EpochSnapshot::new(epoch, first_epoch, committee, randomized, da_committee),
1293 );
1294 }
1295
1296 fn put_randomized_committee(
1300 &mut self,
1301 epoch: EpochNumber,
1302 randomized: Arc<RandomizedCommittee<StakeTableEntry<PubKey>>>,
1303 ) {
1304 let Some(existing) = self.snapshots.get(&epoch).cloned() else {
1305 return;
1306 };
1307 let committee = existing.inner.committee.clone();
1308 let da_committee = existing.inner.da_committee.clone();
1309 let first_epoch = self.first_epoch;
1310 self.snapshots.insert(
1311 epoch,
1312 EpochSnapshot::new(
1313 epoch,
1314 first_epoch,
1315 committee,
1316 Some(randomized),
1317 da_committee,
1318 ),
1319 );
1320 }
1321
1322 #[cfg_attr(not(feature = "node"), allow(dead_code))]
1334 fn prune_epochs(&mut self, added: EpochNumber) {
1335 let cutoff = EpochNumber::new(added.saturating_sub(RECENT_STAKE_TABLES_LIMIT));
1336 if cutoff <= self.prune_cutoff {
1337 return;
1338 }
1339 self.prune_cutoff = cutoff;
1340 let keep = self.first_epoch + 1u64;
1343 self.snapshots.retain(|e, _| *e <= keep || *e >= cutoff);
1344 }
1345}
1346
1347#[derive(Clone, Debug)]
1351pub struct EpochSnapshot {
1352 inner: Arc<EpochSnapshotInner>,
1353}
1354
1355#[derive(Debug)]
1356struct EpochSnapshotInner {
1357 epoch: EpochNumber,
1358 first_epoch: EpochNumber,
1359 committee: Arc<EpochCommittee>,
1360 randomized: Option<Arc<RandomizedCommittee<StakeTableEntry<PubKey>>>>,
1361 da_committee: Arc<DaCommittee>,
1362}
1363
1364impl EpochSnapshot {
1365 fn new(
1366 epoch: EpochNumber,
1367 first_epoch: EpochNumber,
1368 committee: Arc<EpochCommittee>,
1369 randomized: Option<Arc<RandomizedCommittee<StakeTableEntry<PubKey>>>>,
1370 da_committee: Arc<DaCommittee>,
1371 ) -> Self {
1372 Self {
1373 inner: Arc::new(EpochSnapshotInner {
1374 epoch,
1375 first_epoch,
1376 committee,
1377 randomized,
1378 da_committee,
1379 }),
1380 }
1381 }
1382}
1383
1384impl EpochSnapshot {
1385 pub fn validator_index(&self, key: &PubKey) -> Option<usize> {
1387 self.inner.committee.stake_table.get_index_of(key)
1388 }
1389
1390 pub fn validator_config(
1392 &self,
1393 key: &BLSPubKey,
1394 ) -> anyhow::Result<&AuthenticatedValidator<BLSPubKey>> {
1395 let address = self
1396 .inner
1397 .committee
1398 .address_mapping
1399 .get(key)
1400 .context(format!(
1401 "failed to get ethereum address for bls key {key}. epoch={}",
1402 self.inner.epoch
1403 ))?;
1404 self.inner
1405 .committee
1406 .validators
1407 .get(address)
1408 .context("validator not found")
1409 }
1410
1411 pub fn epoch_block_reward(&self) -> Option<RewardAmount> {
1412 self.inner.committee.block_reward
1413 }
1414
1415 pub fn validators(&self) -> &AuthenticatedValidatorMap {
1416 &self.inner.committee.validators
1417 }
1418}
1419
1420impl MembershipSnapshot<SeqTypes> for EpochSnapshot {
1421 type Error = EpochCommitteesError;
1422 type StakeTableHash = StakeTableState;
1423
1424 fn epoch(&self) -> EpochNumber {
1425 self.inner.epoch
1426 }
1427
1428 fn first_epoch(&self) -> EpochNumber {
1429 self.inner.first_epoch
1430 }
1431
1432 fn has_drb(&self) -> bool {
1433 self.inner.randomized.is_some()
1434 }
1435
1436 fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<SeqTypes>> + Send {
1437 self.inner.committee.stake_table.values()
1438 }
1439
1440 fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<SeqTypes>> + Send {
1441 self.inner.da_committee.committee.iter()
1442 }
1443
1444 fn committee_members(&self, _: ViewNumber) -> impl ExactSizeIterator<Item = &PubKey> + Send {
1445 self.inner.committee.stake_table.keys()
1446 }
1447
1448 fn da_committee_members(&self, _: ViewNumber) -> impl ExactSizeIterator<Item = &PubKey> + Send {
1449 self.inner.da_committee.indexed_committee.keys()
1450 }
1451
1452 fn stake(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
1453 self.inner.committee.stake_table.get(key).cloned()
1454 }
1455
1456 fn da_stake(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
1457 self.inner.da_committee.indexed_committee.get(key).cloned()
1458 }
1459
1460 fn has_stake(&self, key: &PubKey) -> bool {
1461 self.stake(key)
1462 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
1463 .unwrap_or_default()
1464 }
1465
1466 fn has_da_stake(&self, key: &PubKey) -> bool {
1467 self.da_stake(key)
1468 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
1469 .unwrap_or_default()
1470 }
1471
1472 fn lookup_leader(&self, view: ViewNumber) -> Result<PubKey, Self::Error> {
1480 let inner = &self.inner;
1481 let first_epoch = inner.first_epoch;
1482 if inner.epoch < first_epoch {
1483 error!(
1484 "leader requested for epoch {} before first epoch {first_epoch}",
1485 inner.epoch,
1486 );
1487 return Err(EpochCommitteesError::LeaderLookupError);
1488 }
1489 let Some(rand) = inner.randomized.as_deref() else {
1490 error!(
1491 "missing randomized committee for epoch {} in snapshot",
1492 inner.epoch,
1493 );
1494 return Err(EpochCommitteesError::LeaderLookupError);
1495 };
1496 if inner.committee.eligible_leaders.is_empty() {
1499 error!("no eligible leader for epoch {}", inner.epoch);
1500 return Err(EpochCommitteesError::LeaderLookupError);
1501 }
1502 Ok(PubKey::public_key(&select_randomized_leader(rand, *view)))
1503 }
1504
1505 fn stake_table_hash(&self) -> Option<Commitment<Self::StakeTableHash>> {
1506 self.inner.committee.stake_table_hash
1507 }
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512 use std::sync::{
1513 Arc,
1514 atomic::{AtomicBool, AtomicUsize, Ordering},
1515 };
1516
1517 use committable::Committable;
1518 use hotshot_query_service::testing::mocks::MOCK_UPGRADE;
1519 use hotshot_types::{
1520 ValidatorConfig,
1521 traits::{BlockPayload, block_contents::BlockHeader},
1522 };
1523 use rstest::rstest;
1524 use tokio::{task::JoinSet, time::Duration};
1525
1526 use super::*;
1527 use crate::{NodeState, Payload, Transaction, traits::MembershipPersistence};
1528
1529 const TEST_DURATION: Duration = Duration::from_secs(5);
1533
1534 fn build_committees(num_peers: u64) -> EpochCommittees {
1535 build_committees_with_fetcher(num_peers, Fetcher::mock(), EpochNumber::genesis())
1536 }
1537
1538 fn build_committees_with_fetcher(
1539 num_peers: u64,
1540 fetcher: Fetcher,
1541 first_epoch: EpochNumber,
1542 ) -> EpochCommittees {
1543 let peers: Vec<PeerConfig<SeqTypes>> = (0..num_peers)
1544 .map(|i| {
1545 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
1546 [42u8; 32],
1547 i,
1548 U256::from(100),
1549 true,
1550 )
1551 .public_config()
1552 })
1553 .collect();
1554 EpochCommittees::new(peers.clone(), peers, None, fetcher, 100u64, first_epoch)
1555 }
1556
1557 #[derive(Debug)]
1563 struct MockStakeStore {
1564 header: Header,
1565 stored_headers: std::sync::Mutex<HashMap<EpochNumber, Header>>,
1566 fail_next_store: Arc<AtomicBool>,
1567 }
1568
1569 #[async_trait::async_trait]
1570 impl crate::traits::MembershipPersistence for MockStakeStore {
1571 async fn load_stake(
1572 &self,
1573 epoch: EpochNumber,
1574 ) -> anyhow::Result<Option<crate::traits::StakeTuple>> {
1575 Ok((*epoch == 7 || *epoch == 8).then(|| (Default::default(), None, None)))
1576 }
1577
1578 async fn load_latest_stake(
1579 &self,
1580 _limit: u64,
1581 ) -> anyhow::Result<Option<Vec<crate::v0_3::IndexedStake>>> {
1582 Ok(None)
1583 }
1584
1585 async fn load_drb_result(&self, epoch: EpochNumber) -> anyhow::Result<Option<DrbResult>> {
1586 Ok((*epoch == 7).then_some([7; 32]))
1587 }
1588
1589 async fn load_epoch_root(&self, epoch: EpochNumber) -> anyhow::Result<Option<Header>> {
1590 if let Some(header) = self.stored_headers.lock().unwrap().get(&epoch) {
1591 return Ok(Some(header.clone()));
1592 }
1593 Ok((*epoch == 7).then(|| self.header.clone()))
1594 }
1595
1596 async fn store_epoch_root(
1597 &self,
1598 epoch: EpochNumber,
1599 block_header: Header,
1600 ) -> anyhow::Result<()> {
1601 if self.fail_next_store.swap(false, Ordering::SeqCst) {
1602 anyhow::bail!("injected store_epoch_root failure");
1603 }
1604 self.stored_headers
1605 .lock()
1606 .unwrap()
1607 .insert(epoch, block_header);
1608 Ok(())
1609 }
1610
1611 async fn store_stake(
1612 &self,
1613 _epoch: EpochNumber,
1614 _stake: AuthenticatedValidatorMap,
1615 _block_reward: Option<RewardAmount>,
1616 _stake_table_hash: Option<StakeTableHash>,
1617 ) -> anyhow::Result<()> {
1618 Ok(())
1619 }
1620
1621 async fn store_events(
1622 &self,
1623 _l1_finalized: u64,
1624 _events: Vec<(crate::v0_3::EventKey, crate::v0_3::StakeTableEvent)>,
1625 ) -> anyhow::Result<()> {
1626 Ok(())
1627 }
1628
1629 async fn load_events(
1630 &self,
1631 _from_l1_block: u64,
1632 _l1_finalized: u64,
1633 ) -> anyhow::Result<(
1634 Option<crate::traits::EventsPersistenceRead>,
1635 Vec<(crate::v0_3::EventKey, crate::v0_3::StakeTableEvent)>,
1636 )> {
1637 Ok((None, vec![]))
1638 }
1639
1640 async fn delete_stake_tables(&self) -> anyhow::Result<()> {
1641 Ok(())
1642 }
1643
1644 async fn store_all_validators(
1645 &self,
1646 _epoch: EpochNumber,
1647 _all_validators: IndexMap<Address, crate::v0_3::RegisteredValidator<PubKey>>,
1648 ) -> anyhow::Result<()> {
1649 Ok(())
1650 }
1651
1652 async fn load_all_validators(
1653 &self,
1654 _epoch: EpochNumber,
1655 _offset: u64,
1656 _limit: u64,
1657 ) -> anyhow::Result<Vec<crate::v0_3::RegisteredValidator<PubKey>>> {
1658 Ok(vec![])
1659 }
1660 }
1661
1662 #[tokio::test]
1667 async fn restore_stake_table_from_persistence() {
1668 let store = MockStakeStore {
1669 header: build_epoch_root_header().await,
1670 stored_headers: Default::default(),
1671 fail_next_store: Default::default(),
1672 };
1673 let fetcher = Fetcher::new(
1674 Arc::new(crate::mock::MockStateCatchup::default()),
1675 Arc::new(AsyncMutex::new(store)),
1676 crate::L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
1677 .expect("Failed to create L1 client"),
1678 crate::ChainConfig::default(),
1679 );
1680 let committees = build_committees_with_fetcher(4, fetcher, EpochNumber::genesis());
1681
1682 let seven = EpochNumber::new(7);
1684 assert!(committees.snapshot(seven).is_none());
1685 assert!(committees.load_stake_table(seven).await);
1686 let snapshot = committees.snapshot(seven).unwrap();
1687 assert!(snapshot.has_drb());
1688 assert!(snapshot.inner.committee.header.is_some());
1689 assert!(committees.load_stake_table(seven).await);
1691 let eight = EpochNumber::new(8);
1693 assert!(committees.load_stake_table(eight).await);
1694 let snapshot = committees.snapshot(eight).unwrap();
1695 assert!(!snapshot.has_drb());
1696 assert!(snapshot.inner.committee.header.is_none());
1697 assert!(!committees.load_stake_table(EpochNumber::new(9)).await);
1699 }
1700
1701 #[tokio::test]
1708 async fn won_race_reported_loaded_when_lock_phase_times_out() {
1709 let store = MockStakeStore {
1710 header: build_epoch_root_header().await,
1711 stored_headers: Default::default(),
1712 fail_next_store: Default::default(),
1713 };
1714 let persistence = Arc::new(AsyncMutex::new(store));
1715 let fetcher = Fetcher::new(
1716 Arc::new(crate::mock::MockStateCatchup::default()),
1717 Arc::clone(&persistence) as Arc<AsyncMutex<dyn MembershipPersistence>>,
1718 crate::L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
1719 .expect("Failed to create L1 client"),
1720 crate::ChainConfig::default(),
1721 );
1722 let committees = build_committees_with_fetcher(4, fetcher, EpochNumber::genesis())
1723 .with_storage_read_timeout(Duration::from_millis(400));
1724
1725 let _busy = persistence.lock().await;
1728
1729 let seven = EpochNumber::new(7);
1732 let loader = tokio::spawn({
1733 let committees = committees.clone();
1734 async move { committees.load_stake_table(seven).await }
1735 });
1736 tokio::time::sleep(Duration::from_millis(100)).await;
1739
1740 {
1743 let mut inner = committees.inner.write();
1744 let template = inner
1745 .epoch_committee(EpochNumber::genesis())
1746 .expect("genesis committee exists")
1747 .clone();
1748 inner.put_epoch_committee(seven, template);
1749 }
1750
1751 let loaded = loader.await.expect("loader panicked");
1752 assert!(
1753 loaded,
1754 "load_stake_table returned false for an epoch whose snapshot was loaded while it \
1755 queued for the persistence lock: catchup would re-fetch from peers what is already \
1756 in memory"
1757 );
1758 }
1759
1760 #[test]
1764 fn prune_epochs_keeps_recent_window() {
1765 let committees = build_committees(4);
1766 let mut inner = committees.inner.write();
1767 inner.first_epoch = EpochNumber::new(1);
1768 let template = inner
1769 .epoch_committee(EpochNumber::genesis())
1770 .expect("genesis committee exists")
1771 .clone();
1772
1773 for e in 2..=50 {
1774 inner.put_epoch_committee(EpochNumber::new(e), template.clone());
1775 inner.prune_epochs(EpochNumber::new(e));
1776 }
1777
1778 let cutoff = EpochNumber::new(50 - RECENT_STAKE_TABLES_LIMIT);
1781 let expected: Vec<EpochNumber> = [1, 2]
1782 .into_iter()
1783 .chain(*cutoff..=50)
1784 .map(EpochNumber::new)
1785 .collect();
1786 assert_eq!(
1787 inner.snapshots.keys().copied().collect::<Vec<_>>(),
1788 expected
1789 );
1790
1791 inner.put_epoch_committee(EpochNumber::new(5), template);
1794 inner.prune_epochs(EpochNumber::new(5));
1795 assert!(inner.snapshots.contains_key(&EpochNumber::new(5)));
1796 inner.prune_epochs(EpochNumber::new(51));
1797 assert!(!inner.snapshots.contains_key(&EpochNumber::new(5)));
1798 }
1799
1800 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1814 async fn concurrent_reads_during_mutations() {
1815 let committees = build_committees(8);
1816
1817 {
1822 let mut inner = committees.inner.write();
1823 let template = inner
1824 .epoch_committee(EpochNumber::genesis())
1825 .expect("genesis committee exists")
1826 .clone();
1827 for e in 2..6 {
1828 inner.put_epoch_committee(EpochNumber::new(e), template.clone());
1829 }
1830 }
1831
1832 let stop = Arc::new(AtomicBool::new(false));
1833 let mut tasks = JoinSet::new();
1834
1835 for _ in 0..8 {
1836 let c = committees.clone();
1837 let stop = Arc::clone(&stop);
1838 tasks.spawn(async move {
1839 let stable = EpochNumber::new(1);
1840 let mutating = EpochNumber::new(3);
1841 let view = ViewNumber::new(0);
1842 while !stop.load(Ordering::Relaxed) {
1843 let stable_snap = c.snapshot(stable).expect("stable snapshot");
1849 let len = stable_snap.stake_table().len();
1850 assert_eq!(len, stable_snap.total_nodes());
1851 let leader = stable_snap.lookup_leader(view).expect("leader");
1852 assert!(
1853 stable_snap.committee_members(view).any(|p| p == &leader),
1854 "leader {leader:?} not in committee_members for stable epoch",
1855 );
1856 assert_eq!(c.first_epoch(), stable);
1857
1858 let _ = c.snapshot(mutating);
1864 if let Some(s) = c.snapshot(mutating) {
1865 let _ = s.lookup_leader(view);
1866 }
1867 tokio::task::yield_now().await;
1868 }
1869 });
1870 }
1871
1872 tasks.spawn({
1876 let c = committees.clone();
1877 let stop = Arc::clone(&stop);
1878 async move {
1879 let extra: Vec<PeerConfig<SeqTypes>> = (0..3)
1880 .map(|i| {
1881 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
1882 [99u8; 32],
1883 i,
1884 U256::from(50),
1885 true,
1886 )
1887 .public_config()
1888 })
1889 .collect();
1890 let mut i: u64 = 0;
1891 while !stop.load(Ordering::Relaxed) {
1892 c.add_drb_result(EpochNumber::new(2 + (i % 4)), [(i % 256) as u8; 32]);
1895 c.add_drb_result(EpochNumber::new(10_000 + i), [0xAB; 32]);
1898 if i.is_multiple_of(50) {
1899 c.add_da_committee(i.into(), extra.clone());
1900 }
1901 if i.is_multiple_of(16) {
1902 tokio::task::yield_now().await;
1903 }
1904 i += 1;
1905 }
1906 }
1907 });
1908
1909 tokio::time::sleep(TEST_DURATION).await;
1910 stop.store(true, Ordering::Relaxed);
1911 while let Some(res) = tasks.join_next().await {
1912 res.expect("task panicked");
1913 }
1914 }
1915
1916 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1922 async fn concurrent_add_drb_result_same_epoch() {
1923 let committees = build_committees(4);
1924 let epoch = EpochNumber::new(1);
1925
1926 let stop = Arc::new(AtomicBool::new(false));
1927 let writes = Arc::new(AtomicUsize::new(0));
1928 let lookups = Arc::new(AtomicUsize::new(0));
1929
1930 let mut writers = JoinSet::new();
1931 let mut readers = JoinSet::new();
1932
1933 for _ in 0..4 {
1937 let c = committees.clone();
1938 let stop = Arc::clone(&stop);
1939 let lookups = Arc::clone(&lookups);
1940 readers.spawn(async move {
1941 let view = ViewNumber::new(0);
1942 while !stop.load(Ordering::Relaxed) {
1943 c.snapshot(epoch)
1944 .expect("snapshot")
1945 .lookup_leader(view)
1946 .expect("randomized committee must remain present once set");
1947 lookups.fetch_add(1, Ordering::Relaxed);
1948 tokio::task::yield_now().await;
1949 }
1950 });
1951 }
1952
1953 for tid in 0..8u8 {
1957 let c = committees.clone();
1958 let stop = Arc::clone(&stop);
1959 let writes = Arc::clone(&writes);
1960 writers.spawn(async move {
1961 let mut i: u64 = 0;
1962 while !stop.load(Ordering::Relaxed) {
1963 let mut drb = [tid; 32];
1964 drb[0] = (i & 0xFF) as u8;
1965 c.add_drb_result(epoch, drb);
1966 writes.fetch_add(1, Ordering::Relaxed);
1967 if i.is_multiple_of(16) {
1968 tokio::task::yield_now().await;
1969 }
1970 i += 1;
1971 }
1972 });
1973 }
1974
1975 tokio::time::sleep(TEST_DURATION).await;
1976 stop.store(true, Ordering::Relaxed);
1977 while let Some(res) = writers.join_next().await {
1978 res.expect("writer panicked");
1979 }
1980 while let Some(res) = readers.join_next().await {
1981 res.expect("reader panicked");
1982 }
1983
1984 assert!(writes.load(Ordering::Relaxed) > 0, "writers never advanced",);
1985 assert!(
1986 lookups.load(Ordering::Relaxed) > 0,
1987 "readers never observed the randomized committee",
1988 );
1989 let snap = committees
1990 .snapshot(epoch)
1991 .expect("randomized committee must survive concurrent writes");
1992 assert!(snap.has_drb(), "randomized committee must remain present");
1993 let view = ViewNumber::new(0);
1994 let _leader = snap
1995 .lookup_leader(view)
1996 .expect("lookup_leader succeeds when randomized committee is present");
1997 }
1998
1999 async fn build_epoch_root_header() -> Header {
2003 let instance = NodeState::mock_v2();
2004 let tx = Transaction::of_size(10);
2005 let (payload, _) = Payload::from_transactions(&[tx], &instance.genesis_state, &instance)
2006 .await
2007 .expect("payload");
2008 let metadata = payload.ns_table().clone();
2009 let header = Header::genesis(&instance, payload, &metadata, MOCK_UPGRADE.base);
2010 match header {
2011 Header::V2(mut h) => {
2012 h.height = 95;
2013 Header::V2(h)
2014 },
2015 other => panic!("expected V2 header from NodeState::mock_v2, got {other:?}"),
2016 }
2017 }
2018
2019 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2033 async fn add_epoch_root_is_atomic() {
2034 let header = build_epoch_root_header().await;
2035 let target = EpochNumber::new(3);
2036
2037 fn snapshot(c: &EpochCommittees, e: EpochNumber) -> (bool, bool) {
2038 let inner = c.inner.read();
2039 let header_set = inner
2040 .snapshots
2041 .get(&e)
2042 .map(|s| s.inner.committee.header.is_some())
2043 .unwrap_or(false);
2044 let all_validators_present = inner.all_validators.contains_key(&e);
2045 (header_set, all_validators_present)
2046 }
2047
2048 let test_start = tokio::time::Instant::now();
2052 let mut round: u64 = 0;
2053 while test_start.elapsed() < TEST_DURATION {
2054 let committees = build_committees(4);
2055
2056 {
2061 let mut inner = committees.inner.write();
2062 let template = inner
2063 .epoch_committee(EpochNumber::genesis())
2064 .expect("genesis committee exists");
2065 let prefilled = EpochCommittee {
2066 block_reward: Some(RewardAmount::default()),
2067 stake_table_hash: Some(StakeTableState::default().commit()),
2068 header: None,
2069 eligible_leaders: template.eligible_leaders.clone(),
2070 stake_table: template.stake_table.clone(),
2071 validators: template.validators.clone(),
2072 address_mapping: template.address_mapping.clone(),
2073 };
2074 inner.put_epoch_committee(target, Arc::new(prefilled));
2075 }
2076
2077 let stop = Arc::new(AtomicBool::new(false));
2078 let post = Arc::new(AtomicUsize::new(0));
2079
2080 let reader = {
2081 let c = committees.clone();
2082 let stop = Arc::clone(&stop);
2083 let post = Arc::clone(&post);
2084 tokio::spawn(async move {
2085 while !stop.load(Ordering::Relaxed) {
2086 match snapshot(&c, target) {
2087 (false, false) => {}, (true, true) => {
2089 post.fetch_add(1, Ordering::Relaxed);
2090 },
2091 torn => panic!(
2092 "round {round}: torn snapshot for epoch {target}: header_set={}, \
2093 all_validators_present={}",
2094 torn.0, torn.1,
2095 ),
2096 }
2097 tokio::task::yield_now().await;
2098 }
2099 })
2100 };
2101
2102 tokio::time::sleep(Duration::from_millis(2)).await;
2105 let coordinator = EpochMembershipCoordinator::<SeqTypes>::new(
2106 committees.clone(),
2107 *committees.epoch_height(),
2108 &hotshot_example_types::storage_types::TestStorage::default(),
2109 );
2110 coordinator
2111 .add_epoch_root(header.clone())
2112 .await
2113 .expect("add_epoch_root should succeed for the prefilled state");
2114
2115 let deadline = tokio::time::Instant::now() + Duration::from_millis(200);
2116 while tokio::time::Instant::now() < deadline && post.load(Ordering::Relaxed) == 0 {
2117 tokio::task::yield_now().await;
2118 }
2119 stop.store(true, Ordering::Relaxed);
2120 reader.await.expect("reader panicked");
2121 assert!(
2122 post.load(Ordering::Relaxed) > 0,
2123 "round {round}: reader never observed post-state",
2124 );
2125 round += 1;
2126 }
2127 assert!(round > 0, "test loop never executed a round");
2128 }
2129
2130 fn epoch_root_fixture(
2135 header: &Header,
2136 cached_header: bool,
2137 ) -> (
2138 EpochCommittees,
2139 EpochMembershipCoordinator<SeqTypes>,
2140 EpochNumber,
2141 Arc<AtomicBool>,
2142 ) {
2143 let fail_next_store = Arc::<AtomicBool>::default();
2144 let store = MockStakeStore {
2145 header: header.clone(),
2146 stored_headers: Default::default(),
2147 fail_next_store: fail_next_store.clone(),
2148 };
2149 let fetcher = Fetcher::new(
2150 Arc::new(crate::mock::MockStateCatchup::default()),
2151 Arc::new(AsyncMutex::new(store)),
2152 crate::L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
2153 .expect("Failed to create L1 client"),
2154 crate::ChainConfig::default(),
2155 );
2156 let committees = build_committees_with_fetcher(4, fetcher, EpochNumber::genesis());
2157 let epoch_height = *committees.epoch_height();
2158 let epoch = EpochNumber::new(epoch_from_block_number(header.height(), epoch_height) + 2);
2159
2160 {
2161 let mut inner = committees.inner.write();
2162 let template = inner
2163 .epoch_committee(EpochNumber::genesis())
2164 .expect("genesis committee exists");
2165 let prefilled = EpochCommittee {
2166 block_reward: Some(RewardAmount::default()),
2167 stake_table_hash: Some(StakeTableState::default().commit()),
2168 header: cached_header.then(|| header.clone()),
2169 eligible_leaders: template.eligible_leaders.clone(),
2170 stake_table: template.stake_table.clone(),
2171 validators: template.validators.clone(),
2172 address_mapping: template.address_mapping.clone(),
2173 };
2174 inner.put_epoch_committee(epoch, Arc::new(prefilled));
2175 }
2176
2177 let coordinator = EpochMembershipCoordinator::<SeqTypes>::new(
2178 committees.clone(),
2179 epoch_height,
2180 &hotshot_example_types::storage_types::TestStorage::default(),
2181 );
2182 (committees, coordinator, epoch, fail_next_store)
2183 }
2184
2185 #[rstest]
2192 #[case::incomplete_committee(false)]
2193 #[case::complete_committee(true)]
2194 #[tokio::test]
2195 async fn add_epoch_root_persists_epoch_root_header(#[case] cached_header: bool) {
2196 let header = build_epoch_root_header().await;
2197 let (committees, coordinator, epoch, _) = epoch_root_fixture(&header, cached_header);
2198
2199 coordinator
2200 .add_epoch_root(header.clone())
2201 .await
2202 .expect("add_epoch_root should succeed for the prefilled state");
2203
2204 let persistence = committees.fetcher.persistence.lock().await;
2205 assert_eq!(
2206 persistence.load_epoch_root(epoch).await.unwrap(),
2207 Some(header)
2208 );
2209 }
2210
2211 #[tokio::test]
2216 async fn add_epoch_root_retries_a_failed_store() {
2217 let header = build_epoch_root_header().await;
2218 let (committees, coordinator, epoch, fail_next_store) = epoch_root_fixture(&header, false);
2219
2220 fail_next_store.store(true, Ordering::SeqCst);
2221
2222 coordinator
2223 .add_epoch_root(header.clone())
2224 .await
2225 .expect("a dropped store must not fail `add_epoch_root`");
2226 assert_eq!(
2227 committees
2228 .fetcher
2229 .persistence
2230 .lock()
2231 .await
2232 .load_epoch_root(epoch)
2233 .await
2234 .unwrap(),
2235 None,
2236 "the injected failure should have dropped the write"
2237 );
2238
2239 coordinator
2240 .add_epoch_root(header.clone())
2241 .await
2242 .expect("add_epoch_root should succeed for the complete committee");
2243 assert_eq!(
2244 committees
2245 .fetcher
2246 .persistence
2247 .lock()
2248 .await
2249 .load_epoch_root(epoch)
2250 .await
2251 .unwrap(),
2252 Some(header)
2253 );
2254 }
2255
2256 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2262 async fn add_da_committee_rebuilds_affected_snapshots() {
2263 fn da_keys(c: &EpochCommittees, e: EpochNumber) -> Vec<PubKey> {
2264 c.snapshot(e)
2265 .expect("snapshot")
2266 .da_stake_table()
2267 .map(|p| PubKey::public_key(&p.stake_table_entry))
2268 .collect()
2269 }
2270
2271 let committees = build_committees_with_fetcher(4, Fetcher::mock(), EpochNumber::new(2));
2275
2276 let initial_e2 = da_keys(&committees, EpochNumber::new(2));
2277 let initial_e3 = da_keys(&committees, EpochNumber::new(3));
2278
2279 let new_da: Vec<PeerConfig<SeqTypes>> = (0..2)
2280 .map(|i| {
2281 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
2282 [123u8; 32],
2283 i,
2284 U256::from(50),
2285 true,
2286 )
2287 .public_config()
2288 })
2289 .collect();
2290 let new_da_keys: Vec<PubKey> = new_da
2291 .iter()
2292 .map(|p| PubKey::public_key(&p.stake_table_entry))
2293 .collect();
2294 assert_ne!(
2295 new_da_keys, initial_e2,
2296 "test setup: new DA must differ from initial"
2297 );
2298
2299 committees.add_da_committee(EpochNumber::new(2), new_da);
2304
2305 assert_eq!(
2306 da_keys(&committees, EpochNumber::new(2)),
2307 new_da_keys,
2308 "snapshot(2) must reflect the new DA",
2309 );
2310 assert_eq!(
2311 da_keys(&committees, EpochNumber::new(3)),
2312 new_da_keys,
2313 "snapshot(3) must reflect the new DA",
2314 );
2315 assert_eq!(
2316 da_keys(&committees, EpochNumber::new(1)),
2317 initial_e2,
2318 "snapshot(1) lies before first_epoch=2 and must keep the bootstrap DA",
2319 );
2320 assert_eq!(initial_e2, initial_e3);
2325 }
2326
2327 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2331 async fn add_da_committee_layered_does_not_rebuild_earlier_ranges() {
2332 fn da_keys(c: &EpochCommittees, e: EpochNumber) -> Vec<PubKey> {
2333 c.snapshot(e)
2334 .expect("snapshot")
2335 .da_stake_table()
2336 .map(|p| PubKey::public_key(&p.stake_table_entry))
2337 .collect()
2338 }
2339
2340 let committees = build_committees(4);
2341
2342 {
2346 let mut inner = committees.inner.write();
2347 let template = inner
2348 .epoch_committee(EpochNumber::genesis())
2349 .expect("genesis committee exists")
2350 .clone();
2351 for e in 3..6 {
2352 inner.put_epoch_committee(EpochNumber::new(e), template.clone());
2353 }
2354 }
2355
2356 let da_b: Vec<PeerConfig<SeqTypes>> = (0..2)
2357 .map(|i| {
2358 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
2359 [200u8; 32],
2360 i,
2361 U256::from(50),
2362 true,
2363 )
2364 .public_config()
2365 })
2366 .collect();
2367 let da_b_keys: Vec<PubKey> = da_b
2368 .iter()
2369 .map(|p| PubKey::public_key(&p.stake_table_entry))
2370 .collect();
2371
2372 let da_c: Vec<PeerConfig<SeqTypes>> = (0..2)
2373 .map(|i| {
2374 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
2375 [201u8; 32],
2376 i,
2377 U256::from(50),
2378 true,
2379 )
2380 .public_config()
2381 })
2382 .collect();
2383 let da_c_keys: Vec<PubKey> = da_c
2384 .iter()
2385 .map(|p| PubKey::public_key(&p.stake_table_entry))
2386 .collect();
2387
2388 committees.add_da_committee(EpochNumber::new(5), da_c);
2391 committees.add_da_committee(EpochNumber::new(3), da_b);
2392
2393 assert_eq!(da_keys(&committees, EpochNumber::new(3)), da_b_keys);
2394 assert_eq!(da_keys(&committees, EpochNumber::new(4)), da_b_keys);
2395 assert_eq!(
2396 da_keys(&committees, EpochNumber::new(5)),
2397 da_c_keys,
2398 "epoch 5 must keep C — it is outside B's range",
2399 );
2400 }
2401
2402 fn peer(seed: u8, index: u64, stake: u64) -> PeerConfig<SeqTypes> {
2403 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
2404 [seed; 32],
2405 index,
2406 U256::from(stake),
2407 true,
2408 )
2409 .public_config()
2410 }
2411
2412 fn genesis_members() -> (Vec<PeerConfig<SeqTypes>>, Vec<PeerConfig<SeqTypes>>) {
2416 let staked: Vec<_> = (0..4).map(|i| peer(42, i, 100)).collect();
2417 let mut all = staked.clone();
2418 all.insert(2, peer(42, 4, 0));
2419 (all, staked)
2420 }
2421
2422 fn committees_from_first_epoch(first_epoch: EpochNumber, fetcher: Fetcher) -> EpochCommittees {
2423 let (members, _) = genesis_members();
2424 EpochCommittees::new(members.clone(), members, None, fetcher, 100u64, first_epoch)
2425 }
2426
2427 fn assert_runs_on_committee(
2430 committees: &EpochCommittees,
2431 epoch: EpochNumber,
2432 staked: &[PeerConfig<SeqTypes>],
2433 ) {
2434 let snapshot = committees
2435 .snapshot(epoch)
2436 .unwrap_or_else(|| panic!("no snapshot for epoch {epoch}"));
2437
2438 assert_eq!(
2439 snapshot.stake_table().len(),
2440 staked.len(),
2441 "epoch {epoch}: stake table size"
2442 );
2443 for peer in staked {
2444 let key = PubKey::public_key(&peer.stake_table_entry);
2445 assert_eq!(
2446 snapshot.stake(&key).as_ref(),
2447 Some(peer),
2448 "epoch {epoch}: stake table entry of {key}"
2449 );
2450 }
2451
2452 assert!(snapshot.has_drb(), "epoch {epoch}: no DRB result");
2453
2454 let keys: std::collections::HashSet<PubKey> = staked
2455 .iter()
2456 .map(|p| PubKey::public_key(&p.stake_table_entry))
2457 .collect();
2458 for view in 0..64 {
2459 let leader = snapshot
2460 .lookup_leader(ViewNumber::new(view))
2461 .unwrap_or_else(|err| panic!("epoch {epoch}, view {view}: no leader: {err}"));
2462 assert!(
2463 keys.contains(&leader),
2464 "epoch {epoch}, view {view}: leader {leader} is not in the committee"
2465 );
2466 }
2467 }
2468
2469 #[rstest]
2473 #[case::genesis(1)]
2474 #[case::later(5)]
2475 #[case::mainnet(275)]
2476 fn new_seeds_first_two_epochs_with_genesis_committee(#[case] first_epoch: u64) {
2477 let first_epoch = EpochNumber::new(first_epoch);
2478 let (_, staked) = genesis_members();
2479 let committees = committees_from_first_epoch(first_epoch, Fetcher::mock());
2480
2481 assert_runs_on_committee(&committees, first_epoch, &staked);
2482 assert_runs_on_committee(&committees, first_epoch + 1, &staked);
2483 }
2484
2485 #[rstest]
2488 #[case::genesis(1)]
2489 #[case::later(5)]
2490 fn add_da_committee_keeps_leaders_of_first_two_epochs(#[case] first_epoch: u64) {
2491 let first_epoch = EpochNumber::new(first_epoch);
2492 let (_, staked) = genesis_members();
2493 let committees = committees_from_first_epoch(first_epoch, Fetcher::mock());
2494
2495 let other_da: Vec<_> = (0..2).map(|i| peer(123, i, 50)).collect();
2496 committees.add_da_committee(first_epoch, other_da.clone());
2497
2498 for epoch in [first_epoch, first_epoch + 1] {
2499 assert_runs_on_committee(&committees, epoch, &staked);
2500 let da: Vec<_> = committees
2501 .snapshot(epoch)
2502 .expect("snapshot")
2503 .da_stake_table()
2504 .cloned()
2505 .collect();
2506 assert_eq!(da, other_da, "epoch {epoch}: DA committee");
2507 }
2508 }
2509
2510 fn unstaked_members(count: u64) -> Vec<PeerConfig<SeqTypes>> {
2512 (0..count).map(|i| peer(42, i, 0)).collect()
2513 }
2514
2515 #[rstest]
2518 #[case::empty_from_genesis(0, EpochNumber::genesis())]
2519 #[case::empty_from_later(0, EpochNumber::new(5))]
2520 #[case::zero_stake_from_genesis(3, EpochNumber::genesis())]
2521 #[case::zero_stake_from_later(3, EpochNumber::new(5))]
2522 fn lookup_leader_errors_without_staked_members(
2523 #[case] members: u64,
2524 #[case] first_epoch: EpochNumber,
2525 ) {
2526 let members = unstaked_members(members);
2527 let committees = EpochCommittees::new(
2528 members.clone(),
2529 members,
2530 None,
2531 Fetcher::mock(),
2532 100u64,
2533 first_epoch,
2534 );
2535
2536 for epoch in [first_epoch, first_epoch + 1] {
2537 let snapshot = committees
2538 .snapshot(epoch)
2539 .unwrap_or_else(|| panic!("no snapshot for epoch {epoch}"));
2540 for view in [0, 1, 7, 1000] {
2541 let result = snapshot.lookup_leader(ViewNumber::new(view));
2542 assert!(
2543 matches!(result, Err(EpochCommitteesError::LeaderLookupError)),
2544 "epoch {epoch}, view {view}: expected LeaderLookupError, got {result:?}"
2545 );
2546 }
2547 }
2548 }
2549
2550 #[tokio::test]
2552 async fn mock_coordinator_leader_errors_without_staked_members() {
2553 let coordinator = NodeState::mock().coordinator;
2554 let first_epoch = coordinator.membership().first_epoch();
2555
2556 for epoch in [first_epoch, first_epoch + 1] {
2557 let membership = coordinator
2558 .membership_for_epoch(Some(epoch))
2559 .unwrap_or_else(|err| panic!("epoch {epoch} is not seeded: {err:#}"));
2560 for view in [0, 1, 7, 1000] {
2561 let result = membership.leader(ViewNumber::new(view));
2562 assert!(
2563 result.is_err(),
2564 "epoch {epoch}, view {view}: expected an error, got {result:?}"
2565 );
2566 }
2567 }
2568 }
2569
2570 struct LatestStakeStore {
2573 stakes: Vec<crate::v0_3::IndexedStake>,
2574 }
2575
2576 #[async_trait::async_trait]
2577 impl crate::traits::MembershipPersistence for LatestStakeStore {
2578 async fn load_stake(
2579 &self,
2580 _epoch: EpochNumber,
2581 ) -> anyhow::Result<Option<crate::traits::StakeTuple>> {
2582 Ok(None)
2583 }
2584
2585 async fn load_latest_stake(
2586 &self,
2587 _limit: u64,
2588 ) -> anyhow::Result<Option<Vec<crate::v0_3::IndexedStake>>> {
2589 Ok(Some(self.stakes.clone()))
2590 }
2591
2592 async fn load_drb_result(&self, _epoch: EpochNumber) -> anyhow::Result<Option<DrbResult>> {
2593 Ok(None)
2594 }
2595
2596 async fn load_epoch_root(&self, _epoch: EpochNumber) -> anyhow::Result<Option<Header>> {
2597 Ok(None)
2598 }
2599
2600 async fn store_epoch_root(
2601 &self,
2602 _epoch: EpochNumber,
2603 _block_header: Header,
2604 ) -> anyhow::Result<()> {
2605 Ok(())
2606 }
2607
2608 async fn store_stake(
2609 &self,
2610 _epoch: EpochNumber,
2611 _stake: AuthenticatedValidatorMap,
2612 _block_reward: Option<RewardAmount>,
2613 _stake_table_hash: Option<StakeTableHash>,
2614 ) -> anyhow::Result<()> {
2615 Ok(())
2616 }
2617
2618 async fn store_events(
2619 &self,
2620 _l1_finalized: u64,
2621 _events: Vec<(crate::v0_3::EventKey, crate::v0_3::StakeTableEvent)>,
2622 ) -> anyhow::Result<()> {
2623 Ok(())
2624 }
2625
2626 async fn load_events(
2627 &self,
2628 _from_l1_block: u64,
2629 _l1_finalized: u64,
2630 ) -> anyhow::Result<(
2631 Option<crate::traits::EventsPersistenceRead>,
2632 Vec<(crate::v0_3::EventKey, crate::v0_3::StakeTableEvent)>,
2633 )> {
2634 Ok((None, vec![]))
2635 }
2636
2637 async fn delete_stake_tables(&self) -> anyhow::Result<()> {
2638 Ok(())
2639 }
2640
2641 async fn store_all_validators(
2642 &self,
2643 _epoch: EpochNumber,
2644 _all_validators: IndexMap<Address, crate::v0_3::RegisteredValidator<PubKey>>,
2645 ) -> anyhow::Result<()> {
2646 Ok(())
2647 }
2648
2649 async fn load_all_validators(
2650 &self,
2651 _epoch: EpochNumber,
2652 _offset: u64,
2653 _limit: u64,
2654 ) -> anyhow::Result<Vec<crate::v0_3::RegisteredValidator<PubKey>>> {
2655 Ok(vec![])
2656 }
2657 }
2658
2659 #[rstest]
2664 #[case::genesis(1)]
2665 #[case::later(5)]
2666 #[tokio::test]
2667 async fn reload_stake_keeps_genesis_committee_of_first_two_epochs(#[case] first_epoch: u64) {
2668 let first_epoch = EpochNumber::new(first_epoch);
2669 let (_, staked) = genesis_members();
2670
2671 let stored = AuthenticatedValidator::mock();
2672 let stored_key = *stored.stake_table_key();
2673 let stored_table: AuthenticatedValidatorMap =
2674 [(stored.account, stored)].into_iter().collect();
2675 let store = LatestStakeStore {
2676 stakes: [first_epoch, first_epoch + 1, first_epoch + 2]
2677 .into_iter()
2678 .map(|epoch| (epoch, (stored_table.clone(), None), None))
2679 .collect(),
2680 };
2681 let fetcher = Fetcher::new(
2682 Arc::new(crate::mock::MockStateCatchup::default()),
2683 Arc::new(AsyncMutex::new(store)),
2684 crate::L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
2685 .expect("Failed to create L1 client"),
2686 crate::ChainConfig::default(),
2687 );
2688 let mut committees = committees_from_first_epoch(first_epoch, fetcher);
2689
2690 committees.reload_stake(RECENT_STAKE_TABLES_LIMIT).await;
2691
2692 assert_runs_on_committee(&committees, first_epoch, &staked);
2693 assert_runs_on_committee(&committees, first_epoch + 1, &staked);
2694
2695 let later = first_epoch + 2;
2696 let loaded: Vec<PubKey> = committees
2697 .snapshot(later)
2698 .unwrap_or_else(|| panic!("stored stake table for epoch {later} was not loaded"))
2699 .stake_table()
2700 .map(|p| PubKey::public_key(&p.stake_table_entry))
2701 .collect();
2702 assert_eq!(loaded, vec![stored_key], "epoch {later}: stake table");
2703
2704 committees.add_drb_result(later, [7u8; 32]);
2705 let leader = committees
2706 .snapshot(later)
2707 .expect("snapshot")
2708 .lookup_leader(ViewNumber::new(0))
2709 .unwrap_or_else(|err| panic!("epoch {later}: no leader: {err}"));
2710 assert_eq!(leader, stored_key, "epoch {later}: leader");
2711 }
2712
2713 #[rstest]
2716 #[case::later(5)]
2717 #[case::mainnet(275)]
2718 #[tokio::test]
2719 async fn coordinator_serves_stake_tables_from_first_epoch(#[case] first_epoch: u64) {
2720 let before = EpochNumber::new(first_epoch - 1);
2721 let first_epoch = EpochNumber::new(first_epoch);
2722 let committees = committees_from_first_epoch(first_epoch, Fetcher::mock());
2723 let coordinator = EpochMembershipCoordinator::<SeqTypes>::new(
2724 committees,
2725 100u64,
2726 &hotshot_example_types::storage_types::TestStorage::default(),
2727 );
2728
2729 for epoch in [first_epoch, first_epoch + 1] {
2730 coordinator
2731 .stake_table_for_epoch(Some(epoch))
2732 .unwrap_or_else(|err| panic!("epoch {epoch}: no stake table: {err}"));
2733 coordinator
2734 .membership_for_epoch(Some(epoch))
2735 .unwrap_or_else(|err| panic!("epoch {epoch}: no randomized stake table: {err}"));
2736 }
2737 assert!(
2738 coordinator.stake_table_for_epoch(Some(before)).is_err(),
2739 "epoch {before} is before the first epoch {first_epoch}"
2740 );
2741 }
2742
2743 type SeqEpochMembership = hotshot_types::epoch_membership::EpochMembership<SeqTypes>;
2744
2745 fn coordinator_for(committees: EpochCommittees) -> EpochMembershipCoordinator<SeqTypes> {
2746 EpochMembershipCoordinator::<SeqTypes>::new(
2747 committees,
2748 100u64,
2749 &hotshot_example_types::storage_types::TestStorage::default(),
2750 )
2751 }
2752
2753 fn lookups(
2756 coordinator: &EpochMembershipCoordinator<SeqTypes>,
2757 epoch: Option<EpochNumber>,
2758 ) -> [(&'static str, SeqEpochMembership); 2] {
2759 [
2760 (
2761 "stake_table_for_epoch",
2762 coordinator.stake_table_for_epoch(epoch),
2763 ),
2764 (
2765 "membership_for_epoch",
2766 coordinator.membership_for_epoch(epoch),
2767 ),
2768 ]
2769 .map(|(lookup, result)| {
2770 let membership = result.unwrap_or_else(|err| panic!("{lookup}({epoch:?}): {err}"));
2771 (lookup, membership)
2772 })
2773 }
2774
2775 fn assert_none_reads_first_epoch(
2780 coordinator: &EpochMembershipCoordinator<SeqTypes>,
2781 first_epoch: EpochNumber,
2782 members: &[PeerConfig<SeqTypes>],
2783 ) {
2784 for ((lookup, none), (_, first)) in lookups(coordinator, None)
2785 .into_iter()
2786 .zip(lookups(coordinator, Some(first_epoch)))
2787 {
2788 let context = format!("{lookup}(None) vs {lookup}(Some({first_epoch}))");
2789 assert_same_reads(&context, &none, &first, members);
2790 }
2791 }
2792
2793 fn assert_same_reads(
2794 context: &str,
2795 none: &SeqEpochMembership,
2796 first: &SeqEpochMembership,
2797 members: &[PeerConfig<SeqTypes>],
2798 ) {
2799 let stake_table = |m: &SeqEpochMembership| m.stake_table().cloned().collect::<Vec<_>>();
2800 let committee_members = |m: &SeqEpochMembership| {
2801 m.committee_members(ViewNumber::new(0))
2802 .copied()
2803 .collect::<std::collections::HashSet<_>>()
2804 };
2805
2806 assert_eq!(
2807 stake_table(none),
2808 stake_table(first),
2809 "{context}: stake table"
2810 );
2811 assert_eq!(
2812 committee_members(none),
2813 committee_members(first),
2814 "{context}: committee members"
2815 );
2816 assert_eq!(
2817 none.total_nodes(),
2818 first.total_nodes(),
2819 "{context}: total nodes"
2820 );
2821 assert_eq!(
2822 none.success_threshold(),
2823 first.success_threshold(),
2824 "{context}: success threshold"
2825 );
2826
2827 let outsider = peer(99, 0, 100);
2828 for member in members.iter().chain(std::iter::once(&outsider)) {
2829 let key = PubKey::public_key(&member.stake_table_entry);
2830 assert_eq!(
2831 none.stake(&key),
2832 first.stake(&key),
2833 "{context}: stake({key})"
2834 );
2835 assert_eq!(
2836 none.has_stake(&key),
2837 first.has_stake(&key),
2838 "{context}: has_stake({key})"
2839 );
2840 }
2841 }
2842
2843 fn assert_da_committee(
2846 context: &str,
2847 membership: &SeqEpochMembership,
2848 expected: &[PeerConfig<SeqTypes>],
2849 ) {
2850 let da: Vec<_> = membership.da_stake_table().cloned().collect();
2851 assert_eq!(da, expected, "{context}: DA stake table");
2852 assert_eq!(
2853 membership.da_total_nodes(),
2854 expected.len(),
2855 "{context}: DA total nodes"
2856 );
2857
2858 let keys: std::collections::HashSet<PubKey> = expected
2859 .iter()
2860 .map(|p| PubKey::public_key(&p.stake_table_entry))
2861 .collect();
2862 let members: std::collections::HashSet<PubKey> = membership
2863 .da_committee_members(ViewNumber::new(0))
2864 .copied()
2865 .collect();
2866 assert_eq!(members, keys, "{context}: DA committee members");
2867
2868 for peer in expected {
2869 let key = PubKey::public_key(&peer.stake_table_entry);
2870 assert_eq!(
2871 membership.da_stake(&key).as_ref(),
2872 Some(peer),
2873 "{context}: da_stake({key})"
2874 );
2875 assert!(
2876 membership.has_da_stake(&key),
2877 "{context}: has_da_stake({key})"
2878 );
2879 }
2880 let outsider = PubKey::public_key(&peer(99, 0, 100).stake_table_entry);
2881 assert!(
2882 !membership.has_da_stake(&outsider),
2883 "{context}: has_da_stake of a key outside the DA committee"
2884 );
2885 }
2886
2887 #[rstest]
2890 #[case::genesis(1)]
2891 #[case::later(5)]
2892 #[case::mainnet(275)]
2893 #[tokio::test]
2894 async fn none_epoch_reads_first_epoch_stake_table(#[case] first_epoch: u64) {
2895 let first_epoch = EpochNumber::new(first_epoch);
2896 let (members, staked) = genesis_members();
2897 let coordinator =
2898 coordinator_for(committees_from_first_epoch(first_epoch, Fetcher::mock()));
2899
2900 assert_none_reads_first_epoch(&coordinator, first_epoch, &members);
2901 for (lookup, membership) in lookups(&coordinator, None) {
2904 assert_da_committee(&format!("{lookup}(None)"), &membership, &staked);
2905 }
2906 }
2907
2908 #[rstest]
2912 #[case::genesis(1)]
2913 #[case::later(5)]
2914 #[case::mainnet(275)]
2915 #[tokio::test]
2916 async fn none_epoch_leaders_rotate_round_robin(#[case] first_epoch: u64) {
2917 let first_epoch = EpochNumber::new(first_epoch);
2918 let (all, staked) = unordered_genesis_members();
2919 let committees =
2920 EpochCommittees::new(all.clone(), all, None, Fetcher::mock(), 100u64, first_epoch);
2921 let coordinator = coordinator_for(committees);
2922
2923 for (lookup, membership) in lookups(&coordinator, None) {
2924 for view in 0..64 {
2925 let expected =
2926 PubKey::public_key(&staked[view as usize % staked.len()].stake_table_entry);
2927 let leader = membership
2928 .leader(ViewNumber::new(view))
2929 .unwrap_or_else(|err| panic!("{lookup}(None), view {view}: no leader: {err}"));
2930 assert_eq!(leader, expected, "{lookup}(None), view {view}: leader");
2931 }
2932 }
2933 }
2934
2935 fn unordered_genesis_members() -> (Vec<PeerConfig<SeqTypes>>, Vec<PeerConfig<SeqTypes>>) {
2939 let all: Vec<_> = [
2940 (5, 300),
2941 (0, 0),
2942 (7, 100),
2943 (2, 500),
2944 (6, 0),
2945 (1, 200),
2946 (3, 400),
2947 ]
2948 .into_iter()
2949 .map(|(index, stake)| peer(42, index, stake))
2950 .collect();
2951 let staked = all
2952 .iter()
2953 .filter(|p| p.stake_table_entry.stake() > U256::ZERO)
2954 .cloned()
2955 .collect();
2956 (all, staked)
2957 }
2958
2959 #[rstest]
2962 #[case::genesis(1)]
2963 #[case::later(5)]
2964 #[case::mainnet(275)]
2965 #[tokio::test]
2966 async fn none_epoch_stake_table_is_staked_genesis_members_in_order(#[case] first_epoch: u64) {
2967 let first_epoch = EpochNumber::new(first_epoch);
2968 let (all, staked) = unordered_genesis_members();
2969 let unstaked: Vec<PubKey> = all
2970 .iter()
2971 .filter(|p| p.stake_table_entry.stake() == U256::ZERO)
2972 .map(|p| PubKey::public_key(&p.stake_table_entry))
2973 .collect();
2974 let committees =
2975 EpochCommittees::new(all.clone(), all, None, Fetcher::mock(), 100u64, first_epoch);
2976 let coordinator = coordinator_for(committees);
2977
2978 for (lookup, membership) in lookups(&coordinator, None) {
2979 let stake_table: Vec<_> = membership.stake_table().cloned().collect();
2980 assert_eq!(stake_table, staked, "{lookup}(None): stake table");
2981 for key in &unstaked {
2982 assert!(
2983 membership.stake(key).is_none(),
2984 "{lookup}(None): member {key} without stake is in the stake table"
2985 );
2986 }
2987 }
2988 }
2989
2990 #[rstest]
2993 #[case::genesis(1)]
2994 #[case::later(5)]
2995 #[case::mainnet(275)]
2996 #[tokio::test]
2997 async fn none_epoch_keeps_genesis_da_committee_when_first_epoch_has_another(
2998 #[case] first_epoch: u64,
2999 ) {
3000 let first_epoch = EpochNumber::new(first_epoch);
3001 let (members, staked) = genesis_members();
3002 let committees = committees_from_first_epoch(first_epoch, Fetcher::mock());
3003 let other_da: Vec<_> = (0..2).map(|i| peer(123, i, 50)).collect();
3004 committees.add_da_committee(first_epoch, other_da.clone());
3005 let coordinator = coordinator_for(committees);
3006
3007 for (lookup, membership) in lookups(&coordinator, Some(first_epoch)) {
3008 assert_da_committee(&format!("{lookup}({first_epoch})"), &membership, &other_da);
3009 }
3010 for (lookup, membership) in lookups(&coordinator, None) {
3011 assert_da_committee(&format!("{lookup}(None)"), &membership, &staked);
3012 }
3013 assert_none_reads_first_epoch(&coordinator, first_epoch, &members);
3014 }
3015
3016 fn fetcher_with_latest_stake(stakes: Vec<crate::v0_3::IndexedStake>) -> Fetcher {
3019 Fetcher::new(
3020 Arc::new(crate::mock::MockStateCatchup::default()),
3021 Arc::new(AsyncMutex::new(LatestStakeStore { stakes })),
3022 crate::L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
3023 .expect("Failed to create L1 client"),
3024 crate::ChainConfig::default(),
3025 )
3026 }
3027
3028 #[rstest]
3029 #[case::genesis(1)]
3030 #[case::later(5)]
3031 #[case::mainnet(275)]
3032 #[tokio::test]
3033 async fn none_epoch_ignores_stake_table_and_da_committee_of_later_epoch(
3034 #[case] first_epoch: u64,
3035 ) {
3036 let first_epoch = EpochNumber::new(first_epoch);
3037 let later = first_epoch + 2;
3038 let (_, staked) = genesis_members();
3039
3040 let stored = AuthenticatedValidator::mock();
3041 let stored_key = *stored.stake_table_key();
3042 let stored_table: AuthenticatedValidatorMap =
3043 [(stored.account, stored)].into_iter().collect();
3044 let fetcher = fetcher_with_latest_stake(vec![(later, (stored_table, None), None)]);
3045 let mut committees = committees_from_first_epoch(first_epoch, fetcher);
3046 committees.reload_stake(RECENT_STAKE_TABLES_LIMIT).await;
3047 let coordinator = coordinator_for(committees);
3048 let later_da: Vec<_> = (0..2).map(|i| peer(77, i, 50)).collect();
3049 coordinator
3050 .membership()
3051 .add_da_committee(later, later_da.clone());
3052
3053 let loaded = coordinator
3054 .stake_table_for_epoch(Some(later))
3055 .unwrap_or_else(|err| panic!("epoch {later}: no stake table: {err}"));
3056 let loaded_keys: Vec<PubKey> = loaded
3057 .stake_table()
3058 .map(|p| PubKey::public_key(&p.stake_table_entry))
3059 .collect();
3060 assert_eq!(loaded_keys, vec![stored_key], "epoch {later}: stake table");
3061 let loaded_da: Vec<_> = loaded.da_stake_table().cloned().collect();
3062 assert_eq!(loaded_da, later_da, "epoch {later}: DA committee");
3063
3064 for (lookup, membership) in lookups(&coordinator, None) {
3067 let stake_table: Vec<_> = membership.stake_table().cloned().collect();
3068 assert_eq!(stake_table, staked, "{lookup}(None): stake table");
3069 let da: Vec<_> = membership.da_stake_table().cloned().collect();
3070 assert_eq!(da, staked, "{lookup}(None): DA committee");
3071 }
3072 }
3073
3074 #[rstest]
3077 #[case::empty_from_genesis(0, EpochNumber::genesis())]
3078 #[case::empty_from_later(0, EpochNumber::new(5))]
3079 #[case::zero_stake_from_genesis(3, EpochNumber::genesis())]
3080 #[case::zero_stake_from_later(3, EpochNumber::new(5))]
3081 #[tokio::test]
3082 async fn none_epoch_leader_errors_without_staked_members(
3083 #[case] members: u64,
3084 #[case] first_epoch: EpochNumber,
3085 ) {
3086 let members = unstaked_members(members);
3087 let committees = EpochCommittees::new(
3088 members.clone(),
3089 members,
3090 None,
3091 Fetcher::mock(),
3092 100u64,
3093 first_epoch,
3094 );
3095 let coordinator = coordinator_for(committees);
3096
3097 for (lookup, membership) in lookups(&coordinator, None) {
3098 for view in [0, 1, 7, 1000] {
3099 let result = membership.leader(ViewNumber::new(view));
3100 assert!(
3101 result.is_err(),
3102 "{lookup}(None), view {view}: expected an error, got {result:?}"
3103 );
3104 }
3105 }
3106 }
3107
3108 fn members_with_stakes(seed: u8, stakes: &[u64]) -> Vec<PeerConfig<SeqTypes>> {
3110 (0..)
3111 .zip(stakes)
3112 .map(|(index, &stake)| peer(seed, index, stake))
3113 .collect()
3114 }
3115
3116 fn member_with_hex_stake(seed: u8, stake: &str) -> PeerConfig<SeqTypes> {
3119 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
3120 [seed; 32],
3121 0,
3122 hex_u256(stake),
3123 true,
3124 )
3125 .public_config()
3126 }
3127
3128 fn hex_u256(hex: &str) -> U256 {
3129 U256::from_str_radix(hex, 16).unwrap_or_else(|err| panic!("{hex}: {err}"))
3130 }
3131
3132 fn assert_epoch_thresholds(
3137 quorum: Vec<PeerConfig<SeqTypes>>,
3138 da: Vec<PeerConfig<SeqTypes>>,
3139 failure: U256,
3140 da_success: U256,
3141 ) {
3142 let first_epoch = EpochNumber::new(5);
3143 let committees =
3144 EpochCommittees::new(quorum, da, None, Fetcher::mock(), 100u64, first_epoch);
3145 let coordinator = coordinator_for(committees);
3146
3147 for epoch in [first_epoch, first_epoch + 1] {
3148 let snapshot = coordinator
3149 .membership()
3150 .snapshot(epoch)
3151 .unwrap_or_else(|| panic!("no snapshot for epoch {epoch}"));
3152 assert_eq!(
3153 snapshot.failure_threshold(),
3154 failure,
3155 "snapshot({epoch}): failure threshold"
3156 );
3157 assert_eq!(
3158 snapshot.da_success_threshold(),
3159 da_success,
3160 "snapshot({epoch}): DA success threshold"
3161 );
3162
3163 for (lookup, membership) in lookups(&coordinator, Some(epoch)) {
3164 assert_eq!(
3165 membership.failure_threshold(),
3166 failure,
3167 "{lookup}({epoch}): failure threshold"
3168 );
3169 assert_eq!(
3170 membership.da_success_threshold(),
3171 da_success,
3172 "{lookup}({epoch}): DA success threshold"
3173 );
3174 }
3175 }
3176 }
3177
3178 #[rstest]
3183 #[case::empty(&[], &[], 1, 1)]
3184 #[case::quorum_1_da_100(&[1], &[40, 60], 1, 67)]
3185 #[case::quorum_2_da_10(&[1, 1], &[1, 2, 3, 4], 1, 7)]
3186 #[case::quorum_3_da_4(&[1, 1, 1], &[4], 2, 3)]
3187 #[case::quorum_4_da_3(&[1, 3], &[1, 1, 1], 2, 3)]
3188 #[case::quorum_7_da_5(&[7], &[2, 3], 3, 4)]
3189 #[case::quorum_10_da_2(&[1, 2, 3, 4], &[2], 4, 2)]
3190 #[case::quorum_100_da_1(&[10, 20, 30, 40], &[1], 34, 1)]
3191 #[case::quorum_1000_da_11(&[100, 200, 300, 400], &[5, 6], 334, 8)]
3192 #[tokio::test]
3193 async fn epoch_failure_and_da_success_thresholds(
3194 #[case] quorum: &[u64],
3195 #[case] da: &[u64],
3196 #[case] failure: u64,
3197 #[case] da_success: u64,
3198 ) {
3199 assert_epoch_thresholds(
3200 members_with_stakes(42, quorum),
3201 members_with_stakes(123, da),
3202 U256::from(failure),
3203 U256::from(da_success),
3204 );
3205 }
3206
3207 #[rstest]
3210 #[case::da_just_below_half_of_max(
3212 "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3213 "7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
3214 "5555555555555555555555555555555555555555555555555555555555555556",
3215 "5555555555555555555555555555555555555555555555555555555555555555"
3216 )]
3217 #[case::da_half_of_max(
3219 "7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
3220 "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3221 "2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
3222 "5555555555555555555555555555555555555555555555555555555555555556"
3223 )]
3224 #[case::da_max(
3226 "1",
3227 "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3228 "1",
3229 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"
3230 )]
3231 #[tokio::test]
3232 async fn epoch_failure_and_da_success_thresholds_near_u256_max(
3233 #[case] quorum: &str,
3234 #[case] da: &str,
3235 #[case] failure: &str,
3236 #[case] da_success: &str,
3237 ) {
3238 assert_epoch_thresholds(
3239 vec![member_with_hex_stake(42, quorum)],
3240 vec![member_with_hex_stake(123, da)],
3241 hex_u256(failure),
3242 hex_u256(da_success),
3243 );
3244 }
3245}