Skip to main content

hotshot_types/
epoch_membership.rs

1use std::{
2    collections::{HashMap, hash_map::Entry},
3    sync::Arc,
4    time::Duration,
5};
6
7use alloy_primitives::U256;
8use committable::Commitment;
9use either::Either;
10use futures::future::{BoxFuture, FutureExt, Shared};
11use hotshot_utils::anytrace::{self, Wrap};
12use parking_lot::{Mutex, RwLock};
13use sha2::{Digest, Sha256};
14use tokio::{spawn, time::timeout};
15use tokio_util::sync::CancellationToken;
16use tracing::{debug, error, info, warn};
17use versions::DRB_FIX_VERSION;
18
19use crate::{
20    PeerConfig, PeerConnectInfo,
21    data::{BlockNumber, EpochNumber, Leaf2, ViewNumber},
22    drb::{
23        DRB_PROGRESS_LOAD_TIMEOUT, DrbDifficultySelectorFn, DrbInput, DrbResult, Heartbeat,
24        compute_drb_result,
25    },
26    traits::{
27        block_contents::BlockHeader,
28        election::{GenesisStakeTables, Membership, MembershipSnapshot},
29        node_implementation::NodeType,
30        signature_key::StakeTableEntryType,
31        storage::{
32            LoadDrbProgressFn, Storage, StoreDrbProgressFn, StoreDrbResultFn, load_drb_progress_fn,
33            store_drb_progress_fn, store_drb_result_fn,
34        },
35    },
36};
37
38type Result<T> = anytrace::Result<T>;
39
40/// One in-flight attempt, shared by every caller that asks for the same epoch
41/// while it runs. Whoever polls it drives it, so it outlives the task that
42/// started it as long as somebody is still waiting.
43type Attempt = Shared<BoxFuture<'static, Result<()>>>;
44
45/// The in-flight DRB computations, each with the token that stops it. An entry
46/// is the claim on its epoch: one computation runs per epoch, and the next may
47/// start only once the entry is gone.
48///
49/// When an external source supplies the DRB result for `epoch` (e.g. a decided
50/// leaf carrying `next_drb_result`), `supply_drb` fires the token so the local
51/// computation can stop early instead of grinding to completion.
52type DrbComputations = Arc<Mutex<HashMap<EpochNumber, CancellationToken>>>;
53
54/// The per-epoch snapshot type associated with `T::Membership`.
55type Snapshot<T> = <<T as NodeType>::Membership as Membership<T>>::Snapshot;
56
57/// The stake-table hash type associated with `T::Membership`'s per-epoch
58/// snapshot.
59type SnapshotStakeTableHash<T> = <Snapshot<T> as MembershipSnapshot<T>>::StakeTableHash;
60
61/// Struct to Coordinate membership catchup
62pub struct EpochMembershipCoordinator<TYPES: NodeType> {
63    membership: Arc<TYPES::Membership>,
64    catchup_map: Attempts,
65    stake_table_map: Attempts,
66    drb_computations: DrbComputations,
67    epoch_height: BlockNumber,
68    catchup_timeout: Duration,
69    store_drb_progress_fn: StoreDrbProgressFn,
70    load_drb_progress_fn: LoadDrbProgressFn,
71    store_drb_result_fn: StoreDrbResultFn,
72    drb_difficulty_selector: Arc<RwLock<Option<DrbDifficultySelectorFn>>>,
73}
74
75impl<TYPES: NodeType> Clone for EpochMembershipCoordinator<TYPES> {
76    fn clone(&self) -> Self {
77        Self {
78            membership: Arc::clone(&self.membership),
79            catchup_map: self.catchup_map.clone(),
80            stake_table_map: self.stake_table_map.clone(),
81            drb_computations: Arc::clone(&self.drb_computations),
82            epoch_height: self.epoch_height,
83            catchup_timeout: self.catchup_timeout,
84            store_drb_progress_fn: Arc::clone(&self.store_drb_progress_fn),
85            load_drb_progress_fn: Arc::clone(&self.load_drb_progress_fn),
86            store_drb_result_fn: self.store_drb_result_fn.clone(),
87            drb_difficulty_selector: Arc::clone(&self.drb_difficulty_selector),
88        }
89    }
90}
91
92impl<TYPES: NodeType> EpochMembershipCoordinator<TYPES> {
93    pub fn new<M, S, B>(membership: M, epoch_height: B, storage: &S) -> Self
94    where
95        M: Into<Arc<TYPES::Membership>>,
96        B: Into<BlockNumber>,
97        S: Storage<TYPES>,
98    {
99        Self {
100            membership: membership.into(),
101            catchup_map: Attempts::default(),
102            stake_table_map: Attempts::default(),
103            drb_computations: Arc::default(),
104            epoch_height: epoch_height.into(),
105            catchup_timeout: DEFAULT_CATCHUP_TIMEOUT,
106            store_drb_progress_fn: store_drb_progress_fn(storage.clone()),
107            load_drb_progress_fn: load_drb_progress_fn(storage.clone()),
108            store_drb_result_fn: store_drb_result_fn(storage.clone()),
109            drb_difficulty_selector: Arc::new(RwLock::new(None)),
110        }
111    }
112
113    pub fn epoch_height(&self) -> BlockNumber {
114        self.epoch_height
115    }
116
117    /// Override the bound on a single catchup step. Mostly for tests.
118    pub fn with_catchup_timeout(mut self, timeout: Duration) -> Self {
119        self.catchup_timeout = timeout;
120        self
121    }
122
123    /// Override the callback that persists a computed DRB result. For tests.
124    pub fn with_store_drb_result_fn(mut self, f: StoreDrbResultFn) -> Self {
125        self.store_drb_result_fn = f;
126        self
127    }
128
129    /// Get a reference to the membership
130    pub fn membership(&self) -> &TYPES::Membership {
131        &self.membership
132    }
133
134    /// Handles notifications that a new epoch root has been created.
135    pub async fn add_epoch_root(
136        &self,
137        header: TYPES::BlockHeader,
138    ) -> std::result::Result<(), <TYPES::Membership as Membership<TYPES>>::Error> {
139        self.membership.add_epoch_root(header, self).await
140    }
141
142    /// Gets the validated block header and epoch number of the epoch root
143    /// at the given block height.
144    pub async fn get_epoch_root(
145        &self,
146        epoch: EpochNumber,
147    ) -> std::result::Result<Leaf2<TYPES>, <TYPES::Membership as Membership<TYPES>>::Error> {
148        self.membership.get_epoch_root(epoch, self).await
149    }
150
151    /// Gets the DRB result for the given epoch.
152    pub async fn get_epoch_drb(
153        &self,
154        epoch: EpochNumber,
155    ) -> std::result::Result<DrbResult, <TYPES::Membership as Membership<TYPES>>::Error> {
156        self.membership.get_epoch_drb(epoch, self).await
157    }
158
159    /// Set the DRB difficulty selector
160    pub fn set_drb_difficulty_selector(&self, f: DrbDifficultySelectorFn) {
161        let mut drb_difficulty_selector_writer = self.drb_difficulty_selector.write();
162        *drb_difficulty_selector_writer = Some(f);
163    }
164
165    /// Get a Membership for a given Epoch, which is guaranteed to have a randomized stake
166    /// table for the given Epoch
167    pub fn membership_for_epoch(
168        &self,
169        maybe_epoch: Option<EpochNumber>,
170    ) -> Result<EpochMembership<TYPES>> {
171        let Some(epoch) = maybe_epoch else {
172            return Ok(self.genesis_membership());
173        };
174        let first_epoch = self.membership.first_epoch();
175        if epoch < first_epoch {
176            return Err(anytrace::error!(
177                "membership_for_epoch called with epoch {epoch:?} before first_epoch {first_epoch}"
178            ));
179        }
180        if let Some(snapshot) = self.membership.snapshot(epoch)
181            && snapshot.has_drb()
182        {
183            return Ok(EpochMembership {
184                coordinator: self.clone(),
185                snapshot: EpochMembershipSnapshot::Epoch { epoch, snapshot },
186            });
187        }
188        if self.spawn_catchup(epoch) {
189            Err(anytrace::warn!(
190                "Randomized stake table for epoch {epoch:?} unavailable. Starting catchup"
191            ))
192        } else {
193            Err(anytrace::warn!(
194                "Randomized stake table for epoch {epoch:?} unavailable. Catchup already in \
195                 progress"
196            ))
197        }
198    }
199
200    /// Get a Membership for a given Epoch, which is guaranteed to have a stake
201    /// table for the given Epoch
202    pub fn stake_table_for_epoch(&self, e: Option<EpochNumber>) -> Result<EpochMembership<TYPES>> {
203        let Some(epoch) = e else {
204            return Ok(self.genesis_membership());
205        };
206        let first_epoch = self.membership.first_epoch();
207        if epoch < first_epoch {
208            return Err(anytrace::error!(
209                "stake_table_for_epoch called with epoch {epoch:?} before first_epoch \
210                 {first_epoch}"
211            ));
212        }
213        if let Some(membership) = self.epoch_membership(epoch) {
214            return Ok(membership);
215        }
216        if self.spawn_catchup(epoch) {
217            Err(anytrace::warn!(
218                "Stake table for epoch {epoch:?} unavailable. Starting catchup"
219            ))
220        } else {
221            Err(anytrace::warn!(
222                "Stake table for epoch {epoch:?} unavailable. Catchup already in progress"
223            ))
224        }
225    }
226
227    /// Return the union of the stake table and DA committee for `epoch`,
228    /// keyed by signature key. Each entry's `Option<PeerConnectInfo>`
229    /// reflects whether the peer has connection info registered.
230    ///
231    /// For a peer in both, `Some` connect info wins over `None`: the snapshot's DA
232    /// committee may be a stale bootstrap copy lacking connect info while the stake
233    /// table has fresh L1-derived info (e.g. during CLIQUENET cutover), so plain
234    /// last-write-wins would clobber the live data.
235    ///
236    /// Returns `None` if the stake table for `epoch` is unavailable
237    /// (e.g. catchup is still in progress).
238    pub fn epoch_peers(
239        &self,
240        e: Option<EpochNumber>,
241    ) -> Option<HashMap<TYPES::SignatureKey, Option<PeerConnectInfo>>> {
242        let membership = self.stake_table_for_epoch(e).ok()?;
243        let mut out: HashMap<TYPES::SignatureKey, Option<PeerConnectInfo>> = HashMap::new();
244        let mut merge =
245            |key: TYPES::SignatureKey, info: Option<PeerConnectInfo>| match out.entry(key) {
246                Entry::Vacant(slot) => {
247                    slot.insert(info);
248                },
249                Entry::Occupied(mut slot) => {
250                    if slot.get().is_none() && info.is_some() {
251                        slot.insert(info);
252                    }
253                },
254            };
255        for m in membership.stake_table().chain(membership.da_stake_table()) {
256            merge(m.stake_table_entry.public_key(), m.connect_info.clone());
257        }
258        Some(out)
259    }
260
261    /// Collect the union of `epoch-1`, `epoch`, and `epoch+1` stake tables
262    /// (each merged with its DA committee) as a flat map of peers to dial.
263    ///
264    /// Newest-wins ordering for `connect_info`: next overrides curr overrides
265    /// prev. Entries with no `connect_info` are filtered out.
266    ///
267    /// Used to seed networks like cliquenet with the same window
268    /// `on_epoch_change` would build for `epoch`.
269    pub fn window_peers(&self, e: EpochNumber) -> HashMap<TYPES::SignatureKey, PeerConnectInfo> {
270        let curr = self.epoch_peers(Some(e)).unwrap_or_default();
271        let prev = if *e > 0 {
272            self.epoch_peers(Some(e - 1)).unwrap_or_default()
273        } else {
274            HashMap::new()
275        };
276        let next = self.epoch_peers(Some(e + 1)).unwrap_or_default();
277
278        // Newest-wins merge: start from prev, overlay curr and next.
279        let mut merged: HashMap<TYPES::SignatureKey, Option<PeerConnectInfo>> = prev;
280        for (k, v) in curr.into_iter().chain(next) {
281            merged.insert(k, v);
282        }
283
284        merged
285            .into_iter()
286            .filter_map(|(k, v)| v.map(|info| (k, info)))
287            .collect()
288    }
289
290    /// Start a catchup task for `epoch` unless one is already running.
291    ///
292    /// Returns `true` if this call started one.
293    ///
294    /// The work runs in its own task. The task spawned here holds the claim on
295    /// the epoch and reports a failure nobody happened to be waiting for, so
296    /// killing it releases the claim and lets the next caller start again,
297    /// while the work itself runs on until its steps time out.
298    fn spawn_catchup(&self, epoch: EpochNumber) -> bool {
299        if self.stake_table_map.contains_attempt(epoch) {
300            return false;
301        }
302        let (attempt, entry) = self.catchup_map.get_or_insert(epoch, || {
303            let coordinator = self.clone();
304            async move { coordinator.catchup(epoch).await }.boxed()
305        });
306        let Some(entry) = entry else {
307            return false;
308        };
309        spawn(async move {
310            let _entry = entry; // Dropping the entry removes the attempt.
311            if let Err(err) = attempt.await {
312                error!(%epoch, %err, "catchup failed");
313            }
314        });
315        true
316    }
317
318    /// Bring `epoch` to a usable membership: its stake table, then its DRB.
319    ///
320    /// Walks from the latest consecutive pair of stake tables this node holds
321    /// to `epoch`, fetching what is missing one epoch at a time. `epoch` is
322    /// where the walk stops, not where it starts, so an epoch nobody can serve
323    /// costs one fetch attempt at the first gap rather than a probe of every
324    /// earlier epoch.
325    ///
326    /// Concurrent catchups walk the same epochs and share one attempt per
327    /// epoch, so whichever reaches an epoch first fetches it while the others
328    /// wait for its result. Every walk awaits attempts in increasing epoch
329    /// order and an attempt awaits none, so no two can wait on each other.
330    async fn catchup(&self, epoch: EpochNumber) -> Result<()> {
331        let seeded = self.membership.first_epoch() + 1;
332
333        if epoch <= seeded {
334            return Err(anytrace::error!(
335                "catchup requested for epoch {epoch}, which the membership seeds at construction \
336                 rather than deriving it from an epoch root"
337            ));
338        }
339
340        let known = self.membership.highest_known_epoch().unwrap_or(seeded);
341
342        let mut start = known
343            .min(EpochNumber::new(epoch.saturating_sub(1)))
344            .max(seeded);
345
346        while start > seeded {
347            if self.stake_table_present(start).await? && self.stake_table_present(start - 1).await?
348            {
349                break;
350            }
351            start = start - 1;
352        }
353
354        info!(target: "announce::catchup", %epoch, %start, "catching up to epoch");
355
356        let mut try_epoch = start + 1;
357        while try_epoch <= epoch {
358            let (attempt, _entry) = self.stake_table_map.get_or_insert(try_epoch, || {
359                let coordinator = self.clone();
360                async move {
361                    if coordinator.stake_table_present(try_epoch).await? {
362                        return Ok(());
363                    }
364                    coordinator.fetch_stake_table(try_epoch).await
365                }
366                .boxed()
367            });
368            attempt.await.map_err(|err| {
369                anytrace::error!("catchup for epoch {epoch} failed at epoch {try_epoch}: {err:?}")
370            })?;
371            try_epoch += 1;
372        }
373
374        self.drb_ready(epoch).await
375    }
376
377    /// The membership for `epoch`, if its stake table is in memory.
378    fn epoch_membership(&self, epoch: EpochNumber) -> Option<EpochMembership<TYPES>> {
379        Some(EpochMembership {
380            coordinator: self.clone(),
381            snapshot: EpochMembershipSnapshot::Epoch {
382                epoch,
383                snapshot: self.membership.snapshot(epoch)?,
384            },
385        })
386    }
387
388    /// The membership of views before the first epoch.
389    fn genesis_membership(&self) -> EpochMembership<TYPES> {
390        EpochMembership {
391            coordinator: self.clone(),
392            snapshot: EpochMembershipSnapshot::Genesis(self.membership.genesis_stake_tables()),
393        }
394    }
395
396    /// Whether `epoch`'s stake table is in memory.
397    ///
398    /// It is read from storage if it is not already in RAM.
399    async fn stake_table_present(&self, epoch: EpochNumber) -> Result<bool> {
400        if self.membership.snapshot(epoch).is_some() {
401            return Ok(true);
402        }
403        debug!(%epoch, "loading stake table from storage");
404        timeout(
405            self.catchup_timeout,
406            self.membership.load_stake_table(epoch),
407        )
408        .await
409        .map_err(|_| {
410            anytrace::error!(
411                "loading the stake table for epoch {epoch} from storage exceeded {:?}",
412                self.catchup_timeout
413            )
414        })
415    }
416
417    /// Make `epoch`'s DRB result present.
418    ///
419    /// From peers if they have it, by computing it locally if they do not.
420    async fn drb_ready(&self, epoch: EpochNumber) -> Result<()> {
421        if self
422            .membership
423            .snapshot(epoch)
424            .is_some_and(|snapshot| snapshot.has_drb())
425        {
426            return Ok(());
427        }
428        debug!(%epoch, "fetching drb result from peers");
429        let fetched = timeout(self.catchup_timeout, self.get_epoch_drb(epoch))
430            .await
431            .map_err(|_| {
432                anytrace::error!(
433                    "fetching the DRB result for epoch {epoch} from peers exceeded {:?}",
434                    self.catchup_timeout
435                )
436            })?;
437        match fetched {
438            Ok(drb_result) => {
439                info!(
440                    target: "announce::catchup",
441                    %epoch,
442                    ?drb_result,
443                    "drb result retrieved from peers"
444                );
445                self.membership.add_drb_result(epoch, drb_result);
446            },
447            Err(err) => {
448                info!(
449                    target: "announce::catchup",
450                    %epoch,
451                    %err,
452                    "recalculating the drb result the peers lack"
453                );
454                let root_leaf = self.epoch_root_leaf(epoch).await?;
455                self.compute_drb_result(epoch, root_leaf).await?;
456            },
457        }
458
459        if self
460            .membership
461            .snapshot(epoch)
462            .is_some_and(|snapshot| snapshot.has_drb())
463        {
464            Ok(())
465        } else {
466            Err(anytrace::error!(
467                "the drb result for epoch {epoch} was not stored; its stake table is gone"
468            ))
469        }
470    }
471
472    /// Get the stake table for `epoch`, blocking on catchup if necessary.
473    ///
474    /// Unlike `stake_table_for_epoch`, this returns the result rather than
475    /// kicking off catchup and immediately returning an error. Used at startup
476    /// to drive the existing catchup chain synchronously before consensus is
477    /// running.
478    pub async fn wait_for_stake_table(&self, epoch: EpochNumber) -> Result<EpochMembership<TYPES>> {
479        match self.stake_table_for_epoch(Some(epoch)) {
480            Ok(mem) => Ok(mem),
481            Err(_) => self.wait_for_catchup(epoch).await,
482        }
483    }
484
485    /// Call this method if you think catchup is in progress for a given epoch
486    /// and you want to wait for it to finish and get the stake table.
487    /// If it's not, it will try to return the stake table if already available.
488    /// Returns an error if the catchup failed or the catchup is not in progress
489    /// and the stake table is not available.
490    pub async fn wait_for_catchup(&self, epoch: EpochNumber) -> Result<EpochMembership<TYPES>> {
491        let failed = |err| anytrace::error!("Catchup for epoch {epoch} failed: {err:?}");
492        if let Some(attempt) = self.catchup_map.get_attempt(epoch) {
493            attempt.await.map_err(failed)?;
494        } else if let Some(attempt) = self.stake_table_map.get_attempt(epoch) {
495            attempt.await.map_err(failed)?;
496        } else if let Some(membership) = self.epoch_membership(epoch) {
497            return Ok(membership);
498        } else {
499            return Err(anytrace::error!(
500                "No catchup in progress for epoch {epoch} and we don't have a stake table for it"
501            ));
502        }
503        self.epoch_membership(epoch).ok_or_else(|| {
504            anytrace::error!("Catchup for epoch {epoch} reported success but left no stake table")
505        })
506    }
507
508    /// The epoch root leaf `epoch`'s stake table is derived from.
509    ///
510    /// Fetched from peers and verified against the stake table of `epoch - 2`,
511    /// which must already be present. Also seeds the local DRB computation, so
512    /// it is fetched on that path too, without a stake table to install.
513    async fn epoch_root_leaf(&self, epoch: EpochNumber) -> Result<Leaf2<TYPES>> {
514        let root_epoch = EpochNumber::new(epoch.saturating_sub(2));
515        // Snapshot eviction can drop the root epoch from memory while a catchup
516        // is in progress, so load it back from local storage. Deliberately not
517        // `stake_table_for_epoch`, which on a miss claims the root epoch and
518        // spawns a nested catchup for it: that is how one step turns into a
519        // cascade.
520        self.stake_table_present(root_epoch).await?;
521        let Some(root_membership) = self.epoch_membership(root_epoch) else {
522            return Err(anytrace::error!(
523                "the stake table for epoch {epoch} is derived from epoch {root_epoch}, which is \
524                 in neither memory nor local storage"
525            ));
526        };
527
528        // Verification of the root is handled in get_epoch_root_and_drb
529        debug!(%epoch, %root_epoch, "fetching epoch root");
530        let fetched = timeout(self.catchup_timeout, root_membership.get_epoch_root())
531            .await
532            .map_err(|_| {
533                anytrace::error!(
534                    "fetching the epoch root of epoch {epoch} exceeded {:?}",
535                    self.catchup_timeout
536                )
537            })?;
538        fetched.map_err(|err| {
539            anytrace::error!("get epoch root leaf failed for epoch {root_epoch:?}: {err:#}")
540        })
541    }
542
543    /// Fetch `epoch`'s stake table and add it to the membership.
544    async fn fetch_stake_table(&self, epoch: EpochNumber) -> Result<()> {
545        let root_leaf = self.epoch_root_leaf(epoch).await?;
546
547        debug!(%epoch, "adding epoch root to membership");
548        timeout(
549            self.catchup_timeout,
550            self.add_epoch_root(root_leaf.block_header().clone()),
551        )
552        .await
553        .map_err(|_| {
554            anytrace::error!(
555                "adding the epoch root of epoch {epoch} to membership exceeded {:?}",
556                self.catchup_timeout
557            )
558        })?
559        .map_err(|e| {
560            anytrace::error!("Failed to add epoch root for epoch {epoch:?} to membership: {e}")
561        })
562    }
563
564    /// Compute `epoch`'s DRB result locally, claiming the epoch for the
565    /// duration so that only one computation runs per epoch.
566    ///
567    /// Bounded by progress rather than by time: the hash chain is
568    /// purposefully long, so it is given as long as it keeps hashing and
569    /// abandoned once it stops, which a chain queued behind a saturated
570    /// blocking pool does.
571    pub async fn compute_drb_result(
572        &self,
573        epoch: EpochNumber,
574        root_leaf: Leaf2<TYPES>,
575    ) -> Result<DrbResult> {
576        let cancel_token = {
577            let mut computations = self.drb_computations.lock();
578            if computations.contains_key(&epoch) {
579                return Err(anytrace::debug!(
580                    "DRB calculation for epoch {epoch} already in progress"
581                ));
582            }
583            let token = CancellationToken::new();
584            computations.insert(epoch, token.clone());
585            token
586        };
587        // The single owner of the bookkeeping inserted above: cleared on drop.
588        let _drb_state = DrbStateGuard {
589            coordinator: self.clone(),
590            epoch,
591        };
592
593        let Ok(drb_seed_input_vec) = bincode::serialize(&root_leaf.justify_qc().signatures) else {
594            return Err(anytrace::error!(
595                "Failed to serialize the QC signature for leaf {root_leaf:?}"
596            ));
597        };
598
599        let Some(drb_difficulty_selector) = self.drb_difficulty_selector.read().clone() else {
600            return Err(anytrace::error!(
601                "The DRB difficulty selector is missing from the epoch membership coordinator. \
602                 This node will not be able to spawn any DRB calculation tasks from catchup."
603            ));
604        };
605
606        let drb_difficulty = drb_difficulty_selector(root_leaf.block_header().version()).await;
607
608        let mut drb_seed_input = [0u8; 32];
609
610        if root_leaf.block_header().version() >= DRB_FIX_VERSION {
611            drb_seed_input = Sha256::digest(&drb_seed_input_vec).into();
612        } else {
613            let len = drb_seed_input_vec.len().min(32);
614            drb_seed_input[..len].copy_from_slice(&drb_seed_input_vec[..len]);
615        }
616
617        let drb_input = DrbInput {
618            epoch: *epoch,
619            iteration: 0,
620            value: drb_seed_input,
621            difficulty_level: drb_difficulty,
622        };
623
624        let store_drb_progress_fn = self.store_drb_progress_fn.clone();
625        let load_drb_progress_fn = self.load_drb_progress_fn.clone();
626
627        let heartbeat = Heartbeat::default();
628        let Some(computed) = heartbeat
629            .while_alive(
630                self.catchup_timeout,
631                compute_drb_result(
632                    drb_input,
633                    store_drb_progress_fn,
634                    load_drb_progress_fn,
635                    DRB_PROGRESS_LOAD_TIMEOUT,
636                    heartbeat.clone(),
637                    cancel_token,
638                ),
639            )
640            .await
641        else {
642            return Err(anytrace::error!(
643                "the DRB computation for epoch {epoch} reported no progress for {:?}",
644                self.catchup_timeout
645            ));
646        };
647        let drb = match computed {
648            Some(drb) => drb,
649            None => {
650                info!(
651                    %epoch,
652                    "the local drb computation was cancelled; fetching the externally supplied \
653                     result"
654                );
655                let fetched = timeout(self.catchup_timeout, self.get_epoch_drb(epoch))
656                    .await
657                    .map_err(|_| {
658                        anytrace::error!(
659                            "fetching the DRB result for epoch {epoch} from peers after \
660                             cancellation exceeded {:?}",
661                            self.catchup_timeout
662                        )
663                    })?;
664                return fetched.map_err(|e| {
665                    anytrace::error!(
666                        "DRB calculation for epoch {epoch} was cancelled but no externally \
667                         supplied result is available: {e}"
668                    )
669                });
670            },
671        };
672
673        // Publish the result in memory before persisting it, mirroring
674        // `supply_drb`.
675        self.membership.add_drb_result(epoch, drb);
676
677        // The result is already published in memory, so the epoch resolves
678        // whether or not the write gets through.
679        info!(%epoch, ?drb, "writing the computed drb result to storage");
680        match timeout(self.catchup_timeout, (self.store_drb_result_fn)(epoch, drb)).await {
681            Ok(Err(err)) => warn!(%epoch, %err, "failed to store the drb result"),
682            Ok(Ok(())) => {},
683            Err(_) => warn!(
684                %epoch,
685                timeout = ?self.catchup_timeout,
686                "storing the drb result timed out"
687            ),
688        }
689
690        Ok(drb)
691    }
692
693    /// Supply a DRB result obtained from an external source (e.g. a decided
694    /// leaf carrying `next_drb_result`). Adds the result to membership,
695    /// persists it to storage, and cancels any in-flight local computation
696    /// for `epoch`.
697    ///
698    /// If the stake table for `epoch` has not yet been loaded (e.g. the async
699    /// catchup that registers it is still in flight), this logs an error and
700    /// returns; the in-flight catchup will compute the DRB itself once it
701    /// completes.
702    pub fn supply_drb(&self, epoch: EpochNumber, drb: DrbResult) {
703        if self.membership.snapshot(epoch).is_none() {
704            error!(
705                %epoch,
706                "supply_drb called before the stake table is loaded; dropping the externally \
707                 supplied drb and relying on the in-flight catchup"
708            );
709            return;
710        }
711        self.membership.add_drb_result(epoch, drb);
712        // Left in place: the claim belongs to the computation, which clears it
713        // on its way out.
714        let maybe_token = self.drb_computations.lock().get(&epoch).cloned();
715        if let Some(token) = maybe_token {
716            token.cancel();
717        }
718        let store_drb_result_fn = self.store_drb_result_fn.clone();
719        spawn(async move {
720            info!(%epoch, ?drb, "writing the supplied drb result to storage");
721            if let Err(e) = store_drb_result_fn(epoch, drb).await {
722                warn!(%epoch, %e, "failed to store the supplied drb result");
723            }
724        });
725    }
726
727    /// The cancel token of the in-flight DRB computation for `epoch`, if one
728    /// is running. Test observability: lets a test assert that dropping a
729    /// computation's future fires its token.
730    pub fn drb_cancel_token(&self, epoch: EpochNumber) -> Option<CancellationToken> {
731        self.drb_computations.lock().get(&epoch).cloned()
732    }
733
734    /// Cancel all in-flight DRB calculations (e.g. on shutdown).
735    pub fn cancel_all_drb(&self) {
736        // Claims are left for the computations to release as they unwind, so
737        // that none of them is replaced by a fresh one on the way out.
738        let tokens: Vec<_> = self.drb_computations.lock().values().cloned().collect();
739        for token in tokens {
740            token.cancel();
741        }
742    }
743}
744
745/// Owns one computation's entry in `drb_computations`. Dropping it releases
746/// the claim and fires the token it held.
747struct DrbStateGuard<TYPES: NodeType> {
748    coordinator: EpochMembershipCoordinator<TYPES>,
749    epoch: EpochNumber,
750}
751
752impl<TYPES: NodeType> Drop for DrbStateGuard<TYPES> {
753    fn drop(&mut self) {
754        let token = self.coordinator.drb_computations.lock().remove(&self.epoch);
755        if let Some(token) = token {
756            token.cancel();
757        }
758    }
759}
760
761/// Default upper bound on a single catchup step.
762const DEFAULT_CATCHUP_TIMEOUT: Duration = Duration::from_secs(300);
763
764/// The in-flight attempts of one kind of work, keyed by the epoch they serve.
765#[derive(Clone, Default)]
766struct Attempts {
767    map: Arc<Mutex<HashMap<EpochNumber, Attempt>>>,
768}
769
770impl Attempts {
771    fn get_or_insert(
772        &self,
773        epoch: EpochNumber,
774        action: impl FnOnce() -> BoxFuture<'static, Result<()>>,
775    ) -> (Attempt, Option<AttemptEntry>) {
776        let mut map = self.map.lock();
777        if let Some(running) = map.get(&epoch) {
778            return (running.clone(), None);
779        }
780        // The work runs in its own task so that a panic in it is contained by
781        // the runtime: a `Shared` whose future panics poisons itself, and every
782        // task that later polls a handle panics too, so one panicking step
783        // would take down every caller waiting on the epoch. What the `Shared`
784        // holds is the join, which cannot panic.
785        //
786        // TODO: Remove explicit panic calls in drb.rs and committee.rs.
787        let task = spawn(action());
788        let attempt = async move {
789            task.await.unwrap_or_else(|err| {
790                Err(anytrace::error!(
791                    "the attempt for epoch {epoch} died: {err}"
792                ))
793            })
794        }
795        .boxed()
796        .shared();
797        map.insert(epoch, attempt.clone());
798        (
799            attempt,
800            Some(AttemptEntry {
801                attempts: self.clone(),
802                epoch,
803            }),
804        )
805    }
806
807    fn contains_attempt(&self, epoch: EpochNumber) -> bool {
808        self.map.lock().contains_key(&epoch)
809    }
810
811    fn get_attempt(&self, epoch: EpochNumber) -> Option<Attempt> {
812        self.map.lock().get(&epoch).cloned()
813    }
814}
815
816/// Holds an epoch's place in an [`Attempts`] map for as long as its owner is driving it.
817struct AttemptEntry {
818    attempts: Attempts,
819    epoch: EpochNumber,
820}
821
822impl Drop for AttemptEntry {
823    fn drop(&mut self) {
824        self.attempts.map.lock().remove(&self.epoch);
825    }
826}
827
828/// Wrapper around a membership that holds a captured snapshot for a given
829/// epoch (or the pre-epoch state). All accessors observe one consistent
830/// view because the snapshot is held inline.
831pub struct EpochMembership<TYPES: NodeType> {
832    /// The captured snapshot, either per-epoch or pre-epoch.
833    snapshot: EpochMembershipSnapshot<TYPES>,
834    /// Underlying coordinator, retained so navigation methods like
835    /// `next_epoch` can construct fresh snapshots.
836    pub coordinator: EpochMembershipCoordinator<TYPES>,
837}
838
839enum EpochMembershipSnapshot<TYPES: NodeType> {
840    Epoch {
841        epoch: EpochNumber,
842        snapshot: <TYPES::Membership as Membership<TYPES>>::Snapshot,
843    },
844    /// Views before the first epoch.
845    Genesis(Arc<GenesisStakeTables<TYPES>>),
846}
847
848impl<TYPES: NodeType> Clone for EpochMembershipSnapshot<TYPES> {
849    fn clone(&self) -> Self {
850        match self {
851            Self::Epoch { epoch, snapshot } => Self::Epoch {
852                epoch: *epoch,
853                snapshot: snapshot.clone(),
854            },
855            Self::Genesis(g) => Self::Genesis(g.clone()),
856        }
857    }
858}
859
860impl<TYPES: NodeType> Clone for EpochMembership<TYPES> {
861    fn clone(&self) -> Self {
862        Self {
863            coordinator: self.coordinator.clone(),
864            snapshot: self.snapshot.clone(),
865        }
866    }
867}
868
869impl<TYPES: NodeType> EpochMembership<TYPES> {
870    pub fn epoch(&self) -> Option<EpochNumber> {
871        match &self.snapshot {
872            EpochMembershipSnapshot::Epoch { epoch, .. } => Some(*epoch),
873            EpochMembershipSnapshot::Genesis(_) => None,
874        }
875    }
876
877    pub fn next_epoch(&self) -> Result<Self> {
878        let epoch = self
879            .epoch()
880            .ok_or_else(|| anytrace::error!("No next epoch because epoch is None"))?;
881        self.coordinator.membership_for_epoch(Some(epoch + 1))
882    }
883
884    pub fn next_epoch_stake_table(&self) -> Result<Self> {
885        let epoch = self
886            .epoch()
887            .ok_or_else(|| anytrace::error!("No next epoch because epoch is None"))?;
888        self.coordinator.stake_table_for_epoch(Some(epoch + 1))
889    }
890
891    pub fn get_new_epoch(&self, epoch: Option<EpochNumber>) -> Result<Self> {
892        self.coordinator.membership_for_epoch(epoch)
893    }
894
895    async fn get_epoch_root(&self) -> anyhow::Result<Leaf2<TYPES>> {
896        let Some(epoch) = self.epoch() else {
897            anyhow::bail!("Cannot get root for None epoch");
898        };
899        let leaf = self.coordinator.get_epoch_root(epoch).await?;
900        Ok(leaf)
901    }
902
903    pub async fn get_epoch_drb(&self) -> Result<DrbResult> {
904        let Some(epoch) = self.epoch() else {
905            return Err(anytrace::warn!("Cannot get drb for None epoch"));
906        };
907        self.coordinator.get_epoch_drb(epoch).await.wrap()
908    }
909
910    /// Borrow the per-epoch snapshot, or `None` before the first epoch.
911    pub fn snapshot(&self) -> Option<&<TYPES::Membership as Membership<TYPES>>::Snapshot> {
912        match &self.snapshot {
913            EpochMembershipSnapshot::Epoch { snapshot, .. } => Some(snapshot),
914            EpochMembershipSnapshot::Genesis(_) => None,
915        }
916    }
917
918    /// Add the DRB result for this epoch to the membership.
919    pub fn add_drb_result(&self, drb_result: DrbResult) {
920        if let Some(epoch) = self.epoch() {
921            self.coordinator
922                .membership
923                .add_drb_result(epoch, drb_result);
924        }
925    }
926
927    // ---------------------------------------------------------------------
928    // Single-call convenience accessors. Each delegates to whichever
929    // snapshot was captured at construction time, so a single accessor
930    // call observes one consistent view. For *sequences* of related reads
931    // that must observe the same view, take a snapshot via
932    // [`Self::snapshot`] and call methods on it directly.
933    // ---------------------------------------------------------------------
934
935    pub fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<TYPES>> + Send {
936        match &self.snapshot {
937            EpochMembershipSnapshot::Epoch { snapshot, .. } => Either::Left(snapshot.stake_table()),
938            EpochMembershipSnapshot::Genesis(g) => Either::Right(g.stake_table()),
939        }
940    }
941
942    pub fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<TYPES>> + Send {
943        match &self.snapshot {
944            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
945                Either::Left(snapshot.da_stake_table())
946            },
947            EpochMembershipSnapshot::Genesis(g) => Either::Right(g.da_stake_table()),
948        }
949    }
950
951    pub fn committee_members(
952        &self,
953        view: ViewNumber,
954    ) -> impl ExactSizeIterator<Item = &TYPES::SignatureKey> + Send {
955        match &self.snapshot {
956            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
957                Either::Left(snapshot.committee_members(view))
958            },
959            EpochMembershipSnapshot::Genesis(g) => Either::Right(g.committee_members()),
960        }
961    }
962
963    pub fn da_committee_members(
964        &self,
965        view: ViewNumber,
966    ) -> impl ExactSizeIterator<Item = &TYPES::SignatureKey> + Send {
967        match &self.snapshot {
968            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
969                Either::Left(snapshot.da_committee_members(view))
970            },
971            EpochMembershipSnapshot::Genesis(g) => Either::Right(g.da_committee_members()),
972        }
973    }
974
975    pub fn stake(&self, key: &TYPES::SignatureKey) -> Option<PeerConfig<TYPES>> {
976        match &self.snapshot {
977            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.stake(key),
978            EpochMembershipSnapshot::Genesis(g) => g.stake(key),
979        }
980    }
981
982    pub fn da_stake(&self, key: &TYPES::SignatureKey) -> Option<PeerConfig<TYPES>> {
983        match &self.snapshot {
984            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_stake(key),
985            EpochMembershipSnapshot::Genesis(g) => g.da_stake(key),
986        }
987    }
988
989    pub fn has_stake(&self, key: &TYPES::SignatureKey) -> bool {
990        match &self.snapshot {
991            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.has_stake(key),
992            EpochMembershipSnapshot::Genesis(g) => g.has_stake(key),
993        }
994    }
995
996    pub fn has_da_stake(&self, key: &TYPES::SignatureKey) -> bool {
997        match &self.snapshot {
998            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.has_da_stake(key),
999            EpochMembershipSnapshot::Genesis(g) => g.has_da_stake(key),
1000        }
1001    }
1002
1003    /// The leader for `view`, returning a HotShot-internal error type.
1004    ///
1005    /// # Errors
1006    ///
1007    /// Returns an error if the leader cannot be calculated.
1008    pub fn leader(&self, view: ViewNumber) -> Result<TYPES::SignatureKey> {
1009        match &self.snapshot {
1010            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.leader(view),
1011            EpochMembershipSnapshot::Genesis(g) => g.leader(view),
1012        }
1013    }
1014
1015    pub fn total_nodes(&self) -> usize {
1016        match &self.snapshot {
1017            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.total_nodes(),
1018            EpochMembershipSnapshot::Genesis(g) => g.total_nodes(),
1019        }
1020    }
1021
1022    pub fn da_total_nodes(&self) -> usize {
1023        match &self.snapshot {
1024            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_total_nodes(),
1025            EpochMembershipSnapshot::Genesis(g) => g.da_total_nodes(),
1026        }
1027    }
1028
1029    pub fn success_threshold(&self) -> U256 {
1030        match &self.snapshot {
1031            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.success_threshold(),
1032            EpochMembershipSnapshot::Genesis(g) => g.success_threshold(),
1033        }
1034    }
1035
1036    pub fn da_success_threshold(&self) -> U256 {
1037        match &self.snapshot {
1038            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_success_threshold(),
1039            EpochMembershipSnapshot::Genesis(g) => g.da_success_threshold(),
1040        }
1041    }
1042
1043    pub fn failure_threshold(&self) -> U256 {
1044        match &self.snapshot {
1045            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.failure_threshold(),
1046            EpochMembershipSnapshot::Genesis(g) => g.failure_threshold(),
1047        }
1048    }
1049
1050    pub fn upgrade_threshold(&self) -> U256 {
1051        match &self.snapshot {
1052            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.upgrade_threshold(),
1053            EpochMembershipSnapshot::Genesis(g) => g.upgrade_threshold(),
1054        }
1055    }
1056
1057    pub fn stake_table_hash(&self) -> Option<Commitment<SnapshotStakeTableHash<TYPES>>> {
1058        match &self.snapshot {
1059            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.stake_table_hash(),
1060            EpochMembershipSnapshot::Genesis(_) => None,
1061        }
1062    }
1063}