Skip to main content

espresso_types/v0/impls/
committee.rs

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
48/// Number of recent per-epoch stake tables kept in memory.
49///
50/// `reload_stake` loads this many epochs from persistence on startup and
51/// `prune_epochs` evicts anything older as the tip advances, so a
52/// long-running node holds the same window as a freshly restarted one.
53/// Evicted epochs are recovered on demand via the membership coordinator's
54/// catchup.
55pub const RECENT_STAKE_TABLES_LIMIT: u64 = 20;
56
57/// Default for how long `load_stake_table` may spend on each of its two
58/// phases.
59///
60/// Catchup calls `load_stake_table` once per epoch it walks and bounds each
61/// call by its own `DEFAULT_CATCHUP_TIMEOUT` (300 s), so this is the tighter
62/// of the two and is what a stalled store actually costs per epoch. Neither
63/// bounds a walk as a whole.
64const DEFAULT_STORAGE_READ_TIMEOUT: Duration = Duration::from_secs(60);
65
66/// Type to describe DA and Stake memberships.
67#[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    /// The stake tables of views before the first epoch.
81    ///
82    /// Built once at constructor time. `add_da_committee` does not change it.
83    genesis: Arc<GenesisStakeTables<SeqTypes>>,
84
85    /// The DA committee of the epochs that no entry of `da_committees` covers.
86    genesis_da_committee: Arc<DaCommittee>,
87
88    /// Per-epoch snapshots.
89    ///
90    /// Each mutator rebuilds the snapshot for its affected epoch(s).
91    snapshots: BTreeMap<EpochNumber, EpochSnapshot>,
92
93    /// Holds the full validator candidate sets temporarily, until we store them.
94    #[cfg_attr(not(feature = "node"), allow(dead_code))]
95    all_validators: BTreeMap<EpochNumber, RegisteredValidatorMap>,
96
97    /// DA committees, indexed by the first epoch in which they apply.
98    ///
99    /// Kept separate from `snapshots` because the lookup is a range query.
100    da_committees: BTreeMap<EpochNumber, Arc<DaCommittee>>,
101
102    /// Epochs below this have been evicted by `prune_epochs`.
103    ///
104    /// Only ever moves forward.
105    prune_cutoff: EpochNumber,
106
107    first_epoch: EpochNumber,
108
109    /// Fixed block reward (used only in V3).
110    ///
111    /// Starting from V4, block reward is dynamic
112    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/// Holds Stake table and da stake
122#[derive(Debug)]
123struct EpochCommittee {
124    /// The nodes eligible for leadership.
125    ///
126    /// NOTE: This is currently a hack because the DA leader needs to be the quorum
127    /// leader but without voting rights.
128    eligible_leaders: Vec<PeerConfig<SeqTypes>>,
129    /// Keys for nodes participating in the network
130    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    /// Find the most recent stake-table entry for `key`.
201    ///
202    /// Scanns loaded epochs from highest to lowest and falling back to the
203    /// genesis bootstrap committee.
204    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    /// Fetch the fixed block reward and update it if its None.
224    /// We used a fixed block reward for version v3
225    /// Version v4 uses the dynamic block reward
226    /// Assumes the stake table contract proxy address does not change
227    #[cfg(feature = "node")]
228    async fn fetch_and_update_fixed_block_reward(
229        &self,
230        epoch: EpochNumber,
231    ) -> anyhow::Result<RewardAmount> {
232        // Ensure there is only one `fetch_and_update_fixed_block_reward` at a time:
233        let _guard = self.update_fixed_block_reward_lock.lock().await;
234
235        // Clippy claims "temporary with significant `Drop` in `if let`
236        // scrutinee will live until the end of the `if let` expression",
237        // however this is incorrect. The 2024 edition changed the drop
238        // scope of `if-let` expressions:
239        //
240        // https://doc.rust-lang.org/edition-guide/rust-2024/temporary-if-let-scope.html
241        //
242        // The read guard is dropped before `else`.
243        #[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    /// Calculates the dynamic block reward for a given block header within an epoch.
263    ///
264    /// The reward is based on a dynamic inflation rate computed from the current stake ratio (p),
265    /// where `p = total_stake / total_supply`. The inflation function R(p) is defined piecewise:
266    /// - If `p <= 0.01`: R(p) = 0.03 / sqrt(2 * 0.01)
267    /// - Else: R(p) = 0.03 / sqrt(2 * p)
268    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        // Return early if previous epoch is not the first two epochs
285        // and we don't have the stake table.
286        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        // Calculate total stake across all active validators
296        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        // Calculate average block time over the last epoch
303        let curr_ts = header.timestamp_millis_internal();
304        debug!(?epoch, "curr_ts={curr_ts:?}");
305
306        // If the node starts from epoch version V4, there is no previous epoch root available.
307        // In this case, we assume a fixed average block time of 2000 milli seconds (2s)
308        // for the first epoch in which reward id distributed
309        let average_block_time_ms = if previous_epoch <= first_epoch + 1 {
310            ASSUMED_BLOCK_TIME_SECONDS as u64 * 1000 // 2 seconds in milliseconds
311        } else {
312            // We are calculating rewards for epoch `epoch`, so the current epoch should be `epoch - 2`.
313            // We need to calculate the average block time for the current epoch, so we need to know
314            // the previous epoch root which is stored with epoch `epoch - 1`, i.e. the next epoch.
315            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    /// This function just returns the stored block reward in epoch committee
360    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    /// Get the index of a validator's BLS key in the epoch's stake table.
368    /// Returns None if the validator is not in the stake table for this epoch.
369    ///
370    /// The index corresponds to the position in the `leader_counts` array in V6 headers.
371    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    /// The membership of a chain whose epochs start at `first_epoch`.
401    ///
402    /// `first_epoch` and `first_epoch + 1` run on the genesis stake table,
403    /// `committee_members`, with the initial DRB result.
404    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        // For each member, get the stake table entry
413        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        // For each member, get the stake table entry
421        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        // Index the stake table by public key
428        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        // TODO: remove this, workaround for hotshot asking for stake tables from epoch 1
472        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    /// Override how long `load_stake_table` may spend on each of its two
505    /// phases. Mostly for tests.
506    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        // Load the `limit` latest stored stake tables
524        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            // `new` seeded the epochs up to `first_epoch + 1` with the genesis stake table.
546            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    /// Get root leaf header for a given epoch
557    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    /// Run `epoch` and `epoch + 1` on the genesis stake table with the initial DRB result.
569    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        // Build snapshots for `epoch` and `epoch + 1` with the genesis
582        // stake table and the initial DRB result.
583        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
614/// returns the block reward for the given epoch.
615///
616/// Reward depends on the epoch root header version:
617/// V3: Returns the fixed block reward as V3 only supports fixed reward
618/// >= V4 : Returns the dynamic block reward
619///
620/// It also attempts catchup for the root header if not present in the committee,
621/// and also for the stake table of the previous epoch
622/// before computing the dynamic block reward
623pub 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        // Return early if committee has a reward already
640        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 the previous epoch is not in the first two epochs,
684    // there should be a stake table for it
685    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    /// Load the committee for `epoch` from storage.
715    ///
716    /// Everything persisted for the epoch is restored: the stake table, the
717    /// epoch root header, and the randomized committee (rebuilt from the DRB
718    /// result).
719    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            // Ensure there is only one `load_stake_table` at a time:
725            let guard = self.load_from_storage_lock.lock().await;
726            // Check if someone else won the race while we queued.
727            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            // Someone else loaded the epoch while we queued for the first lock.
737            Ok(None) => return true,
738            Err(_) => {
739                // A timeout can also mean a won race.
740                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        // Check if someone else won the race:
752        if self.inner.read().snapshots.contains_key(&epoch) {
753            return true;
754        }
755        // Bound the reads so a stalled query cannot pin the locks held above.
756        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    /// Adds the epoch committee and block reward for a given epoch,
832    /// either by fetching from L1 or using local state if available.
833    /// It also calculates and stores the block reward based on header version.
834    #[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        // Fetching stake table events for the new epoch requires an L1 client.
841        unimplemented!("add_epoch_root requires the node feature");
842    }
843
844    /// Adds the epoch committee and block reward for a given epoch,
845    /// either by fetching from L1 or using local state if available.
846    /// It also calculates and stores the block reward based on header version.
847    #[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        // Stored ahead of the early return below so a failed write is retried by the next
870        // `add_epoch_root` for this epoch. Downstream of `put_epoch_committee` the committee
871        // is complete, which arms that early return and also stops catchup from re-entering
872        // `fetch_stake_table`, so one dropped error would leave the header unpersisted.
873        //
874        // Keyed by `epoch`, unlike the previous-epoch stake table stored further down, and
875        // not gated behind the stake table consistency check, which doesn't apply here.
876        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        // Update the chain config if the block header contains a newer one.
889        self.fetcher
890            .update_chain_config(&block_header)
891            .await
892            .map_err(Self::Error::Fetcher)?;
893
894        let mut block_reward = None;
895        // Even if the current header is the root of the epoch which falls in the post upgrade
896        // we use the fixed block reward
897        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        // TODO: If the stake table is missing should it be fetched by an unbounded
910        // number of tasks?
911
912        // If the epoch committee:
913        // - exists and has a header stake table hash and block reward, return early.
914        // - exists without a reward, reuse validators and update reward.
915        // and fetch from L1 if the stake table hash is missing.
916        // - doesn't exist, fetch it from L1.
917        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                    // if stake table hash is missing then recalculate from events
940                    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 we are past the DRB+Header upgrade point,
973        // and don't have block reward
974        // calculate the dynamic block reward based on validator info and block header.
975        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 is the epoch prior to `epoch`,
1005            // or the epoch immediately succeeding the block header
1006            previous_epoch = EpochNumber::new(epoch.saturating_sub(1));
1007            previous_committee = inner.epoch_committee(previous_epoch).cloned();
1008            // garbage collect the validator set
1009            inner.all_validators = inner.all_validators.split_off(&previous_epoch);
1010            // extract `all_validators` for the previous epoch
1011            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        // we store the information from the previous epoch's in-memory committeee
1021        // if the decided stake_table_hash is consistent with what we get
1022        //
1023        // in principle this is unnecessary and we could've stored these right away,
1024        // without offsetting the epoch. but the intention is to catch L1 provider issues
1025        // if there is a mismatch
1026        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        // the root block may not exist anywhere yet
1073        // Each attempt tries all peers once
1074        // `retry` only scales the per peer timeout
1075        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        // Try to retrieve the DRB result from an existing snapshot's randomized committee.
1099        if let Some(snap) = self.snapshot(epoch)
1100            && let Some(rand) = &snap.inner.randomized
1101        {
1102            return Ok(rand.drb_result());
1103        }
1104
1105        // Otherwise, we try to fetch the epoch root leaf
1106        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        // The DA committee inserted at `first_epoch` applies to every epoch
1185        // up to (but not including) the next `da_committees` key. Snapshots
1186        // for those epochs were built with whatever DA was current at the
1187        // time and must be rebuilt so reads of `da_stake_table()` etc.
1188        // reflect the new committee.
1189        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    /// The DA committee that applies to `epoch`, or the genesis DA committee
1243    /// when `epoch` is `None` or no explicit DA committee covers it.
1244    fn resolve_da_committee(&self, epoch: Option<EpochNumber>) -> Arc<DaCommittee> {
1245        if let Some(e) = epoch {
1246            // The greatest key ≤ `e` is the DA committee that applies.
1247            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    /// Borrow the per-epoch `EpochCommittee` if loaded.
1258    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    /// Rebuild (or insert) the snapshot for `epoch` carrying forward
1282    /// `randomized` from any existing snapshot for that epoch.
1283    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    /// Rebuild the snapshot for `epoch` with a new randomized committee,
1297    /// carrying forward the existing committee/da. No-op if no snapshot
1298    /// for `epoch` exists yet.
1299    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    /// Evict snapshots more than [`RECENT_STAKE_TABLES_LIMIT`] epochs below
1323    /// `added`.
1324    ///
1325    /// The cutoff only ever moves forward: `add_epoch_root` also runs for old
1326    /// epochs during catchup, and a lagging insert must neither evict newer
1327    /// state nor evict itself before its readers had a chance to observe it.
1328    ///
1329    /// `da_committees` is exempt: it holds one entry per configured DA
1330    /// committee (seeded from config at startup and on upgrade certificates),
1331    /// so it is bounded, and `resolve_da_committee`'s greatest-key-below
1332    /// lookup needs the full history for re-inserted old epochs.
1333    #[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        // The snapshots for `first_epoch` and `first_epoch + 1` (seeded by
1341        // `new`) are never evicted:
1342        let keep = self.first_epoch + 1u64;
1343        self.snapshots.retain(|e, _| *e <= keep || *e >= cutoff);
1344    }
1345}
1346
1347/// A consistent per-epoch view of `EpochCommittees`.
1348///
1349/// Returned by [`Membership::snapshot`].
1350#[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    /// Index of `key` in this epoch's stake table, if present.
1386    pub fn validator_index(&self, key: &PubKey) -> Option<usize> {
1387        self.inner.committee.stake_table.get_index_of(key)
1388    }
1389
1390    /// The full validator record (account, stake, delegators, etc.) for `key`.
1391    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    /// Returns the leader's public key for a given view number in this epoch.
1473    ///
1474    /// # Errors
1475    ///
1476    /// Returns `LeaderLookupError` if the snapshot's epoch is before
1477    /// `first_epoch`, the randomized committee is missing, or the epoch has
1478    /// no eligible leader.
1479    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        // `select_randomized_leader` panics on an empty committee. `new` seeds
1497        // one when the genesis stake table has no staked member.
1498        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    /// Wall-clock target each concurrency test runs for. Long enough to
1530    /// catch flaky races that one-shot tests would miss; short enough to
1531    /// be tolerable in CI.
1532    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    /// Persistence stub holding stake tables for epochs 7 and 8, plus a DRB
1558    /// result and an epoch root header for epoch 7 only. `stored_headers`
1559    /// records headers written via `store_epoch_root`, independent of
1560    /// `header`. `fail_next_store` makes the next `store_epoch_root` fail
1561    /// once, then clears itself.
1562    #[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    // `load_stake_table` materializes a snapshot from the persisted stake
1663    // table (e.g. after eviction by `prune_epochs`) without network catchup,
1664    // including the randomized committee and the epoch root header when
1665    // they are persisted.
1666    #[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        // Epoch 7 has a stake table, a DRB result and a root header in storage.
1683        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        // A second call is satisfied by the in-memory snapshot.
1690        assert!(committees.load_stake_table(seven).await);
1691        // Epoch 8 has a stake table but no DRB result and no root header.
1692        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        // Nothing persisted for epoch 9.
1698        assert!(!committees.load_stake_table(EpochNumber::new(9)).await);
1699    }
1700
1701    // A caller that times out queueing for the storage locks must still
1702    // answer `true` when the epoch was loaded by someone else while it
1703    // queued: `load_stake_table`'s contract is "is a snapshot available",
1704    // and a false here sends catchup to peers for an epoch already in
1705    // memory. The held lock models the persistence lock's other users (the
1706    // L1 event fetcher) keeping it busy past the lock-phase budget.
1707    #[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        // Hold the persistence lock for the whole test, well past the
1726        // lock-phase budget.
1727        let _busy = persistence.lock().await;
1728
1729        // The loader passes its entry checks (epoch 7 not loaded yet) and
1730        // parks queueing on the held persistence lock.
1731        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        // Give the loader time to run its entry checks and park; inserting
1737        // before it enters would satisfy the entry check and prove nothing.
1738        tokio::time::sleep(Duration::from_millis(100)).await;
1739
1740        // Another task wins the race: epoch 7's snapshot appears while the
1741        // loader is still queued.
1742        {
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    // `prune_epochs` keeps a bounded window behind the newest decided epoch
1761    // plus the first-epoch seeds (catchup anchors), and its cutoff never
1762    // regresses when an older epoch is (re-)inserted during catchup.
1763    #[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        // The seeds for `first_epoch` and `first_epoch + 1` survive, then
1779        // nothing until the cutoff.
1780        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        // A catchup insert below the cutoff is not evicted by its own
1792        // insertion; it goes once the tip advances past it.
1793        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    // Concurrent reads must not panic or deadlock while a writer drives
1801    // real mutations on the same `Inner` lock.
1802    //
1803    // Per-call invariants (within a single method invocation) are
1804    // checked. Cross-call invariants are *not*: each public method
1805    // takes its own short-lived lock, so a sequence of two read calls
1806    // observes two snapshots in time and the writer can run between
1807    // them. See the `EpochCommittees` doc-comment for the rationale.
1808    //
1809    // To make the contention real, we pre-populate `inner.state` for
1810    // several extra epochs so the writer's `add_drb_result` calls
1811    // actually take the write lock. Without this they would early-exit
1812    // on the missing-state branch and never contend.
1813    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1814    async fn concurrent_reads_during_mutations() {
1815        let committees = build_committees(8);
1816
1817        // Pre-populate snapshots for epochs 2..6 by cloning the genesis
1818        // committee. `add_drb_result(e)` is a no-op when no snapshot for
1819        // `e` exists, so without this the writer never takes the write
1820        // lock for those epochs.
1821        {
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                    // Stable epoch — the writer never touches
1844                    // `inner.state[1]` or `inner.randomized_committees[1]`
1845                    // (both were set by `new` before this
1846                    // loop and stay unchanged thereafter), so the
1847                    // assertions below hold even across separate snapshots.
1848                    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                    // Mutating epoch — the writer churns
1859                    // `randomized_committees[3]`. Just exercise the API
1860                    // path; the value can vary or be transiently absent
1861                    // between calls and that is the documented
1862                    // behaviour, not a bug.
1863                    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        // Writer driving real mutations against fields the readers see.
1873        // Loops until the test signals stop, so the contention window
1874        // matches `TEST_DURATION`.
1875        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                    // Pre-populated epochs 2..5 — these acquire the
1893                    // write lock and contend with reader read locks.
1894                    c.add_drb_result(EpochNumber::new(2 + (i % 4)), [(i % 256) as u8; 32]);
1895                    // Non-existent epoch — exercises the read-then-no-op
1896                    // branch of `add_drb_result` (read lock only).
1897                    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    // Many writer tasks hammer `add_drb_result` for the same epoch with
1917    // distinct DRBs. While they do, reader tasks call `lookup_leader`,
1918    // which must always succeed once the randomized committee is set —
1919    // the writer overwrites the entry but never removes it. After the
1920    // writers drain, the entry must still be present and queryable.
1921    #[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        // Readers: lookup_leader must always succeed for an epoch
1934        // whose randomized committee has been populated, even while it
1935        // is being overwritten.
1936        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        // Writers: each task overwrites the randomized committee with
1954        // a unique DRB derived from its task id and iteration. Loops
1955        // until stop so the contention window matches `TEST_DURATION`.
1956        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    // Build an epoch-root header for `epoch_height = 100`. Block height
2000    // 95 satisfies `is_epoch_root(95, 100)` and produces target epoch 3
2001    // when passed to `add_epoch_root`.
2002    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    // `add_epoch_root` mutates `state[epoch]` and `all_validators[epoch]`
2020    // inside one `inner.write()` block. A reader observing both fields
2021    // under one read lock must see them flip together: pre-state
2022    // (`state[epoch].header == None`, `all_validators[epoch]` absent)
2023    // or post-state (`header == Some(_)`, `all_validators[epoch]`
2024    // present). A torn snapshot in either direction would indicate the
2025    // mutations leaked outside the single write lock.
2026    //
2027    // We pre-populate `state[epoch]` with `block_reward` and
2028    // `stake_table_hash` set so `add_epoch_root` reuses the validators
2029    // already in memory and skips the L1 fetch (the mock fetcher
2030    // points at a non-existent RPC endpoint and would fail). This
2031    // still drives the inner.write block we want to verify.
2032    #[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        // Each round is one race attempt against `add_epoch_root`. Loop
2049        // for `TEST_DURATION` to widen the window for catching a torn
2050        // observation across the inner.write block.
2051        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            // Pre-populate the snapshot for `target` with block_reward and
2057            // stake_table_hash but no header. This lands `add_epoch_root`
2058            // on the second match arm (no L1 fetch) but still drives
2059            // the inner.write() mutation.
2060            {
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) => {}, // pre-state
2088                            (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            // Brief warmup so the reader is in its loop before the
2103            // mutation lands.
2104            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    /// Build an `add_epoch_root` fixture for `header`'s target epoch. The
2131    /// epoch committee is prefilled so the call reuses the cached validators
2132    /// and skips the L1 fetch; `cached_header` decides whether that committee
2133    /// is already complete, i.e. whether the call takes the early return.
2134    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    // `add_epoch_root` is the only path by which a V0_6-native node (which
2186    // never calls the legacy `decide_epoch_root`/`Storage::store_epoch_root`)
2187    // persists the epoch root header. Regression test for that header being
2188    // silently dropped. The header version is irrelevant: the store happens
2189    // before any version-dependent branch. A cached header makes the committee
2190    // complete, which takes the early return; that path must persist too.
2191    #[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    // A failed `store_epoch_root` must be retried by the next `add_epoch_root`
2212    // for the epoch. The first call leaves the in-memory committee complete, so
2213    // the second takes the early return; a store placed after that return would
2214    // leave the header unpersisted for good.
2215    #[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    // `add_da_committee` updates the per-epoch `da_committees` map and
2257    // must retroactively rebuild every loaded snapshot whose epoch
2258    // resolves to the new committee. Without the rebuild, snapshots
2259    // baked the old DA at construction time and reads would silently
2260    // observe stale data until `add_epoch_root` rebuilt them.
2261    #[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        // `EpochNumber::genesis()` is 1, so `new` keeps snapshots for
2272        // epochs 1 and 2, and seeds epochs 2 and 3 for `first_epoch = 2`,
2273        // all with the bootstrap DA.
2274        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        // Apply the new DA starting at epoch 2. Snapshot(2) and
2300        // snapshot(3) were seeded with the bootstrap DA; both must
2301        // observe the new DA after this call. Snapshot(1) lies before
2302        // `first_epoch=2`, so it stays on the bootstrap DA.
2303        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        // Sanity: the original epoch-2 and epoch-3 snapshots both used
2321        // the bootstrap DA before the call. If a future change to
2322        // `new` makes these diverge, this assertion will
2323        // catch it so the test setup can be revisited.
2324        assert_eq!(initial_e2, initial_e3);
2325    }
2326
2327    // A `add_da_committee` call with a `first_epoch` greater than every
2328    // entry in an existing layered map must only rebuild snapshots in
2329    // its own range, not earlier ranges owned by other committees.
2330    #[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        // Pre-populate snapshots for epochs 2..6 (`new` only seeds 1
2343        // and 2, but the range query needs more epochs to
2344        // exercise the layered case).
2345        {
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        // Insert C first at epoch 5, then B at epoch 3. Range [3, 5)
2389        // must be rebuilt with B; [5, ∞) must keep C.
2390        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    /// Genesis committee members: four with stake, and one without stake in
2413    /// the middle, which no stake table may hold. Returns all members and the
2414    /// staked ones.
2415    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    /// Asserts that `epoch` runs on exactly the `staked` committee, has a DRB
2428    /// result, and picks its leaders from `staked`.
2429    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    // The constructor seeds `first_epoch` and `first_epoch + 1` with the
2470    // genesis committee and the initial DRB result, also when the chain does
2471    // not start its epochs at genesis.
2472    #[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    // A snapshot rebuilt after construction must still know the first epoch:
2486    // without it, `lookup_leader` fails.
2487    #[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    /// `count` committee members, none of them with stake.
2511    fn unstaked_members(count: u64) -> Vec<PeerConfig<SeqTypes>> {
2512        (0..count).map(|i| peer(42, i, 0)).collect()
2513    }
2514
2515    // Without a staked member, the seeded first two epochs have no leader
2516    // candidates. A leader lookup there must fail, not panic.
2517    #[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    // `NodeState::mock()` builds its membership without a staked member.
2551    #[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    /// Persistence stub whose `load_latest_stake` returns `stakes`. Every
2571    /// other read finds nothing, and every write is dropped.
2572    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    // On restart, storage can hold stake tables for the first two epochs.
2660    // They must not replace the genesis committee those epochs run on. A
2661    // stored table for a later epoch is loaded, and its snapshot knows the
2662    // first epoch once its DRB result arrives.
2663    #[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    // The coordinator serves stake tables from the first epoch on, and
2714    // rejects epochs before it.
2715    #[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    /// The membership of `epoch`, read once with each lookup of the
2754    /// coordinator. Panics if a lookup fails.
2755    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    /// Asserts that every read of the quorum stake table through a `None`
2776    /// epoch gives the same answer as through `first_epoch`. `members` are the
2777    /// keys to check `stake` and `has_stake` for, next to one key that is not
2778    /// a member.
2779    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    /// Asserts that every read of the DA committee of `membership` gives
2844    /// `expected`, in order, and nothing else.
2845    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    // Data from before epochs were enabled is read with a `None` epoch. It
2888    // runs on the genesis stake table, as the first epoch does.
2889    #[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        // The genesis DA members are the genesis members, and `new` drops the
2902        // ones without stake.
2903        for (lookup, membership) in lookups(&coordinator, None) {
2904            assert_da_committee(&format!("{lookup}(None)"), &membership, &staked);
2905        }
2906    }
2907
2908    // Views before epochs were enabled were led in turn by the staked genesis
2909    // members, in genesis order, whatever their stake. Epochs pick leaders at
2910    // random instead.
2911    #[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    /// Genesis members in an order sorted neither by index nor by stake, with
2936    /// unequal stakes and two members without stake. Returns all members and
2937    /// the staked ones, in the given order.
2938    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    // VID shares and the light client stake table commitment depend on the
2960    // order of the stake table.
2961    #[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    // Before epochs were enabled, the DA committee was the genesis one. A DA
2991    // committee that starts at the first epoch does not change that.
2992    #[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    /// A fetcher whose persistence returns `stakes` as the latest stored
3017    /// stake tables.
3018    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        // The genesis DA members are all staked, so the genesis DA committee
3065        // is `staked` as well.
3066        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    // Without a staked member, the first epoch has no leader, so a `None`
3075    // epoch must not have one either.
3076    #[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    /// One member for each of `stakes`, in order, with keys from `seed`.
3109    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    /// A member with `stake`, given in hexadecimal, so it may not fit in a
3117    /// `u64`.
3118    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    /// Asserts the failure threshold and the DA success threshold of the
3133    /// first two epochs of a chain whose genesis stake table is `quorum` and
3134    /// whose genesis DA committee is `da`. Reads both from the epoch snapshot
3135    /// and from each lookup of the coordinator.
3136    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    // The failure threshold is one third of the quorum stake, rounded down,
3179    // plus one. The DA success threshold is two thirds of the DA stake,
3180    // rounded down, plus one. The quorum and DA totals differ in most cases,
3181    // so a threshold read from the wrong table fails the test.
3182    #[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    // Totals of at least `U256::MAX / 2` take another branch of the DA
3208    // success threshold, because doubling them overflows.
3209    #[rstest]
3210    // Quorum `U256::MAX`, DA `U256::MAX / 2 - 1`.
3211    #[case::da_just_below_half_of_max(
3212        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3213        "7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
3214        "5555555555555555555555555555555555555555555555555555555555555556",
3215        "5555555555555555555555555555555555555555555555555555555555555555"
3216    )]
3217    // Quorum `U256::MAX / 2 - 1`, DA `U256::MAX / 2`.
3218    #[case::da_half_of_max(
3219        "7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
3220        "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
3221        "2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
3222        "5555555555555555555555555555555555555555555555555555555555555556"
3223    )]
3224    // Quorum 1, DA `U256::MAX`.
3225    #[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}