1use std::{
2 collections::{BTreeMap, HashMap},
3 ops::Bound,
4 sync::Arc,
5};
6
7use alloy::primitives::{Address, U256};
8use anyhow::{Context, bail};
9use async_lock::Mutex as AsyncMutex;
10use committable::Commitment;
11use hotshot_types::{
12 PeerConfig, PeerConnectInfo,
13 data::{BlockNumber, EpochNumber, ViewNumber},
14 drb::{
15 DrbResult,
16 election::{RandomizedCommittee, generate_stake_cdf, select_randomized_leader},
17 },
18 epoch_membership::EpochMembershipCoordinator,
19 signature_key::BLSPubKey,
20 stake_table::StakeTableEntry,
21 traits::{
22 election::{Membership, MembershipSnapshot, NonEpochMembershipSnapshot},
23 signature_key::{SignatureKey as _, StakeTableEntryType},
24 },
25 utils::{epoch_from_block_number, root_block_in_epoch, transition_block_for_epoch},
26};
27#[cfg(feature = "node")]
28use hotshot_types::{traits::block_contents::BlockHeader, utils::is_epoch_root};
29use indexmap::IndexMap;
30use parking_lot::RwLock;
31use thiserror::Error;
32use tracing::{debug, error, info, warn};
33#[cfg(feature = "node")]
34use versions::DRB_AND_HEADER_UPGRADE_VERSION;
35use versions::EPOCH_VERSION;
36
37use super::{
38 AuthenticatedValidatorMap, RegisteredValidatorMap, StakeTableHash, StakeTableState,
39 compute_block_reward,
40};
41use crate::{
42 Header, Leaf2, PubKey, SeqTypes,
43 traits::StateCatchup,
44 v0_3::{ASSUMED_BLOCK_TIME_SECONDS, AuthenticatedValidator, Fetcher, RewardAmount},
45};
46
47#[derive(Clone, Debug)]
49pub struct EpochCommittees {
50 inner: Arc<RwLock<Inner>>,
51 fetcher: Arc<Fetcher>,
52 #[cfg_attr(not(feature = "node"), allow(dead_code))]
53 update_fixed_block_reward_lock: Arc<AsyncMutex<()>>,
54 epoch_height: BlockNumber,
55}
56
57#[derive(Debug)]
58struct Inner {
59 non_epoch_snapshot: NonEpochSnapshot,
64
65 snapshots: BTreeMap<EpochNumber, EpochSnapshot>,
69
70 #[cfg_attr(not(feature = "node"), allow(dead_code))]
72 all_validators: BTreeMap<EpochNumber, RegisteredValidatorMap>,
73
74 da_committees: BTreeMap<EpochNumber, Arc<DaCommittee>>,
78
79 first_epoch: Option<EpochNumber>,
80
81 fixed_block_reward: Option<RewardAmount>,
85}
86
87#[derive(Clone, Debug)]
88struct DaCommittee {
89 committee: Vec<PeerConfig<SeqTypes>>,
90 indexed_committee: HashMap<PubKey, PeerConfig<SeqTypes>>,
91}
92
93#[derive(Debug)]
95struct NonEpochCommittee {
96 eligible_leaders: Vec<PeerConfig<SeqTypes>>,
101
102 stake_table: Vec<PeerConfig<SeqTypes>>,
104
105 indexed_stake_table: HashMap<PubKey, PeerConfig<SeqTypes>>,
107}
108
109#[derive(Debug)]
111struct EpochCommittee {
112 eligible_leaders: Vec<PeerConfig<SeqTypes>>,
117 stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>>,
119 validators: AuthenticatedValidatorMap,
120 address_mapping: HashMap<BLSPubKey, Address>,
121 block_reward: Option<RewardAmount>,
122 stake_table_hash: Option<StakeTableHash>,
123 header: Option<Header>,
124}
125
126impl EpochCommittee {
127 fn new(
128 validators: AuthenticatedValidatorMap,
129 block_reward: Option<RewardAmount>,
130 hash: Option<StakeTableHash>,
131 header: Option<Header>,
132 ) -> Self {
133 let mut address_mapping = HashMap::new();
134 let stake_table: IndexMap<PubKey, PeerConfig<SeqTypes>> = validators
135 .values()
136 .map(|v| {
137 let key = *v.stake_table_key();
138 address_mapping.insert(key, v.account);
139 (
140 key,
141 PeerConfig {
142 stake_table_entry: BLSPubKey::stake_table_entry(&key, v.stake),
143 state_ver_key: v.state_ver_key().clone(),
144 connect_info: v.x25519_key.and_then(|p| {
145 let a = v.p2p_addr.clone()?;
146 Some(PeerConnectInfo {
147 x25519_key: p,
148 p2p_addr: a,
149 })
150 }),
151 },
152 )
153 })
154 .collect();
155
156 let eligible_leaders: Vec<PeerConfig<SeqTypes>> =
157 stake_table.iter().map(|(_, l)| l.clone()).collect();
158
159 Self {
160 eligible_leaders,
161 stake_table,
162 validators,
163 address_mapping,
164 block_reward,
165 stake_table_hash: hash,
166 header,
167 }
168 }
169}
170
171impl EpochCommittees {
172 pub fn epoch_height(&self) -> BlockNumber {
173 self.epoch_height
174 }
175
176 pub fn first_epoch(&self) -> Option<EpochNumber> {
177 self.inner.read().first_epoch
178 }
179
180 pub fn fetcher(&self) -> &Fetcher {
181 &self.fetcher
182 }
183
184 pub fn fixed_block_reward(&self) -> Option<RewardAmount> {
185 self.inner.read().fixed_block_reward
186 }
187
188 pub fn latest_peer_config(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
193 let inner = self.inner.read();
194 for snap in inner.snapshots.values().rev() {
195 if let Some(cfg) = snap.inner.committee.stake_table.get(key) {
196 return Some(cfg.clone());
197 }
198 }
199 inner
200 .non_epoch_snapshot
201 .inner
202 .committee
203 .indexed_stake_table
204 .get(key)
205 .cloned()
206 }
207
208 #[cfg(feature = "node")]
213 async fn fetch_and_update_fixed_block_reward(
214 &self,
215 epoch: EpochNumber,
216 ) -> anyhow::Result<RewardAmount> {
217 let _guard = self.update_fixed_block_reward_lock.lock().await;
219
220 #[allow(clippy::significant_drop_in_scrutinee)]
229 if let Some(reward) = self.inner.read().fixed_block_reward {
230 Ok(reward)
231 } else {
232 warn!(%epoch,
233 "Block reward is None. attempting to fetch it from L1",
234 );
235 let block_reward =
236 self.fetcher
237 .fetch_fixed_block_reward()
238 .await
239 .inspect_err(|err| {
240 error!(?epoch, ?err, "failed to fetch block_reward");
241 })?;
242 self.inner.write().fixed_block_reward = Some(block_reward);
243 Ok(block_reward)
244 }
245 }
246
247 async fn calculate_dynamic_block_reward(
254 &self,
255 epoch: EpochNumber,
256 header: &Header,
257 validators: &AuthenticatedValidatorMap,
258 coordinator: &EpochMembershipCoordinator<SeqTypes>,
259 ) -> anyhow::Result<Option<RewardAmount>> {
260 let epoch_height = self.epoch_height;
261 let current_epoch = epoch_from_block_number(header.height(), *epoch_height);
262 let previous_epoch = current_epoch
263 .checked_sub(1)
264 .context("underflow: cannot get previous epoch when current_epoch is 0")?;
265 debug!(?epoch, "previous_epoch={previous_epoch:?}");
266
267 let first_epoch = *self.first_epoch().context("first epoch is None")?;
268
269 if previous_epoch > first_epoch + 1 && self.snapshot(previous_epoch.into()).is_none() {
272 warn!(?previous_epoch, "missing stake table for previous epoch");
273 return Ok(None);
274 }
275
276 let previous_reward_distributed = header
277 .total_reward_distributed()
278 .context("Invalid block header: missing total_reward_distributed field")?;
279
280 let total_stake: U256 = validators.values().map(|v| v.stake).sum();
282 let initial_supply = self.fetcher.initial_supply_or_fetch().await?;
283 let total_supply = initial_supply
284 .checked_add(previous_reward_distributed.0)
285 .context("initial_supply + previous_reward_distributed overflow")?;
286
287 let curr_ts = header.timestamp_millis_internal();
289 debug!(?epoch, "curr_ts={curr_ts:?}");
290
291 let average_block_time_ms = if previous_epoch <= first_epoch + 1 {
295 ASSUMED_BLOCK_TIME_SECONDS as u64 * 1000 } else {
297 let next_epoch = epoch
301 .checked_sub(1)
302 .context("underflow: cannot get next epoch when epoch is 0")?;
303 let prev_ts = match self.map_header(next_epoch, |h| h.timestamp_millis_internal()) {
304 Some(ts) => ts,
305 None => {
306 info!(
307 "Calculating rewards for epoch {}, we have no root leaf header for epoch \
308 - 1. Fetching from peers",
309 epoch
310 );
311
312 let root_height = header.height().checked_sub(*epoch_height).context(
313 "Epoch height is greater than block height. cannot compute previous epoch \
314 root height",
315 )?;
316
317 self.fetcher
318 .peers
319 .fetch_leaf(coordinator.clone(), root_height)
320 .await
321 .context("Epoch root leaf not found")?
322 .block_header()
323 .timestamp_millis_internal()
324 },
325 };
326
327 let time_diff = curr_ts.checked_sub(prev_ts).context(
328 "Current timestamp is earlier than previous. underflow in block time calculation",
329 )?;
330
331 time_diff
332 .checked_div(*epoch_height)
333 .context("Epoch height is zero. cannot compute average block time")?
334 };
335 info!(?epoch, %total_supply, %total_stake, %average_block_time_ms,
336 "dynamic block reward parameters");
337
338 let block_reward =
339 compute_block_reward(epoch, total_supply, total_stake, average_block_time_ms)?;
340
341 Ok(Some(block_reward))
342 }
343
344 pub fn epoch_block_reward(&self, epoch: EpochNumber) -> Option<RewardAmount> {
346 self.inner
347 .read()
348 .epoch_committee(epoch)
349 .and_then(|committee| committee.block_reward)
350 }
351
352 pub fn get_validator_index(&self, epoch: EpochNumber, bls_key: &PubKey) -> Option<usize> {
357 self.inner
358 .read()
359 .epoch_committee(epoch)
360 .and_then(|committee| committee.stake_table.get_index_of(bls_key))
361 }
362
363 pub fn active_validators(&self, e: EpochNumber) -> anyhow::Result<AuthenticatedValidatorMap> {
364 self.inner.read().active_validators(e)
365 }
366
367 pub fn address(&self, e: EpochNumber, key: &BLSPubKey) -> anyhow::Result<Address> {
368 self.inner.read().address(e, key)
369 }
370
371 pub fn get_validator_config(
372 &self,
373 epoch: EpochNumber,
374 key: &BLSPubKey,
375 ) -> anyhow::Result<AuthenticatedValidator<BLSPubKey>> {
376 let inner = self.inner.read();
377 let address = inner.address(epoch, key)?;
378 let validators = inner.active_validators(epoch)?;
379 validators
380 .get(&address)
381 .context("validator not found")
382 .cloned()
383 }
384
385 pub fn new_stake<B: Into<BlockNumber>>(
387 committee_members: Vec<PeerConfig<SeqTypes>>,
390 da_members: Vec<PeerConfig<SeqTypes>>,
391 fixed_block_reward: Option<RewardAmount>,
392 fetcher: Fetcher,
393 epoch_height: B,
394 ) -> Self {
395 let stake_table: Vec<_> = committee_members
397 .iter()
398 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
399 .cloned()
400 .collect();
401
402 let eligible_leaders = stake_table.clone();
403 let da_members: Vec<_> = da_members
405 .iter()
406 .filter(|&peer_config| peer_config.stake_table_entry.stake() > U256::ZERO)
407 .cloned()
408 .collect();
409
410 let indexed_stake_table: HashMap<PubKey, _> = stake_table
412 .iter()
413 .map(|peer_config| {
414 (
415 PubKey::public_key(&peer_config.stake_table_entry),
416 peer_config.clone(),
417 )
418 })
419 .collect();
420
421 let indexed_da_members: HashMap<PubKey, _> = da_members
423 .iter()
424 .map(|peer_config| {
425 (
426 PubKey::public_key(&peer_config.stake_table_entry),
427 peer_config.clone(),
428 )
429 })
430 .collect();
431
432 let da_committee = Arc::new(DaCommittee {
433 committee: da_members,
434 indexed_committee: indexed_da_members,
435 });
436
437 let members = Arc::new(NonEpochCommittee {
438 eligible_leaders,
439 stake_table,
440 indexed_stake_table,
441 });
442
443 let non_epoch_snapshot = NonEpochSnapshot::new(members.clone(), da_committee.clone());
444
445 let epoch_committee = Arc::new(EpochCommittee {
446 eligible_leaders: members.eligible_leaders.clone(),
447 stake_table: members
448 .stake_table
449 .iter()
450 .map(|x| (PubKey::public_key(&x.stake_table_entry), x.clone()))
451 .collect(),
452 validators: Default::default(),
453 address_mapping: HashMap::new(),
454 block_reward: Default::default(),
455 stake_table_hash: None,
456 header: None,
457 });
458
459 let mut snapshots = BTreeMap::new();
460 snapshots.insert(
461 EpochNumber::genesis(),
462 EpochSnapshot::new(
463 EpochNumber::genesis(),
464 None,
465 epoch_committee.clone(),
466 None,
467 da_committee.clone(),
468 ),
469 );
470 snapshots.insert(
472 EpochNumber::genesis() + 1u64,
473 EpochSnapshot::new(
474 EpochNumber::genesis() + 1u64,
475 None,
476 epoch_committee,
477 None,
478 da_committee,
479 ),
480 );
481
482 Self {
483 inner: Arc::new(RwLock::new(Inner {
484 non_epoch_snapshot,
485 da_committees: BTreeMap::new(),
486 snapshots,
487 all_validators: BTreeMap::new(),
488 first_epoch: None,
489 fixed_block_reward,
490 })),
491 fetcher: Arc::new(fetcher),
492 update_fixed_block_reward_lock: Arc::new(AsyncMutex::new(())),
493 epoch_height: epoch_height.into(),
494 }
495 }
496
497 #[cfg(feature = "node")]
498 pub async fn reload_stake(&mut self, limit: u64) {
499 match self.fetcher.fetch_fixed_block_reward().await {
500 Ok(block_reward) => {
501 info!("Fetched block reward: {block_reward}");
502 self.inner.write().fixed_block_reward = Some(block_reward);
503 },
504 Err(err) => {
505 warn!("Failed to fetch the block reward when reloading the stake tables: {err}");
506 },
507 }
508
509 let loaded_stake = match self
511 .fetcher
512 .persistence
513 .lock()
514 .await
515 .load_latest_stake(limit)
516 .await
517 {
518 Ok(Some(loaded)) => loaded,
519 Ok(None) => {
520 warn!("No stake table history found in persistence!");
521 return;
522 },
523 Err(e) => {
524 error!("Failed to load stake table history from persistence: {e}");
525 return;
526 },
527 };
528
529 for (epoch, (validators, block_reward), stake_table_hash) in loaded_stake {
530 let committee = EpochCommittee::new(validators, block_reward, stake_table_hash, None);
531 self.inner
532 .write()
533 .put_epoch_committee(epoch, Arc::new(committee));
534 }
535 }
536
537 fn map_header<E, F, R>(&self, epoch: E, f: F) -> Option<R>
539 where
540 E: Into<EpochNumber>,
541 F: FnMut(&Header) -> R,
542 {
543 self.inner
544 .read()
545 .epoch_committee(epoch.into())
546 .and_then(|committee| committee.header.as_ref().map(f))
547 }
548
549 fn randomized_committee(
550 &self,
551 epoch: EpochNumber,
552 drb: DrbResult,
553 ) -> Option<RandomizedCommittee<StakeTableEntry<PubKey>>> {
554 let inner = self.inner.read();
555 let Some(raw_stake_table) = inner.epoch_committee(epoch) else {
556 error!(
557 "randomized_committee({epoch}, {drb:?}) was called, but we do not yet have the \
558 stake table for epoch {epoch}"
559 );
560 return None;
561 };
562
563 let leaders = raw_stake_table
564 .eligible_leaders
565 .clone()
566 .into_iter()
567 .map(|peer_config| peer_config.stake_table_entry)
568 .collect::<Vec<_>>();
569
570 Some(generate_stake_cdf(leaders, drb))
571 }
572}
573
574pub async fn fetch_and_calculate_block_reward(
584 coordinator: EpochMembershipCoordinator<SeqTypes>,
585 current_epoch: EpochNumber,
586) -> anyhow::Result<RewardAmount> {
587 let committee;
588 let first_epoch;
589 let fixed_block_reward;
590 {
591 let membership = coordinator.membership().inner.read();
592 fixed_block_reward = membership.fixed_block_reward;
593
594 committee = membership
595 .epoch_committee(current_epoch)
596 .context(format!("committee not found for epoch={current_epoch:?}"))?
597 .clone();
598
599 if let Some(reward) = committee.block_reward {
601 return Ok(reward);
602 }
603
604 first_epoch = membership.first_epoch.context(format!(
605 "First epoch not initialized (current_epoch={current_epoch})"
606 ))?;
607 }
608
609 if *current_epoch <= *first_epoch + 1 {
610 bail!(
611 "epoch is in first two epochs: current_epoch={current_epoch}, \
612 first_epoch={first_epoch}"
613 );
614 }
615
616 let header = match committee.header.clone() {
617 Some(header) => header,
618 None => {
619 let root_epoch = current_epoch.checked_sub(2).context(format!(
620 "Epoch calculation underflow (current_epoch={current_epoch})"
621 ))?;
622
623 info!(?root_epoch, "catchup epoch root header");
624
625 let leaf = coordinator
626 .get_epoch_root(EpochNumber::new(root_epoch))
627 .await
628 .with_context(|| format!("Failed to get epoch root for root_epoch={root_epoch}"))?;
629 leaf.block_header().clone()
630 },
631 };
632
633 if header.version() <= EPOCH_VERSION {
634 return fixed_block_reward.context(format!(
635 "Fixed block reward not found for current_epoch={current_epoch}"
636 ));
637 }
638
639 let prev_epoch_u64 = current_epoch.checked_sub(1).context(format!(
640 "Underflow: cannot compute previous epoch when current_epoch={current_epoch}"
641 ))?;
642
643 let prev_epoch = EpochNumber::new(prev_epoch_u64);
644
645 if *prev_epoch > *first_epoch + 1
648 && let Err(err) = coordinator.stake_table_for_epoch(Some(prev_epoch))
649 {
650 info!("failed to get membership for epoch={prev_epoch:?}: {err:#}");
651
652 coordinator
653 .wait_for_catchup(prev_epoch)
654 .await
655 .context(format!("failed to catch up for epoch={prev_epoch}"))?;
656 }
657
658 coordinator
659 .membership()
660 .calculate_dynamic_block_reward(current_epoch, &header, &committee.validators, &coordinator)
661 .await
662 .with_context(|| {
663 format!("dynamic block reward calculation failed for epoch={current_epoch}")
664 })?
665 .with_context(|| format!("dynamic block reward returned None. epoch={current_epoch}"))
666}
667
668impl Membership<SeqTypes> for EpochCommittees {
669 type Error = EpochCommitteesError;
670 type Snapshot = EpochSnapshot;
671 type NonEpochSnapshot = NonEpochSnapshot;
672
673 fn snapshot(&self, epoch: EpochNumber) -> Option<Self::Snapshot> {
674 self.inner.read().snapshots.get(&epoch).cloned()
675 }
676
677 fn non_epoch_snapshot(&self) -> Self::NonEpochSnapshot {
678 self.inner.read().non_epoch_snapshot.clone()
679 }
680
681 #[cfg(not(feature = "node"))]
685 async fn add_epoch_root(
686 &self,
687 _block_header: Header,
688 _coordinator: &EpochMembershipCoordinator<SeqTypes>,
689 ) -> Result<(), Self::Error> {
690 unimplemented!("add_epoch_root requires the node feature");
692 }
693
694 #[cfg(feature = "node")]
698 async fn add_epoch_root(
699 &self,
700 block_header: Header,
701 coordinator: &EpochMembershipCoordinator<SeqTypes>,
702 ) -> Result<(), Self::Error> {
703 let block_number = block_header.block_number();
704
705 let epoch_height = *self.epoch_height;
706
707 let epoch = EpochNumber::new(epoch_from_block_number(block_number, epoch_height) + 2);
708
709 info!(?epoch, "adding epoch root. height={:?}", block_number);
710
711 if !is_epoch_root(block_number, epoch_height) {
712 error!(
713 "`add_epoch_root` was called with a block header that was not the root block for \
714 an epoch. This should never happen. Header:\n\n{block_header:?}"
715 );
716 return Err(Self::Error::NoRootBlock(block_number.into()));
717 }
718
719 let version = block_header.version();
720 self.fetcher
722 .update_chain_config(&block_header)
723 .await
724 .map_err(Self::Error::Fetcher)?;
725
726 let mut block_reward = None;
727 if version == EPOCH_VERSION {
730 let reward = self
731 .fetch_and_update_fixed_block_reward(epoch)
732 .await
733 .map_err(Self::Error::Fetcher)?;
734 block_reward = Some(reward);
735 }
736
737 let epoch_committee = self.inner.read().epoch_committee(epoch).cloned();
738
739 let (active_validators, all_validators, stake_table_hash) = match epoch_committee {
748 Some(committee)
749 if committee.block_reward.is_some()
750 && committee.header.is_some()
751 && committee.stake_table_hash.is_some() =>
752 {
753 info!(
754 ?epoch,
755 "committee already has block reward, header, and stake table hash; skipping \
756 add_epoch_root"
757 );
758 return Ok(());
759 },
760
761 Some(committee) => {
762 if let Some(reward) = committee.block_reward {
763 block_reward = Some(reward);
764 }
765
766 if let Some(hash) = committee.stake_table_hash {
767 (committee.validators.clone(), Default::default(), Some(hash))
768 } else {
769 info!(
771 "Stake table hash missing for epoch {epoch}. recalculating by fetching \
772 from l1."
773 );
774 let set = self
775 .fetcher
776 .fetch(epoch, &block_header)
777 .await
778 .map_err(Self::Error::Fetcher)?;
779 (
780 set.active_validators,
781 set.all_validators,
782 set.stake_table_hash,
783 )
784 }
785 },
786
787 None => {
788 info!("Stake table missing for epoch {epoch}. Fetching from L1.");
789 let set = self
790 .fetcher
791 .fetch(epoch, &block_header)
792 .await
793 .map_err(Self::Error::Fetcher)?;
794 (
795 set.active_validators,
796 set.all_validators,
797 set.stake_table_hash,
798 )
799 },
800 };
801
802 if block_reward.is_none() && version >= DRB_AND_HEADER_UPGRADE_VERSION {
806 info!(?epoch, "calculating dynamic block reward");
807 let reward = self
808 .calculate_dynamic_block_reward(
809 epoch,
810 &block_header,
811 &active_validators,
812 coordinator,
813 )
814 .await
815 .map_err(Self::Error::Reward)?;
816
817 info!(?epoch, "calculated dynamic block reward = {reward:?}");
818 block_reward = reward;
819 }
820
821 let committee = EpochCommittee::new(
822 active_validators.clone(),
823 block_reward,
824 stake_table_hash,
825 Some(block_header.clone()),
826 );
827
828 let previous_epoch;
829 let previous_committee;
830 let previous_validators;
831 {
832 let mut inner = self.inner.write();
833 inner.put_epoch_committee(epoch, Arc::new(committee));
834 previous_epoch = EpochNumber::new(epoch.saturating_sub(1));
837 previous_committee = inner.epoch_committee(previous_epoch).cloned();
838 inner.all_validators = inner.all_validators.split_off(&previous_epoch);
840 previous_validators = inner.all_validators.remove(&previous_epoch);
842 inner.all_validators.insert(epoch, all_validators.clone());
843 }
844
845 let persistence_lock = self.fetcher.persistence.lock().await;
846
847 let decided_hash = block_header.next_stake_table_hash();
848
849 if let Some(previous_committee) = previous_committee {
856 if decided_hash.is_none() || decided_hash == previous_committee.stake_table_hash {
857 if let Err(e) = persistence_lock
858 .store_stake(
859 previous_epoch,
860 previous_committee.validators.clone(),
861 previous_committee.block_reward,
862 previous_committee.stake_table_hash,
863 )
864 .await
865 {
866 error!(
867 ?e,
868 ?previous_epoch,
869 "`add_epoch_root`, error storing stake table"
870 );
871 }
872
873 if let Some(previous_validators) = previous_validators
874 && let Err(e) = persistence_lock
875 .store_all_validators(previous_epoch, previous_validators)
876 .await
877 {
878 error!(?e, ?epoch, "`add_epoch_root`, error storing all validators");
879 }
880 } else {
881 panic!(
882 "The decided block header's `next_stake_table_hash` does not match the hash \
883 of the stake table we have. This is an unrecoverable error likely due to \
884 issues with your L1 RPC provider. Decided:\n\n{:?}Actual:\n\n{:?}",
885 decided_hash, previous_committee.stake_table_hash
886 );
887 }
888 }
889
890 Ok(())
891 }
892
893 async fn get_epoch_root(
894 &self,
895 epoch: EpochNumber,
896 coordinator: &EpochMembershipCoordinator<SeqTypes>,
897 ) -> Result<Leaf2, Self::Error> {
898 let block_height = root_block_in_epoch(*epoch, *self.epoch_height());
899 let peers = self.fetcher.peers.clone();
900
901 for retry in 0..3 {
905 match peers
906 .try_fetch_leaf(retry, coordinator.clone(), block_height)
907 .await
908 {
909 Ok(leaf) => return Ok(leaf),
910 Err(err) => {
911 warn!(%epoch, block_height, retry, "failed to fetch epoch root leaf: {err:#}");
912 },
913 }
914 }
915 Err(Self::Error::Catchup(anyhow::anyhow!(
916 "failed to fetch epoch root leaf for epoch {epoch} (block {block_height}) from peers"
917 )))
918 }
919
920 async fn get_epoch_drb(
921 &self,
922 epoch: EpochNumber,
923 coordinator: &EpochMembershipCoordinator<SeqTypes>,
924 ) -> Result<DrbResult, Self::Error> {
925 let peers = self.fetcher.peers.clone();
926
927 if let Some(snap) = self.snapshot(epoch)
929 && let Some(rand) = &snap.inner.randomized
930 {
931 return Ok(rand.drb_result());
932 }
933
934 let previous_epoch = match epoch.checked_sub(1) {
936 Some(epoch) => EpochNumber::new(epoch),
937 None => {
938 return self
939 .snapshot(epoch)
940 .and_then(|s| s.inner.randomized.as_ref().map(|r| r.drb_result()))
941 .ok_or_else(|| {
942 Self::Error::Message(format!(
943 "Missing randomized committee for epoch {epoch}"
944 ))
945 });
946 },
947 };
948
949 let block_height = transition_block_for_epoch(*previous_epoch, *self.epoch_height());
950
951 debug!(
952 "Getting DRB for epoch {}, block height {}",
953 epoch, block_height
954 );
955 let drb_leaf = peers
956 .try_fetch_leaf(1, coordinator.clone(), block_height)
957 .await
958 .map_err(Self::Error::Catchup)?;
959
960 let Some(drb) = drb_leaf.next_drb_result else {
961 error!(
962 "We received a leaf that should contain a DRB result, but the DRB result is \
963 missing: {:?}",
964 drb_leaf
965 );
966
967 return Err(Self::Error::Message(
968 "DRB leaf is missing the DRB result.".to_string(),
969 ));
970 };
971
972 Ok(drb)
973 }
974
975 fn add_drb_result(&self, epoch: EpochNumber, drb: DrbResult) {
976 info!("Adding DRB result {drb:?} to epoch {epoch}");
977 if let Some(committee) = self.randomized_committee(epoch, drb) {
978 self.inner
979 .write()
980 .put_randomized_committee(epoch, Arc::new(committee));
981 }
982 }
983
984 fn set_first_epoch(&self, epoch: EpochNumber, initial_drb_result: DrbResult) {
985 let rand_comm = Arc::new(
986 self.randomized_committee(EpochNumber::genesis(), initial_drb_result)
987 .expect("committee exist at genesis"),
988 );
989
990 let mut inner = self.inner.write();
991 inner.first_epoch = Some(epoch);
992
993 let epoch_committee = inner
994 .epoch_committee(EpochNumber::genesis())
995 .expect("committee exists at genesis")
996 .clone();
997
998 inner.put_epoch_committee(epoch, epoch_committee.clone());
1001 inner.put_randomized_committee(epoch, rand_comm.clone());
1002 inner.put_epoch_committee(epoch + 1, epoch_committee);
1003 inner.put_randomized_committee(epoch + 1, rand_comm);
1004 }
1005
1006 fn first_epoch(&self) -> Option<EpochNumber> {
1007 self.inner.read().first_epoch
1008 }
1009
1010 fn highest_known_epoch(&self) -> Option<EpochNumber> {
1011 self.inner.read().snapshots.keys().max().copied()
1012 }
1013
1014 fn add_da_committee(&self, first_epoch: EpochNumber, committee: Vec<PeerConfig<SeqTypes>>) {
1015 let indexed_committee: HashMap<PubKey, _> = committee
1016 .iter()
1017 .map(|peer_config| {
1018 (
1019 PubKey::public_key(&peer_config.stake_table_entry),
1020 peer_config.clone(),
1021 )
1022 })
1023 .collect();
1024
1025 let da_committee = Arc::new(DaCommittee {
1026 committee,
1027 indexed_committee,
1028 });
1029
1030 let mut inner = self.inner.write();
1031 inner
1032 .da_committees
1033 .insert(first_epoch, da_committee.clone());
1034
1035 let upper = inner
1041 .da_committees
1042 .range((Bound::Excluded(first_epoch), Bound::Unbounded))
1043 .next()
1044 .map(|(k, _)| *k);
1045
1046 let range = if let Some(u) = upper {
1047 (Bound::Included(first_epoch), Bound::Excluded(u))
1048 } else {
1049 (Bound::Included(first_epoch), Bound::Unbounded)
1050 };
1051
1052 let affected: Vec<EpochNumber> = inner.snapshots.range(range).map(|(k, _)| *k).collect();
1053 let first_epoch_field = inner.first_epoch;
1054
1055 for epoch in affected {
1056 let Some(existing) = inner.snapshots.get(&epoch) else {
1057 continue;
1058 };
1059 let new_snapshot = EpochSnapshot::new(
1060 epoch,
1061 first_epoch_field,
1062 existing.inner.committee.clone(),
1063 existing.inner.randomized.clone(),
1064 da_committee.clone(),
1065 );
1066 inner.snapshots.insert(epoch, new_snapshot);
1067 }
1068 }
1069}
1070
1071#[derive(Error, Debug)]
1072pub enum EpochCommitteesError {
1073 #[error("could not lookup leader")]
1074 LeaderLookupError,
1075
1076 #[error("block {0} is not the root block for an epoch")]
1077 NoRootBlock(BlockNumber),
1078
1079 #[error("fetcher error: {0}")]
1080 Fetcher(#[source] anyhow::Error),
1081
1082 #[error("{0}")]
1083 Message(String),
1084
1085 #[error("state catchup error: {0}")]
1086 Catchup(#[source] anyhow::Error),
1087
1088 #[error("reward calculation error: {0}")]
1089 Reward(#[source] anyhow::Error),
1090}
1091
1092impl Inner {
1093 fn resolve_da_committee(&self, epoch: Option<EpochNumber>) -> Arc<DaCommittee> {
1096 if let Some(e) = epoch {
1097 self.da_committees
1099 .range((Bound::Included(0.into()), Bound::Included(e)))
1100 .last()
1101 .map(|(_, committee)| committee.clone())
1102 .unwrap_or_else(|| self.non_epoch_snapshot.inner.da_committee.clone())
1103 } else {
1104 self.non_epoch_snapshot.inner.da_committee.clone()
1105 }
1106 }
1107
1108 fn epoch_committee(&self, e: EpochNumber) -> Option<&Arc<EpochCommittee>> {
1110 self.snapshots.get(&e).map(|s| &s.inner.committee)
1111 }
1112
1113 fn address(&self, e: EpochNumber, key: &BLSPubKey) -> anyhow::Result<Address> {
1114 self.epoch_committee(e)
1115 .context("state for found")?
1116 .address_mapping
1117 .get(key)
1118 .copied()
1119 .context(format!(
1120 "failed to get ethereum address for bls key {key}. epoch={e}"
1121 ))
1122 }
1123
1124 fn active_validators(&self, e: EpochNumber) -> anyhow::Result<AuthenticatedValidatorMap> {
1125 Ok(self
1126 .epoch_committee(e)
1127 .context("state not found")?
1128 .validators
1129 .clone())
1130 }
1131
1132 fn put_epoch_committee(&mut self, epoch: EpochNumber, committee: Arc<EpochCommittee>) {
1135 let randomized = self
1136 .snapshots
1137 .get(&epoch)
1138 .and_then(|s| s.inner.randomized.clone());
1139 let da_committee = self.resolve_da_committee(Some(epoch));
1140 let first_epoch = self.first_epoch;
1141 self.snapshots.insert(
1142 epoch,
1143 EpochSnapshot::new(epoch, first_epoch, committee, randomized, da_committee),
1144 );
1145 }
1146
1147 fn put_randomized_committee(
1151 &mut self,
1152 epoch: EpochNumber,
1153 randomized: Arc<RandomizedCommittee<StakeTableEntry<PubKey>>>,
1154 ) {
1155 let Some(existing) = self.snapshots.get(&epoch).cloned() else {
1156 return;
1157 };
1158 let committee = existing.inner.committee.clone();
1159 let da_committee = existing.inner.da_committee.clone();
1160 let first_epoch = self.first_epoch;
1161 self.snapshots.insert(
1162 epoch,
1163 EpochSnapshot::new(
1164 epoch,
1165 first_epoch,
1166 committee,
1167 Some(randomized),
1168 da_committee,
1169 ),
1170 );
1171 }
1172}
1173
1174#[derive(Clone, Debug)]
1178pub struct EpochSnapshot {
1179 inner: Arc<EpochSnapshotInner>,
1180}
1181
1182#[derive(Debug)]
1183struct EpochSnapshotInner {
1184 epoch: EpochNumber,
1185 first_epoch: Option<EpochNumber>,
1186 committee: Arc<EpochCommittee>,
1187 randomized: Option<Arc<RandomizedCommittee<StakeTableEntry<PubKey>>>>,
1188 da_committee: Arc<DaCommittee>,
1189}
1190
1191impl EpochSnapshot {
1192 fn new(
1193 epoch: EpochNumber,
1194 first_epoch: Option<EpochNumber>,
1195 committee: Arc<EpochCommittee>,
1196 randomized: Option<Arc<RandomizedCommittee<StakeTableEntry<PubKey>>>>,
1197 da_committee: Arc<DaCommittee>,
1198 ) -> Self {
1199 Self {
1200 inner: Arc::new(EpochSnapshotInner {
1201 epoch,
1202 first_epoch,
1203 committee,
1204 randomized,
1205 da_committee,
1206 }),
1207 }
1208 }
1209}
1210
1211impl EpochSnapshot {
1212 pub fn validator_index(&self, key: &PubKey) -> Option<usize> {
1214 self.inner.committee.stake_table.get_index_of(key)
1215 }
1216
1217 pub fn validator_config(
1219 &self,
1220 key: &BLSPubKey,
1221 ) -> anyhow::Result<&AuthenticatedValidator<BLSPubKey>> {
1222 let address = self
1223 .inner
1224 .committee
1225 .address_mapping
1226 .get(key)
1227 .context(format!(
1228 "failed to get ethereum address for bls key {key}. epoch={}",
1229 self.inner.epoch
1230 ))?;
1231 self.inner
1232 .committee
1233 .validators
1234 .get(address)
1235 .context("validator not found")
1236 }
1237
1238 pub fn epoch_block_reward(&self) -> Option<RewardAmount> {
1239 self.inner.committee.block_reward
1240 }
1241
1242 pub fn validators(&self) -> &AuthenticatedValidatorMap {
1243 &self.inner.committee.validators
1244 }
1245}
1246
1247impl MembershipSnapshot<SeqTypes> for EpochSnapshot {
1248 type Error = EpochCommitteesError;
1249 type StakeTableHash = StakeTableState;
1250
1251 fn epoch(&self) -> EpochNumber {
1252 self.inner.epoch
1253 }
1254
1255 fn first_epoch(&self) -> Option<EpochNumber> {
1256 self.inner.first_epoch
1257 }
1258
1259 fn has_drb(&self) -> bool {
1260 self.inner.randomized.is_some()
1261 }
1262
1263 fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<SeqTypes>> + Send {
1264 self.inner.committee.stake_table.values()
1265 }
1266
1267 fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<SeqTypes>> + Send {
1268 self.inner.da_committee.committee.iter()
1269 }
1270
1271 fn committee_members(&self, _: ViewNumber) -> impl ExactSizeIterator<Item = &PubKey> + Send {
1272 self.inner.committee.stake_table.keys()
1273 }
1274
1275 fn da_committee_members(&self, _: ViewNumber) -> impl ExactSizeIterator<Item = &PubKey> + Send {
1276 self.inner.da_committee.indexed_committee.keys()
1277 }
1278
1279 fn stake(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
1280 self.inner.committee.stake_table.get(key).cloned()
1281 }
1282
1283 fn da_stake(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
1284 self.inner.da_committee.indexed_committee.get(key).cloned()
1285 }
1286
1287 fn has_stake(&self, key: &PubKey) -> bool {
1288 self.stake(key)
1289 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
1290 .unwrap_or_default()
1291 }
1292
1293 fn has_da_stake(&self, key: &PubKey) -> bool {
1294 self.da_stake(key)
1295 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
1296 .unwrap_or_default()
1297 }
1298
1299 fn lookup_leader(&self, view: ViewNumber) -> Result<PubKey, Self::Error> {
1306 let inner = &self.inner;
1307 let Some(first_epoch) = inner.first_epoch else {
1308 error!(
1309 "leader requested for epoch {} but first_epoch is unset",
1310 inner.epoch,
1311 );
1312 return Err(EpochCommitteesError::LeaderLookupError);
1313 };
1314 if inner.epoch < first_epoch {
1315 error!(
1316 "leader requested for epoch {} before first epoch {first_epoch}",
1317 inner.epoch,
1318 );
1319 return Err(EpochCommitteesError::LeaderLookupError);
1320 }
1321 let Some(rand) = inner.randomized.as_deref() else {
1322 error!(
1323 "missing randomized committee for epoch {} in snapshot",
1324 inner.epoch,
1325 );
1326 return Err(EpochCommitteesError::LeaderLookupError);
1327 };
1328 Ok(PubKey::public_key(&select_randomized_leader(rand, *view)))
1329 }
1330
1331 fn stake_table_hash(&self) -> Option<Commitment<Self::StakeTableHash>> {
1332 self.inner.committee.stake_table_hash
1333 }
1334}
1335
1336#[derive(Clone, Debug)]
1340pub struct NonEpochSnapshot {
1341 inner: Arc<NonEpochSnapshotInner>,
1342}
1343
1344#[derive(Debug)]
1345struct NonEpochSnapshotInner {
1346 committee: Arc<NonEpochCommittee>,
1347 da_committee: Arc<DaCommittee>,
1348}
1349
1350impl NonEpochSnapshot {
1351 fn new(committee: Arc<NonEpochCommittee>, da_committee: Arc<DaCommittee>) -> Self {
1352 Self {
1353 inner: Arc::new(NonEpochSnapshotInner {
1354 committee,
1355 da_committee,
1356 }),
1357 }
1358 }
1359}
1360
1361impl NonEpochMembershipSnapshot<SeqTypes> for NonEpochSnapshot {
1362 type Error = EpochCommitteesError;
1363
1364 fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<SeqTypes>> + Send + '_ {
1365 self.inner.committee.stake_table.iter()
1366 }
1367
1368 fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<SeqTypes>> + Send + '_ {
1369 self.inner.da_committee.committee.iter()
1370 }
1371
1372 fn committee_members(
1373 &self,
1374 _: ViewNumber,
1375 ) -> impl ExactSizeIterator<Item = &PubKey> + Send + '_ {
1376 self.inner.committee.indexed_stake_table.keys()
1377 }
1378
1379 fn da_committee_members(
1380 &self,
1381 _: ViewNumber,
1382 ) -> impl ExactSizeIterator<Item = &PubKey> + Send + '_ {
1383 self.inner.da_committee.indexed_committee.keys()
1384 }
1385
1386 fn stake(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
1387 self.inner.committee.indexed_stake_table.get(key).cloned()
1388 }
1389
1390 fn da_stake(&self, key: &PubKey) -> Option<PeerConfig<SeqTypes>> {
1391 self.inner.da_committee.indexed_committee.get(key).cloned()
1392 }
1393
1394 fn has_stake(&self, key: &PubKey) -> bool {
1395 self.stake(key)
1396 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
1397 .unwrap_or_default()
1398 }
1399
1400 fn has_da_stake(&self, key: &PubKey) -> bool {
1401 self.da_stake(key)
1402 .map(|x| x.stake_table_entry.stake() > U256::ZERO)
1403 .unwrap_or_default()
1404 }
1405
1406 fn lookup_leader(&self, view: ViewNumber) -> Result<PubKey, Self::Error> {
1407 let leaders = &self.inner.committee.eligible_leaders;
1408 if leaders.is_empty() {
1409 return Err(EpochCommitteesError::LeaderLookupError);
1410 }
1411 let index = *view as usize % leaders.len();
1412 Ok(PubKey::public_key(&leaders[index].stake_table_entry))
1413 }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use std::sync::{
1419 Arc,
1420 atomic::{AtomicBool, AtomicUsize, Ordering},
1421 };
1422
1423 use committable::Committable;
1424 use hotshot_query_service::testing::mocks::MOCK_UPGRADE;
1425 use hotshot_types::{
1426 ValidatorConfig,
1427 traits::{BlockPayload, block_contents::BlockHeader},
1428 };
1429 use tokio::{task::JoinSet, time::Duration};
1430
1431 use super::*;
1432 use crate::{NodeState, Payload, Transaction};
1433
1434 const TEST_DURATION: Duration = Duration::from_secs(5);
1438
1439 fn build_committees(num_peers: u64) -> EpochCommittees {
1440 let peers: Vec<PeerConfig<SeqTypes>> = (0..num_peers)
1441 .map(|i| {
1442 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
1443 [42u8; 32],
1444 i,
1445 U256::from(100),
1446 true,
1447 )
1448 .public_config()
1449 })
1450 .collect();
1451 EpochCommittees::new_stake(peers.clone(), peers, None, Fetcher::mock(), 100u64)
1452 }
1453
1454 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1468 async fn concurrent_reads_during_mutations() {
1469 let committees = build_committees(8);
1470 committees.set_first_epoch(EpochNumber::new(1), [0u8; 32]);
1471
1472 {
1477 let mut inner = committees.inner.write();
1478 let template = inner
1479 .epoch_committee(EpochNumber::genesis())
1480 .expect("genesis committee exists")
1481 .clone();
1482 for e in 2..6 {
1483 inner.put_epoch_committee(EpochNumber::new(e), template.clone());
1484 }
1485 }
1486
1487 let stop = Arc::new(AtomicBool::new(false));
1488 let mut tasks = JoinSet::new();
1489
1490 for _ in 0..8 {
1491 let c = committees.clone();
1492 let stop = Arc::clone(&stop);
1493 tasks.spawn(async move {
1494 let stable = EpochNumber::new(1);
1495 let mutating = EpochNumber::new(3);
1496 let view = ViewNumber::new(0);
1497 while !stop.load(Ordering::Relaxed) {
1498 let stable_snap = c.snapshot(stable).expect("stable snapshot");
1504 let len = stable_snap.stake_table().len();
1505 assert_eq!(len, stable_snap.total_nodes());
1506 let leader = stable_snap.lookup_leader(view).expect("leader");
1507 assert!(
1508 stable_snap.committee_members(view).any(|p| p == &leader),
1509 "leader {leader:?} not in committee_members for stable epoch",
1510 );
1511 assert_eq!(c.first_epoch(), Some(stable));
1512
1513 let _ = c.snapshot(mutating);
1519 if let Some(s) = c.snapshot(mutating) {
1520 let _ = s.lookup_leader(view);
1521 }
1522 tokio::task::yield_now().await;
1523 }
1524 });
1525 }
1526
1527 tasks.spawn({
1531 let c = committees.clone();
1532 let stop = Arc::clone(&stop);
1533 async move {
1534 let extra: Vec<PeerConfig<SeqTypes>> = (0..3)
1535 .map(|i| {
1536 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
1537 [99u8; 32],
1538 i,
1539 U256::from(50),
1540 true,
1541 )
1542 .public_config()
1543 })
1544 .collect();
1545 let mut i: u64 = 0;
1546 while !stop.load(Ordering::Relaxed) {
1547 c.add_drb_result(EpochNumber::new(2 + (i % 4)), [(i % 256) as u8; 32]);
1550 c.add_drb_result(EpochNumber::new(10_000 + i), [0xAB; 32]);
1553 if i.is_multiple_of(50) {
1554 c.add_da_committee(i.into(), extra.clone());
1555 }
1556 if i.is_multiple_of(16) {
1557 tokio::task::yield_now().await;
1558 }
1559 i += 1;
1560 }
1561 }
1562 });
1563
1564 tokio::time::sleep(TEST_DURATION).await;
1565 stop.store(true, Ordering::Relaxed);
1566 while let Some(res) = tasks.join_next().await {
1567 res.expect("task panicked");
1568 }
1569 }
1570
1571 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1583 async fn set_first_epoch_is_atomic() {
1584 fn snapshot(c: &EpochCommittees, e: EpochNumber) -> Snapshot {
1587 let inner = c.inner.read();
1588 Snapshot {
1589 first_epoch: inner.first_epoch,
1590 state_e: inner.snapshots.contains_key(&e),
1591 state_e1: inner.snapshots.contains_key(&(e + 1)),
1592 rand_e: inner
1593 .snapshots
1594 .get(&e)
1595 .is_some_and(|s| s.inner.randomized.is_some()),
1596 rand_e1: inner
1597 .snapshots
1598 .get(&(e + 1))
1599 .is_some_and(|s| s.inner.randomized.is_some()),
1600 }
1601 }
1602
1603 #[derive(Debug)]
1604 struct Snapshot {
1605 first_epoch: Option<EpochNumber>,
1606 state_e: bool,
1607 state_e1: bool,
1608 rand_e: bool,
1609 rand_e1: bool,
1610 }
1611
1612 let target = EpochNumber::new(10);
1613
1614 let test_start = tokio::time::Instant::now();
1619 let mut round: u64 = 0;
1620 while test_start.elapsed() < TEST_DURATION {
1621 let committees = build_committees(4);
1622 let stop = Arc::new(AtomicBool::new(false));
1623 let post_observations = Arc::new(AtomicUsize::new(0));
1624
1625 let reader = {
1626 let c = committees.clone();
1627 let stop = Arc::clone(&stop);
1628 let post = Arc::clone(&post_observations);
1629 tokio::spawn(async move {
1630 while !stop.load(Ordering::Relaxed) {
1631 let s = snapshot(&c, target);
1632 match s.first_epoch {
1633 None => assert!(
1634 !s.state_e && !s.state_e1 && !s.rand_e && !s.rand_e1,
1635 "torn snapshot: first_epoch=None but some target state present: \
1636 {s:?}",
1637 ),
1638 Some(e) => {
1639 assert_eq!(e, target, "only target is ever set");
1640 assert!(
1641 s.state_e && s.state_e1 && s.rand_e && s.rand_e1,
1642 "torn snapshot: first_epoch=Some but some target state \
1643 missing: {s:?}",
1644 );
1645 post.fetch_add(1, Ordering::Relaxed);
1646 },
1647 }
1648 tokio::task::yield_now().await;
1649 }
1650 })
1651 };
1652
1653 tokio::time::sleep(Duration::from_millis(2)).await;
1655 committees.set_first_epoch(target, [(round as u8) ^ 0xA5; 32]);
1656
1657 let deadline = tokio::time::Instant::now() + Duration::from_millis(200);
1660 while tokio::time::Instant::now() < deadline
1661 && post_observations.load(Ordering::Relaxed) == 0
1662 {
1663 tokio::task::yield_now().await;
1664 }
1665
1666 stop.store(true, Ordering::Relaxed);
1667 reader.await.expect("reader panicked");
1668 assert!(
1669 post_observations.load(Ordering::Relaxed) > 0,
1670 "round {round}: reader never observed post-set state",
1671 );
1672 round += 1;
1673 }
1674 assert!(round > 0, "test loop never executed a round");
1675 }
1676
1677 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1683 async fn concurrent_add_drb_result_same_epoch() {
1684 let committees = build_committees(4);
1685 let epoch = EpochNumber::new(1);
1686 committees.set_first_epoch(epoch, [0u8; 32]);
1689
1690 let stop = Arc::new(AtomicBool::new(false));
1691 let writes = Arc::new(AtomicUsize::new(0));
1692 let lookups = Arc::new(AtomicUsize::new(0));
1693
1694 let mut writers = JoinSet::new();
1695 let mut readers = JoinSet::new();
1696
1697 for _ in 0..4 {
1701 let c = committees.clone();
1702 let stop = Arc::clone(&stop);
1703 let lookups = Arc::clone(&lookups);
1704 readers.spawn(async move {
1705 let view = ViewNumber::new(0);
1706 while !stop.load(Ordering::Relaxed) {
1707 c.snapshot(epoch)
1708 .expect("snapshot")
1709 .lookup_leader(view)
1710 .expect("randomized committee must remain present once set");
1711 lookups.fetch_add(1, Ordering::Relaxed);
1712 tokio::task::yield_now().await;
1713 }
1714 });
1715 }
1716
1717 for tid in 0..8u8 {
1721 let c = committees.clone();
1722 let stop = Arc::clone(&stop);
1723 let writes = Arc::clone(&writes);
1724 writers.spawn(async move {
1725 let mut i: u64 = 0;
1726 while !stop.load(Ordering::Relaxed) {
1727 let mut drb = [tid; 32];
1728 drb[0] = (i & 0xFF) as u8;
1729 c.add_drb_result(epoch, drb);
1730 writes.fetch_add(1, Ordering::Relaxed);
1731 if i.is_multiple_of(16) {
1732 tokio::task::yield_now().await;
1733 }
1734 i += 1;
1735 }
1736 });
1737 }
1738
1739 tokio::time::sleep(TEST_DURATION).await;
1740 stop.store(true, Ordering::Relaxed);
1741 while let Some(res) = writers.join_next().await {
1742 res.expect("writer panicked");
1743 }
1744 while let Some(res) = readers.join_next().await {
1745 res.expect("reader panicked");
1746 }
1747
1748 assert!(writes.load(Ordering::Relaxed) > 0, "writers never advanced",);
1749 assert!(
1750 lookups.load(Ordering::Relaxed) > 0,
1751 "readers never observed the randomized committee",
1752 );
1753 let snap = committees
1754 .snapshot(epoch)
1755 .expect("randomized committee must survive concurrent writes");
1756 assert!(snap.has_drb(), "randomized committee must remain present");
1757 let view = ViewNumber::new(0);
1758 let _leader = snap
1759 .lookup_leader(view)
1760 .expect("lookup_leader succeeds when randomized committee is present");
1761 }
1762
1763 async fn build_epoch_root_header() -> Header {
1767 let instance = NodeState::mock_v2();
1768 let tx = Transaction::of_size(10);
1769 let (payload, _) = Payload::from_transactions([tx], &instance.genesis_state, &instance)
1770 .await
1771 .expect("payload");
1772 let metadata = payload.ns_table().clone();
1773 let header = Header::genesis(&instance, payload, &metadata, MOCK_UPGRADE.base);
1774 match header {
1775 Header::V2(mut h) => {
1776 h.height = 95;
1777 Header::V2(h)
1778 },
1779 other => panic!("expected V2 header from NodeState::mock_v2, got {other:?}"),
1780 }
1781 }
1782
1783 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1797 async fn add_epoch_root_is_atomic() {
1798 let header = build_epoch_root_header().await;
1799 let target = EpochNumber::new(3);
1800
1801 fn snapshot(c: &EpochCommittees, e: EpochNumber) -> (bool, bool) {
1802 let inner = c.inner.read();
1803 let header_set = inner
1804 .snapshots
1805 .get(&e)
1806 .map(|s| s.inner.committee.header.is_some())
1807 .unwrap_or(false);
1808 let all_validators_present = inner.all_validators.contains_key(&e);
1809 (header_set, all_validators_present)
1810 }
1811
1812 let test_start = tokio::time::Instant::now();
1816 let mut round: u64 = 0;
1817 while test_start.elapsed() < TEST_DURATION {
1818 let committees = build_committees(4);
1819
1820 {
1825 let mut inner = committees.inner.write();
1826 let template = inner
1827 .epoch_committee(EpochNumber::genesis())
1828 .expect("genesis committee exists");
1829 let prefilled = EpochCommittee {
1830 block_reward: Some(RewardAmount::default()),
1831 stake_table_hash: Some(StakeTableState::default().commit()),
1832 header: None,
1833 eligible_leaders: template.eligible_leaders.clone(),
1834 stake_table: template.stake_table.clone(),
1835 validators: template.validators.clone(),
1836 address_mapping: template.address_mapping.clone(),
1837 };
1838 inner.put_epoch_committee(target, Arc::new(prefilled));
1839 }
1840
1841 let stop = Arc::new(AtomicBool::new(false));
1842 let post = Arc::new(AtomicUsize::new(0));
1843
1844 let reader = {
1845 let c = committees.clone();
1846 let stop = Arc::clone(&stop);
1847 let post = Arc::clone(&post);
1848 tokio::spawn(async move {
1849 while !stop.load(Ordering::Relaxed) {
1850 match snapshot(&c, target) {
1851 (false, false) => {}, (true, true) => {
1853 post.fetch_add(1, Ordering::Relaxed);
1854 },
1855 torn => panic!(
1856 "round {round}: torn snapshot for epoch {target}: header_set={}, \
1857 all_validators_present={}",
1858 torn.0, torn.1,
1859 ),
1860 }
1861 tokio::task::yield_now().await;
1862 }
1863 })
1864 };
1865
1866 tokio::time::sleep(Duration::from_millis(2)).await;
1869 let coordinator = EpochMembershipCoordinator::<SeqTypes>::new(
1870 committees.clone(),
1871 *committees.epoch_height(),
1872 &hotshot_example_types::storage_types::TestStorage::default(),
1873 );
1874 coordinator
1875 .add_epoch_root(header.clone())
1876 .await
1877 .expect("add_epoch_root should succeed for the prefilled state");
1878
1879 let deadline = tokio::time::Instant::now() + Duration::from_millis(200);
1880 while tokio::time::Instant::now() < deadline && post.load(Ordering::Relaxed) == 0 {
1881 tokio::task::yield_now().await;
1882 }
1883 stop.store(true, Ordering::Relaxed);
1884 reader.await.expect("reader panicked");
1885 assert!(
1886 post.load(Ordering::Relaxed) > 0,
1887 "round {round}: reader never observed post-state",
1888 );
1889 round += 1;
1890 }
1891 assert!(round > 0, "test loop never executed a round");
1892 }
1893
1894 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1900 async fn add_da_committee_rebuilds_affected_snapshots() {
1901 fn da_keys(c: &EpochCommittees, e: EpochNumber) -> Vec<PubKey> {
1902 c.snapshot(e)
1903 .expect("snapshot")
1904 .da_stake_table()
1905 .map(|p| PubKey::public_key(&p.stake_table_entry))
1906 .collect()
1907 }
1908
1909 let committees = build_committees(4);
1913 committees.set_first_epoch(EpochNumber::new(2), [0u8; 32]);
1914
1915 let initial_e2 = da_keys(&committees, EpochNumber::new(2));
1916 let initial_e3 = da_keys(&committees, EpochNumber::new(3));
1917
1918 let new_da: Vec<PeerConfig<SeqTypes>> = (0..2)
1919 .map(|i| {
1920 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
1921 [123u8; 32],
1922 i,
1923 U256::from(50),
1924 true,
1925 )
1926 .public_config()
1927 })
1928 .collect();
1929 let new_da_keys: Vec<PubKey> = new_da
1930 .iter()
1931 .map(|p| PubKey::public_key(&p.stake_table_entry))
1932 .collect();
1933 assert_ne!(
1934 new_da_keys, initial_e2,
1935 "test setup: new DA must differ from initial"
1936 );
1937
1938 committees.add_da_committee(EpochNumber::new(2), new_da);
1943
1944 assert_eq!(
1945 da_keys(&committees, EpochNumber::new(2)),
1946 new_da_keys,
1947 "snapshot(2) must reflect the new DA",
1948 );
1949 assert_eq!(
1950 da_keys(&committees, EpochNumber::new(3)),
1951 new_da_keys,
1952 "snapshot(3) must reflect the new DA",
1953 );
1954 assert_eq!(
1955 da_keys(&committees, EpochNumber::new(1)),
1956 initial_e2,
1957 "snapshot(1) lies before first_epoch=2 and must keep the bootstrap DA",
1958 );
1959 assert_eq!(initial_e2, initial_e3);
1964 }
1965
1966 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1970 async fn add_da_committee_layered_does_not_rebuild_earlier_ranges() {
1971 fn da_keys(c: &EpochCommittees, e: EpochNumber) -> Vec<PubKey> {
1972 c.snapshot(e)
1973 .expect("snapshot")
1974 .da_stake_table()
1975 .map(|p| PubKey::public_key(&p.stake_table_entry))
1976 .collect()
1977 }
1978
1979 let committees = build_committees(4);
1980 committees.set_first_epoch(EpochNumber::new(1), [0u8; 32]);
1981
1982 {
1986 let mut inner = committees.inner.write();
1987 let template = inner
1988 .epoch_committee(EpochNumber::genesis())
1989 .expect("genesis committee exists")
1990 .clone();
1991 for e in 3..6 {
1992 inner.put_epoch_committee(EpochNumber::new(e), template.clone());
1993 }
1994 }
1995
1996 let da_b: Vec<PeerConfig<SeqTypes>> = (0..2)
1997 .map(|i| {
1998 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
1999 [200u8; 32],
2000 i,
2001 U256::from(50),
2002 true,
2003 )
2004 .public_config()
2005 })
2006 .collect();
2007 let da_b_keys: Vec<PubKey> = da_b
2008 .iter()
2009 .map(|p| PubKey::public_key(&p.stake_table_entry))
2010 .collect();
2011
2012 let da_c: Vec<PeerConfig<SeqTypes>> = (0..2)
2013 .map(|i| {
2014 ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
2015 [201u8; 32],
2016 i,
2017 U256::from(50),
2018 true,
2019 )
2020 .public_config()
2021 })
2022 .collect();
2023 let da_c_keys: Vec<PubKey> = da_c
2024 .iter()
2025 .map(|p| PubKey::public_key(&p.stake_table_entry))
2026 .collect();
2027
2028 committees.add_da_committee(EpochNumber::new(5), da_c);
2031 committees.add_da_committee(EpochNumber::new(3), da_b);
2032
2033 assert_eq!(da_keys(&committees, EpochNumber::new(3)), da_b_keys);
2034 assert_eq!(da_keys(&committees, EpochNumber::new(4)), da_b_keys);
2035 assert_eq!(
2036 da_keys(&committees, EpochNumber::new(5)),
2037 da_c_keys,
2038 "epoch 5 must keep C — it is outside B's range",
2039 );
2040 }
2041}