Skip to main content

hotshot_types/
epoch_membership.rs

1use std::{
2    collections::{HashMap, HashSet, hash_map::Entry},
3    sync::Arc,
4};
5
6use alloy_primitives::U256;
7use async_broadcast::{InactiveReceiver, Sender, broadcast};
8use committable::Commitment;
9use either::Either;
10use hotshot_utils::{anytrace::*, *};
11use parking_lot::{Mutex, RwLock};
12use sha2::{Digest, Sha256};
13use tokio_util::sync::CancellationToken;
14use versions::DRB_FIX_VERSION;
15
16use crate::{
17    PeerConfig, PeerConnectInfo,
18    data::{BlockNumber, EpochNumber, Leaf2, ViewNumber},
19    drb::{DrbDifficultySelectorFn, DrbInput, DrbResult, compute_drb_result},
20    traits::{
21        block_contents::BlockHeader,
22        election::{Membership, MembershipSnapshot, NonEpochMembershipSnapshot},
23        node_implementation::NodeType,
24        signature_key::StakeTableEntryType,
25        storage::{
26            LoadDrbProgressFn, Storage, StoreDrbProgressFn, StoreDrbResultFn, load_drb_progress_fn,
27            store_drb_progress_fn, store_drb_result_fn,
28        },
29    },
30};
31
32type EpochMap<TYPES> = HashMap<EpochNumber, InactiveReceiver<Result<EpochMembership<TYPES>>>>;
33
34type DrbMap = HashSet<EpochNumber>;
35
36/// Cancellation tokens for in-flight DRB computations. When an
37/// external source supplies the DRB result for `epoch` (e.g. a decided leaf
38/// carrying `next_drb_result`), `supply_drb` fires the token so the local
39/// computation can stop early instead of grinding to completion.
40type DrbCancelMap = HashMap<EpochNumber, CancellationToken>;
41
42type EpochSender<TYPES> = (EpochNumber, Sender<Result<EpochMembership<TYPES>>>);
43
44/// The per-epoch snapshot type associated with `T::Membership`.
45type Snapshot<T> = <<T as NodeType>::Membership as Membership<T>>::Snapshot;
46
47/// The stake-table hash type associated with `T::Membership`'s per-epoch
48/// snapshot.
49type SnapshotStakeTableHash<T> = <Snapshot<T> as MembershipSnapshot<T>>::StakeTableHash;
50
51/// Struct to Coordinate membership catchup
52pub struct EpochMembershipCoordinator<TYPES: NodeType> {
53    membership: Arc<TYPES::Membership>,
54    catchup_map: Arc<Mutex<EpochMap<TYPES>>>,
55    drb_calculation_map: Arc<Mutex<DrbMap>>,
56    drb_cancel_map: Arc<Mutex<DrbCancelMap>>,
57    epoch_height: BlockNumber,
58    store_drb_progress_fn: StoreDrbProgressFn,
59    load_drb_progress_fn: LoadDrbProgressFn,
60    store_drb_result_fn: StoreDrbResultFn,
61    drb_difficulty_selector: Arc<RwLock<Option<DrbDifficultySelectorFn>>>,
62}
63
64impl<TYPES: NodeType> Clone for EpochMembershipCoordinator<TYPES> {
65    fn clone(&self) -> Self {
66        Self {
67            membership: Arc::clone(&self.membership),
68            catchup_map: Arc::clone(&self.catchup_map),
69            drb_calculation_map: Arc::clone(&self.drb_calculation_map),
70            drb_cancel_map: Arc::clone(&self.drb_cancel_map),
71            epoch_height: self.epoch_height,
72            store_drb_progress_fn: Arc::clone(&self.store_drb_progress_fn),
73            load_drb_progress_fn: Arc::clone(&self.load_drb_progress_fn),
74            store_drb_result_fn: self.store_drb_result_fn.clone(),
75            drb_difficulty_selector: Arc::clone(&self.drb_difficulty_selector),
76        }
77    }
78}
79
80impl<TYPES: NodeType> EpochMembershipCoordinator<TYPES> {
81    pub fn new<M, S, B>(membership: M, epoch_height: B, storage: &S) -> Self
82    where
83        M: Into<Arc<TYPES::Membership>>,
84        B: Into<BlockNumber>,
85        S: Storage<TYPES>,
86    {
87        Self {
88            membership: membership.into(),
89            catchup_map: Arc::default(),
90            drb_calculation_map: Arc::default(),
91            drb_cancel_map: Arc::default(),
92            epoch_height: epoch_height.into(),
93            store_drb_progress_fn: store_drb_progress_fn(storage.clone()),
94            load_drb_progress_fn: load_drb_progress_fn(storage.clone()),
95            store_drb_result_fn: store_drb_result_fn(storage.clone()),
96            drb_difficulty_selector: Arc::new(RwLock::new(None)),
97        }
98    }
99
100    pub fn epoch_height(&self) -> BlockNumber {
101        self.epoch_height
102    }
103
104    /// Get a reference to the membership
105    pub fn membership(&self) -> &TYPES::Membership {
106        &self.membership
107    }
108
109    /// Handles notifications that a new epoch root has been created.
110    pub async fn add_epoch_root(
111        &self,
112        header: TYPES::BlockHeader,
113    ) -> std::result::Result<(), <TYPES::Membership as Membership<TYPES>>::Error> {
114        self.membership.add_epoch_root(header, self).await
115    }
116
117    /// Gets the validated block header and epoch number of the epoch root
118    /// at the given block height.
119    pub async fn get_epoch_root(
120        &self,
121        epoch: EpochNumber,
122    ) -> std::result::Result<Leaf2<TYPES>, <TYPES::Membership as Membership<TYPES>>::Error> {
123        self.membership.get_epoch_root(epoch, self).await
124    }
125
126    /// Gets the DRB result for the given epoch.
127    pub async fn get_epoch_drb(
128        &self,
129        epoch: EpochNumber,
130    ) -> std::result::Result<DrbResult, <TYPES::Membership as Membership<TYPES>>::Error> {
131        self.membership.get_epoch_drb(epoch, self).await
132    }
133
134    /// Set the DRB difficulty selector
135    pub fn set_drb_difficulty_selector(&self, f: DrbDifficultySelectorFn) {
136        let mut drb_difficulty_selector_writer = self.drb_difficulty_selector.write();
137        *drb_difficulty_selector_writer = Some(f);
138    }
139
140    /// Get a Membership for a given Epoch, which is guaranteed to have a randomized stake
141    /// table for the given Epoch
142    pub fn membership_for_epoch(
143        &self,
144        maybe_epoch: Option<EpochNumber>,
145    ) -> Result<EpochMembership<TYPES>> {
146        let Some(epoch) = maybe_epoch else {
147            return Ok(EpochMembership {
148                coordinator: self.clone(),
149                snapshot: EpochMembershipSnapshot::NonEpoch(self.membership.non_epoch_snapshot()),
150            });
151        };
152        let Some(first_epoch) = self.membership.first_epoch() else {
153            return Err(error!(
154                "membership_for_epoch called with epoch {epoch:?} but first_epoch is unset"
155            ));
156        };
157        if epoch < first_epoch {
158            return Err(error!(
159                "membership_for_epoch called with epoch {epoch:?} before first_epoch {first_epoch}"
160            ));
161        }
162        if let Some(snapshot) = self.membership.snapshot(epoch)
163            && snapshot.has_drb()
164        {
165            return Ok(EpochMembership {
166                coordinator: self.clone(),
167                snapshot: EpochMembershipSnapshot::Epoch { epoch, snapshot },
168            });
169        }
170        let mut catchup_map = self.catchup_map.lock();
171        match catchup_map.entry(epoch) {
172            Entry::Occupied(_) => Err(warn!(
173                "Randomized stake table for epoch {epoch:?} unavailable. Catchup already in \
174                 progress"
175            )),
176            Entry::Vacant(e) => {
177                let coordinator = self.clone();
178                let (tx, rx) = broadcast(1);
179                e.insert(rx.deactivate());
180                drop(catchup_map);
181                spawn_catchup(coordinator, epoch, tx);
182                Err(warn!(
183                    "Randomized stake table for epoch {epoch:?} unavailable. Starting catchup"
184                ))
185            },
186        }
187    }
188
189    /// Get a Membership for a given Epoch, which is guaranteed to have a stake
190    /// table for the given Epoch
191    pub fn stake_table_for_epoch(&self, e: Option<EpochNumber>) -> Result<EpochMembership<TYPES>> {
192        let Some(epoch) = e else {
193            return Ok(EpochMembership {
194                coordinator: self.clone(),
195                snapshot: EpochMembershipSnapshot::NonEpoch(self.membership.non_epoch_snapshot()),
196            });
197        };
198        let Some(first_epoch) = self.membership.first_epoch() else {
199            return Err(error!(
200                "stake_table_for_epoch called with epoch {epoch:?} but first_epoch is unset"
201            ));
202        };
203        if epoch < first_epoch {
204            return Err(error!(
205                "stake_table_for_epoch called with epoch {epoch:?} before first_epoch \
206                 {first_epoch}"
207            ));
208        }
209        if let Some(snapshot) = self.membership.snapshot(epoch) {
210            return Ok(EpochMembership {
211                coordinator: self.clone(),
212                snapshot: EpochMembershipSnapshot::Epoch { epoch, snapshot },
213            });
214        }
215        let mut catchup_map = self.catchup_map.lock();
216        match catchup_map.entry(epoch) {
217            Entry::Occupied(_) => Err(warn!(
218                "Stake table for epoch {epoch:?} unavailable. Catchup already in progress"
219            )),
220            Entry::Vacant(e) => {
221                let coordinator = self.clone();
222                let (tx, rx) = broadcast(1);
223                e.insert(rx.deactivate());
224                drop(catchup_map);
225                spawn_catchup(coordinator, epoch, tx);
226
227                Err(warn!(
228                    "Stake table for epoch {epoch:?} unavailable. Starting catchup"
229                ))
230            },
231        }
232    }
233
234    /// Return the union of the stake table and DA committee for `epoch`,
235    /// keyed by signature key. Each entry's `Option<PeerConnectInfo>`
236    /// reflects whether the peer has connection info registered.
237    ///
238    /// For a peer in both, `Some` connect info wins over `None`: the snapshot's DA
239    /// committee may be a stale bootstrap copy lacking connect info while the stake
240    /// table has fresh L1-derived info (e.g. during CLIQUENET cutover), so plain
241    /// last-write-wins would clobber the live data.
242    ///
243    /// Returns `None` if the stake table for `epoch` is unavailable
244    /// (e.g. catchup is still in progress).
245    pub fn epoch_peers(
246        &self,
247        e: Option<EpochNumber>,
248    ) -> Option<HashMap<TYPES::SignatureKey, Option<PeerConnectInfo>>> {
249        let membership = self.stake_table_for_epoch(e).ok()?;
250        let mut out: HashMap<TYPES::SignatureKey, Option<PeerConnectInfo>> = HashMap::new();
251        let mut merge =
252            |key: TYPES::SignatureKey, info: Option<PeerConnectInfo>| match out.entry(key) {
253                Entry::Vacant(slot) => {
254                    slot.insert(info);
255                },
256                Entry::Occupied(mut slot) => {
257                    if slot.get().is_none() && info.is_some() {
258                        slot.insert(info);
259                    }
260                },
261            };
262        if let Some(snap) = membership.snapshot() {
263            for m in snap.stake_table().chain(snap.da_stake_table()) {
264                merge(m.stake_table_entry.public_key(), m.connect_info.clone());
265            }
266        } else {
267            let snap = membership.non_epoch_snapshot()?;
268            for m in snap.stake_table().chain(snap.da_stake_table()) {
269                merge(m.stake_table_entry.public_key(), m.connect_info.clone());
270            }
271        }
272        Some(out)
273    }
274
275    /// Collect the union of `epoch-1`, `epoch`, and `epoch+1` stake tables
276    /// (each merged with its DA committee) as a flat map of peers to dial.
277    ///
278    /// Newest-wins ordering for `connect_info`: next overrides curr overrides
279    /// prev. Entries with no `connect_info` are filtered out.
280    ///
281    /// Used to seed networks like cliquenet with the same window
282    /// `on_epoch_change` would build for `epoch`.
283    pub fn window_peers(&self, e: EpochNumber) -> HashMap<TYPES::SignatureKey, PeerConnectInfo> {
284        let curr = self.epoch_peers(Some(e)).unwrap_or_default();
285        let prev = if *e > 0 {
286            self.epoch_peers(Some(e - 1)).unwrap_or_default()
287        } else {
288            HashMap::new()
289        };
290        let next = self.epoch_peers(Some(e + 1)).unwrap_or_default();
291
292        // Newest-wins merge: start from prev, overlay curr and next.
293        let mut merged: HashMap<TYPES::SignatureKey, Option<PeerConnectInfo>> = prev;
294        for (k, v) in curr.into_iter().chain(next) {
295            merged.insert(k, v);
296        }
297
298        merged
299            .into_iter()
300            .filter_map(|(k, v)| v.map(|info| (k, info)))
301            .collect()
302    }
303
304    /// Catches the membership up to the epoch passed as an argument.
305    /// To do this, try to get the stake table for the epoch containing this
306    /// epoch's root and the stake table for the epoch containing this epoch's
307    /// drb result. If they do not exist, then go one by one back until we
308    /// find a stake table.
309    ///
310    /// If there is another catchup in progress this will not duplicate efforts
311    /// e.g. if we start with only the first epoch stake table and call catchup
312    /// for epoch 10, then call catchup for epoch 20 the first caller will
313    /// actually do the work for to catchup to epoch 10 then the second caller
314    /// will continue catching up to epoch 20
315    //
316    // Clippy claims "this `MutexGuard` is held across an await point", however
317    // the guard is explicitly dropped before. See also:
318    // https://github.com/rust-lang/rust-clippy/issues/6446
319    //
320    // Even more annoying is that the warning can only be disabled on function
321    // level, instead of putting this attribute on the expression, see
322    // https://github.com/rust-lang/rust-clippy/issues/9047.
323    #[allow(clippy::await_holding_lock)]
324    async fn catchup(self, epoch: EpochNumber, epoch_tx: Sender<Result<EpochMembership<TYPES>>>) {
325        // We need to fetch the requested epoch, that's for sure
326        let mut fetch_epochs = vec![];
327
328        let mut try_epoch = EpochNumber::new(epoch.saturating_sub(1));
329        let maybe_first_epoch = self.membership.first_epoch();
330        let Some(first_epoch) = maybe_first_epoch else {
331            let err = anytrace::error!(
332                "We got a catchup request for epoch {epoch:?} but the first epoch is not set"
333            );
334            self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
335            return;
336        };
337
338        // First figure out which epochs we need to fetch
339        loop {
340            let has_stake_table = self.membership.snapshot(try_epoch).is_some();
341            if has_stake_table {
342                // We have this stake table but we need to make sure we have the
343                // epoch root of the requested epoch
344                if try_epoch <= EpochNumber::new(epoch.saturating_sub(2)) {
345                    break;
346                }
347                try_epoch = EpochNumber::new(try_epoch.saturating_sub(1));
348            } else {
349                if try_epoch <= first_epoch + 1 {
350                    let err = anytrace::error!(
351                        "We are trying to catchup to an epoch lower than the second epoch! This \
352                         means the initial stake table is missing!"
353                    );
354                    self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
355                    return;
356                }
357                // Lock the catchup map
358                let mut map_lock = self.catchup_map.lock();
359                match map_lock
360                    .get(&try_epoch)
361                    .map(InactiveReceiver::activate_cloned)
362                {
363                    Some(mut rx) => {
364                        // Somebody else is already fetching this epoch, drop
365                        // the lock and wait for them to finish
366                        drop(map_lock);
367                        if let Ok(Ok(_)) = rx.recv_direct().await {
368                            break;
369                        };
370                        // If we didn't receive the epoch then we need to try again
371                    },
372                    _ => {
373                        // Nobody else is fetching this epoch. We need to do it.
374                        // Put it in the map and move on to the next epoch
375                        let (mut tx, rx) = broadcast(1);
376                        tx.set_overflow(true);
377                        map_lock.insert(try_epoch, rx.deactivate());
378                        drop(map_lock);
379                        fetch_epochs.push((try_epoch, tx));
380                        try_epoch = EpochNumber::new(try_epoch.saturating_sub(1));
381                    },
382                }
383            };
384        }
385
386        let epochs = fetch_epochs.iter().map(|(e, _)| e).collect::<Vec<_>>();
387        tracing::warn!("Fetching stake tables for epochs: {epochs:?}");
388
389        // Iterate through the epochs we need to fetch in reverse, i.e. from the oldest to the newest
390        while let Some((current_fetch_epoch, tx)) = fetch_epochs.pop() {
391            match self.fetch_stake_table(current_fetch_epoch).await {
392                Ok(_) => {},
393                Err(err) => {
394                    fetch_epochs.push((current_fetch_epoch, tx));
395                    self.catchup_cleanup(epoch, epoch_tx, fetch_epochs, err);
396                    return;
397                },
398            };
399
400            // Signal the other tasks about the success. `fetch_stake_table`
401            // returned `Ok`, so a snapshot must be present. If it isn't,
402            // treat that as a catchup failure: push the in-flight epoch
403            // back and run cleanup so waiters get notified.
404            let Some(snapshot) = self.membership.snapshot(current_fetch_epoch) else {
405                let err = anytrace::error!(
406                    "snapshot for epoch {current_fetch_epoch} unavailable after fetch_stake_table"
407                );
408                fetch_epochs.push((current_fetch_epoch, tx));
409                self.catchup_cleanup(epoch, epoch_tx, fetch_epochs, err);
410                return;
411            };
412            let mem = EpochMembership {
413                coordinator: self.clone(),
414                snapshot: EpochMembershipSnapshot::Epoch {
415                    epoch: current_fetch_epoch,
416                    snapshot,
417                },
418            };
419            if let Ok(Some(res)) = tx.try_broadcast(Ok(mem)) {
420                tracing::warn!(
421                    "The catchup channel for epoch {} was overflown, dropped message {:?}",
422                    current_fetch_epoch,
423                    res.map(|em| em.epoch())
424                );
425            }
426
427            // Remove the epoch from the catchup map to indicate that the catchup is complete
428            self.catchup_map.lock().remove(&current_fetch_epoch);
429        }
430
431        let root_leaf = match self.fetch_stake_table(epoch).await {
432            Ok(root_leaf) => root_leaf,
433            Err(err) => {
434                tracing::error!("Failed to fetch stake table for epoch {epoch:?}: {err:?}");
435                self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
436                return;
437            },
438        };
439
440        match self.get_epoch_drb(epoch).await {
441            Ok(drb_result) => {
442                tracing::warn!(
443                    ?drb_result,
444                    "DRB result for epoch {epoch:?} retrieved from peers. Updating membership."
445                );
446                self.membership.add_drb_result(epoch, drb_result);
447            },
448            Err(err) => {
449                tracing::warn!(
450                    "Recalculating missing DRB result for epoch {}. Catchup failed with error: {}",
451                    epoch,
452                    err
453                );
454
455                let result = self.compute_drb_result(epoch, root_leaf).await;
456
457                log!(result);
458
459                if let Err(err) = result {
460                    self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
461                    return;
462                }
463            },
464        };
465
466        // Signal the other tasks about the success. As above, the snapshot
467        // must be present at this point — if not, treat as a catchup failure.
468        let Some(snapshot) = self.membership.snapshot(epoch) else {
469            let err = anytrace::error!(
470                "snapshot for epoch {epoch} unavailable after fetch_stake_table + DRB"
471            );
472            self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
473            return;
474        };
475        let mem = EpochMembership {
476            coordinator: self.clone(),
477            snapshot: EpochMembershipSnapshot::Epoch { epoch, snapshot },
478        };
479        if let Ok(Some(res)) = epoch_tx.try_broadcast(Ok(mem)) {
480            tracing::warn!(
481                "The catchup channel for epoch {} was overflown, dropped message {:?}",
482                epoch,
483                res.map(|em| em.epoch())
484            );
485        }
486
487        // Remove the epoch from the catchup map to indicate that the catchup is complete
488        self.catchup_map.lock().remove(&epoch);
489    }
490
491    /// Get the stake table for `epoch`, blocking on catchup if necessary.
492    ///
493    /// Unlike `stake_table_for_epoch`, this returns the result rather than
494    /// kicking off catchup and immediately returning an error. Used at startup
495    /// to drive the existing catchup chain synchronously before consensus is
496    /// running.
497    pub async fn wait_for_stake_table(&self, epoch: EpochNumber) -> Result<EpochMembership<TYPES>> {
498        match self.stake_table_for_epoch(Some(epoch)) {
499            Ok(mem) => Ok(mem),
500            Err(_) => self.wait_for_catchup(epoch).await,
501        }
502    }
503
504    /// Call this method if you think catchup is in progress for a given epoch
505    /// and you want to wait for it to finish and get the stake table.
506    /// If it's not, it will try to return the stake table if already available.
507    /// Returns an error if the catchup failed or the catchup is not in progress
508    /// and the stake table is not available.
509    pub async fn wait_for_catchup(&self, epoch: EpochNumber) -> Result<EpochMembership<TYPES>> {
510        let maybe_receiver = self
511            .catchup_map
512            .lock()
513            .get(&epoch)
514            .map(InactiveReceiver::activate_cloned);
515        let Some(mut rx) = maybe_receiver else {
516            // There is no catchup in progress, maybe the epoch is already finalized
517            if let Some(snapshot) = self.membership.snapshot(epoch) {
518                return Ok(EpochMembership {
519                    coordinator: self.clone(),
520                    snapshot: EpochMembershipSnapshot::Epoch { epoch, snapshot },
521                });
522            }
523            return Err(anytrace::error!(
524                "No catchup in progress for epoch {epoch} and we don't have a stake table for it"
525            ));
526        };
527        let Ok(Ok(mem)) = rx.recv_direct().await else {
528            return Err(anytrace::error!("Catchup for epoch {epoch} failed"));
529        };
530        Ok(mem)
531    }
532
533    /// Clean up after a failed catchup attempt.
534    ///
535    /// This method is called when a catchup attempt fails. It cleans up the state of the
536    /// `EpochMembershipCoordinator` by removing the failed epochs from the
537    /// `catchup_map` and broadcasting the error to any tasks that are waiting for the
538    /// catchup to complete.
539    fn catchup_cleanup(
540        &self,
541        req_epoch: EpochNumber,
542        epoch_tx: Sender<Result<EpochMembership<TYPES>>>,
543        mut cancel_epochs: Vec<EpochSender<TYPES>>,
544        err: Error,
545    ) {
546        // Cleanup in case of error
547        cancel_epochs.push((req_epoch, epoch_tx));
548
549        tracing::error!(
550            "catchup for epoch {req_epoch:?} failed: {err:?}. Canceling catchup for epochs: {:?}",
551            cancel_epochs.iter().map(|(e, _)| e).collect::<Vec<_>>()
552        );
553
554        {
555            let mut map_lock = self.catchup_map.lock();
556            for (epoch, _) in cancel_epochs.iter() {
557                // Remove the failed epochs from the catchup map
558                map_lock.remove(epoch);
559            }
560        }
561
562        for (cancel_epoch, tx) in cancel_epochs {
563            // Signal the other tasks about the failures
564            if let Ok(Some(res)) = tx.try_broadcast(Err(err.clone())) {
565                tracing::warn!(
566                    "The catchup channel for epoch {} was overflown during cleanup, dropped \
567                     message {:?}",
568                    cancel_epoch,
569                    res.map(|em| em.epoch())
570                );
571            }
572        }
573    }
574
575    /// A helper method to the `catchup` method.
576    ///
577    /// It tries to fetch the requested stake table from the root epoch,
578    /// and updates the membership accordingly.
579    ///
580    /// # Arguments
581    ///
582    /// * `epoch` - The epoch for which to fetch the stake table.
583    ///
584    /// # Returns
585    ///
586    /// * `Ok(Leaf2<TYPES>)` containing the epoch root leaf if successful.
587    /// * `Err(Error)` if the root membership or root leaf cannot be found, or if
588    ///   updating the membership fails.
589    async fn fetch_stake_table(&self, epoch: EpochNumber) -> Result<Leaf2<TYPES>> {
590        let root_epoch = EpochNumber::new(epoch.saturating_sub(2));
591        let Ok(root_membership) = self.stake_table_for_epoch(Some(root_epoch)) else {
592            return Err(anytrace::error!(
593                "We tried to fetch stake table for epoch {epoch:?} but we don't have its root \
594                 epoch {root_epoch:?}. This should not happen"
595            ));
596        };
597
598        // Get the epoch root headers and update our membership with them, finally sync them
599        // Verification of the root is handled in get_epoch_root_and_drb
600        let Ok(root_leaf) = root_membership.get_epoch_root().await else {
601            return Err(anytrace::error!(
602                "get epoch root leaf failed for epoch {root_epoch:?}"
603            ));
604        };
605
606        self.add_epoch_root(root_leaf.block_header().clone())
607            .await
608            .map_err(|e| {
609                anytrace::error!("Failed to add epoch root for epoch {epoch:?} to membership: {e}")
610            })?;
611
612        Ok(root_leaf)
613    }
614
615    pub async fn compute_drb_result(
616        &self,
617        epoch: EpochNumber,
618        root_leaf: Leaf2<TYPES>,
619    ) -> Result<DrbResult> {
620        let cancel_token = {
621            let mut drb_calculation_map_lock = self.drb_calculation_map.lock();
622
623            if drb_calculation_map_lock.contains(&epoch) {
624                return Err(anytrace::debug!(
625                    "DRB calculation for epoch {} already in progress",
626                    epoch
627                ));
628            }
629            drb_calculation_map_lock.insert(epoch);
630
631            let token = CancellationToken::new();
632            self.drb_cancel_map.lock().insert(epoch, token.clone());
633            token
634        };
635
636        let Ok(drb_seed_input_vec) = bincode::serialize(&root_leaf.justify_qc().signatures) else {
637            self.clear_drb_state(epoch);
638            return Err(anytrace::error!(
639                "Failed to serialize the QC signature for leaf {root_leaf:?}"
640            ));
641        };
642
643        let Some(drb_difficulty_selector) = self.drb_difficulty_selector.read().clone() else {
644            self.clear_drb_state(epoch);
645            return Err(anytrace::error!(
646                "The DRB difficulty selector is missing from the epoch membership coordinator. \
647                 This node will not be able to spawn any DRB calculation tasks from catchup."
648            ));
649        };
650
651        let drb_difficulty = drb_difficulty_selector(root_leaf.block_header().version()).await;
652
653        let mut drb_seed_input = [0u8; 32];
654
655        if root_leaf.block_header().version() >= DRB_FIX_VERSION {
656            drb_seed_input = Sha256::digest(&drb_seed_input_vec).into();
657        } else {
658            let len = drb_seed_input_vec.len().min(32);
659            drb_seed_input[..len].copy_from_slice(&drb_seed_input_vec[..len]);
660        }
661
662        let drb_input = DrbInput {
663            epoch: *epoch,
664            iteration: 0,
665            value: drb_seed_input,
666            difficulty_level: drb_difficulty,
667        };
668
669        let store_drb_progress_fn = self.store_drb_progress_fn.clone();
670        let load_drb_progress_fn = self.load_drb_progress_fn.clone();
671
672        let drb = match compute_drb_result(
673            drb_input,
674            store_drb_progress_fn,
675            load_drb_progress_fn,
676            cancel_token,
677        )
678        .await
679        {
680            Some(drb) => drb,
681            None => {
682                self.clear_drb_state(epoch);
683                return self.get_epoch_drb(epoch).await.map_err(|e| {
684                    anytrace::error!(
685                        "DRB calculation for epoch {epoch} was cancelled but no externally \
686                         supplied result is available: {e}"
687                    )
688                });
689            },
690        };
691
692        self.clear_drb_state(epoch);
693
694        tracing::info!("Writing drb result from catchup to storage for epoch {epoch}: {drb:?}");
695        if let Err(e) = (self.store_drb_result_fn)(epoch, drb).await {
696            tracing::warn!("Failed to add drb result to storage: {e}");
697        }
698        self.membership.add_drb_result(epoch, drb);
699
700        Ok(drb)
701    }
702
703    /// Supply a DRB result obtained from an external source (e.g. a decided
704    /// leaf carrying `next_drb_result`). Adds the result to membership,
705    /// persists it to storage, and cancels any in-flight local computation
706    /// for `epoch`.
707    ///
708    /// If the stake table for `epoch` has not yet been loaded (e.g. the async
709    /// catchup that registers it is still in flight), this logs an error and
710    /// returns; the in-flight catchup will compute the DRB itself once it
711    /// completes.
712    pub fn supply_drb(&self, epoch: EpochNumber, drb: DrbResult) {
713        if self.membership.snapshot(epoch).is_none() {
714            tracing::error!(
715                "supply_drb called for epoch {epoch} but stake table not yet loaded; dropping \
716                 externally-supplied DRB and relying on in-flight catchup"
717            );
718            return;
719        }
720        self.membership.add_drb_result(epoch, drb);
721        let maybe_token = self.drb_cancel_map.lock().remove(&epoch);
722        if let Some(token) = maybe_token {
723            token.cancel();
724        }
725        let store_drb_result_fn = self.store_drb_result_fn.clone();
726        tokio::spawn(async move {
727            tracing::info!(
728                "Writing externally supplied drb result to storage for epoch {epoch}: {drb:?}"
729            );
730            if let Err(e) = store_drb_result_fn(epoch, drb).await {
731                tracing::warn!("Failed to add externally supplied drb result to storage: {e}");
732            }
733        });
734    }
735
736    /// Remove per-epoch DRB bookkeeping after a computation finishes or is
737    /// cancelled. Safe to call multiple times.
738    fn clear_drb_state(&self, epoch: EpochNumber) {
739        self.drb_calculation_map.lock().remove(&epoch);
740        self.drb_cancel_map.lock().remove(&epoch);
741    }
742
743    /// Cancel all in-flight DRB calculations (e.g. on shutdown).
744    pub fn cancel_all_drb(&self) {
745        let tokens: Vec<_> = self.drb_cancel_map.lock().drain().map(|(_, t)| t).collect();
746        for token in tokens {
747            token.cancel();
748        }
749    }
750}
751
752fn spawn_catchup<T: NodeType>(
753    coordinator: EpochMembershipCoordinator<T>,
754    epoch: EpochNumber,
755    epoch_tx: Sender<Result<EpochMembership<T>>>,
756) {
757    tokio::spawn(async move {
758        coordinator.clone().catchup(epoch, epoch_tx).await;
759    });
760}
761
762/// Wrapper around a membership that holds a captured snapshot for a given
763/// epoch (or the pre-epoch state). All accessors observe one consistent
764/// view because the snapshot is held inline.
765pub struct EpochMembership<TYPES: NodeType> {
766    /// The captured snapshot, either per-epoch or pre-epoch.
767    snapshot: EpochMembershipSnapshot<TYPES>,
768    /// Underlying coordinator, retained so navigation methods like
769    /// `next_epoch` can construct fresh snapshots.
770    pub coordinator: EpochMembershipCoordinator<TYPES>,
771}
772
773enum EpochMembershipSnapshot<TYPES: NodeType> {
774    Epoch {
775        epoch: EpochNumber,
776        snapshot: <TYPES::Membership as Membership<TYPES>>::Snapshot,
777    },
778    NonEpoch(<TYPES::Membership as Membership<TYPES>>::NonEpochSnapshot),
779}
780
781impl<TYPES: NodeType> Clone for EpochMembershipSnapshot<TYPES> {
782    fn clone(&self) -> Self {
783        match self {
784            Self::Epoch { epoch, snapshot } => Self::Epoch {
785                epoch: *epoch,
786                snapshot: snapshot.clone(),
787            },
788            Self::NonEpoch(s) => Self::NonEpoch(s.clone()),
789        }
790    }
791}
792
793impl<TYPES: NodeType> Clone for EpochMembership<TYPES> {
794    fn clone(&self) -> Self {
795        Self {
796            coordinator: self.coordinator.clone(),
797            snapshot: self.snapshot.clone(),
798        }
799    }
800}
801
802impl<TYPES: NodeType> EpochMembership<TYPES> {
803    pub fn epoch(&self) -> Option<EpochNumber> {
804        match &self.snapshot {
805            EpochMembershipSnapshot::Epoch { epoch, .. } => Some(*epoch),
806            EpochMembershipSnapshot::NonEpoch(_) => None,
807        }
808    }
809
810    pub fn next_epoch(&self) -> Result<Self> {
811        let epoch = self
812            .epoch()
813            .ok_or_else(|| anytrace::error!("No next epoch because epoch is None"))?;
814        self.coordinator.membership_for_epoch(Some(epoch + 1))
815    }
816
817    pub fn next_epoch_stake_table(&self) -> Result<Self> {
818        let epoch = self
819            .epoch()
820            .ok_or_else(|| anytrace::error!("No next epoch because epoch is None"))?;
821        self.coordinator.stake_table_for_epoch(Some(epoch + 1))
822    }
823
824    pub fn get_new_epoch(&self, epoch: Option<EpochNumber>) -> Result<Self> {
825        self.coordinator.membership_for_epoch(epoch)
826    }
827
828    async fn get_epoch_root(&self) -> anyhow::Result<Leaf2<TYPES>> {
829        let Some(epoch) = self.epoch() else {
830            anyhow::bail!("Cannot get root for None epoch");
831        };
832        let leaf = self.coordinator.get_epoch_root(epoch).await?;
833        Ok(leaf)
834    }
835
836    pub async fn get_epoch_drb(&self) -> Result<DrbResult> {
837        let Some(epoch) = self.epoch() else {
838            return Err(anytrace::warn!("Cannot get drb for None epoch"));
839        };
840        self.coordinator.get_epoch_drb(epoch).await.wrap()
841    }
842
843    /// Borrow the per-epoch snapshot, or `None` for the pre-epoch case.
844    pub fn snapshot(&self) -> Option<&<TYPES::Membership as Membership<TYPES>>::Snapshot> {
845        match &self.snapshot {
846            EpochMembershipSnapshot::Epoch { snapshot, .. } => Some(snapshot),
847            EpochMembershipSnapshot::NonEpoch(_) => None,
848        }
849    }
850
851    /// Borrow the pre-epoch snapshot, or `None` if this is a per-epoch
852    /// membership.
853    pub fn non_epoch_snapshot(
854        &self,
855    ) -> Option<&<TYPES::Membership as Membership<TYPES>>::NonEpochSnapshot> {
856        match &self.snapshot {
857            EpochMembershipSnapshot::NonEpoch(s) => Some(s),
858            EpochMembershipSnapshot::Epoch { .. } => None,
859        }
860    }
861
862    /// Add the DRB result for this epoch to the membership.
863    pub fn add_drb_result(&self, drb_result: DrbResult) {
864        if let Some(epoch) = self.epoch() {
865            self.coordinator
866                .membership
867                .add_drb_result(epoch, drb_result);
868        }
869    }
870
871    // ---------------------------------------------------------------------
872    // Single-call convenience accessors. Each delegates to whichever
873    // snapshot was captured at construction time, so a single accessor
874    // call observes one consistent view. For *sequences* of related reads
875    // that must observe the same view, take a snapshot via
876    // [`Self::snapshot`] / [`Self::non_epoch_snapshot`] and call methods
877    // on it directly.
878    // ---------------------------------------------------------------------
879
880    pub fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<TYPES>> + Send {
881        match &self.snapshot {
882            EpochMembershipSnapshot::Epoch { snapshot, .. } => Either::Left(snapshot.stake_table()),
883            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.stake_table()),
884        }
885    }
886
887    pub fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<TYPES>> + Send {
888        match &self.snapshot {
889            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
890                Either::Left(snapshot.da_stake_table())
891            },
892            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.da_stake_table()),
893        }
894    }
895
896    pub fn committee_members(
897        &self,
898        view: ViewNumber,
899    ) -> impl ExactSizeIterator<Item = &TYPES::SignatureKey> + Send {
900        match &self.snapshot {
901            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
902                Either::Left(snapshot.committee_members(view))
903            },
904            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.committee_members(view)),
905        }
906    }
907
908    pub fn da_committee_members(
909        &self,
910        view: ViewNumber,
911    ) -> impl ExactSizeIterator<Item = &TYPES::SignatureKey> + Send {
912        match &self.snapshot {
913            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
914                Either::Left(snapshot.da_committee_members(view))
915            },
916            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.da_committee_members(view)),
917        }
918    }
919
920    pub fn stake(&self, key: &TYPES::SignatureKey) -> Option<PeerConfig<TYPES>> {
921        match &self.snapshot {
922            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.stake(key),
923            EpochMembershipSnapshot::NonEpoch(s) => s.stake(key),
924        }
925    }
926
927    pub fn da_stake(&self, key: &TYPES::SignatureKey) -> Option<PeerConfig<TYPES>> {
928        match &self.snapshot {
929            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_stake(key),
930            EpochMembershipSnapshot::NonEpoch(s) => s.da_stake(key),
931        }
932    }
933
934    pub fn has_stake(&self, key: &TYPES::SignatureKey) -> bool {
935        match &self.snapshot {
936            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.has_stake(key),
937            EpochMembershipSnapshot::NonEpoch(s) => s.has_stake(key),
938        }
939    }
940
941    pub fn has_da_stake(&self, key: &TYPES::SignatureKey) -> bool {
942        match &self.snapshot {
943            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.has_da_stake(key),
944            EpochMembershipSnapshot::NonEpoch(s) => s.has_da_stake(key),
945        }
946    }
947
948    /// The leader for `view`, returning a HotShot-internal error type.
949    ///
950    /// # Errors
951    ///
952    /// Returns an error if the leader cannot be calculated.
953    pub fn leader(&self, view: ViewNumber) -> Result<TYPES::SignatureKey> {
954        match &self.snapshot {
955            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.leader(view),
956            EpochMembershipSnapshot::NonEpoch(s) => s.leader(view),
957        }
958    }
959
960    /// The leader for `view`, returning the membership-impl error type.
961    ///
962    /// # Errors
963    ///
964    /// Returns the membership-impl error if the leader cannot be calculated.
965    pub fn lookup_leader(
966        &self,
967        view: ViewNumber,
968    ) -> std::result::Result<
969        TYPES::SignatureKey,
970        <<TYPES as NodeType>::Membership as Membership<TYPES>>::Error,
971    > {
972        match &self.snapshot {
973            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.lookup_leader(view),
974            EpochMembershipSnapshot::NonEpoch(s) => s.lookup_leader(view),
975        }
976    }
977
978    pub fn total_nodes(&self) -> usize {
979        match &self.snapshot {
980            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.total_nodes(),
981            EpochMembershipSnapshot::NonEpoch(s) => s.total_nodes(),
982        }
983    }
984
985    pub fn da_total_nodes(&self) -> usize {
986        match &self.snapshot {
987            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_total_nodes(),
988            EpochMembershipSnapshot::NonEpoch(s) => s.da_total_nodes(),
989        }
990    }
991
992    pub fn success_threshold(&self) -> U256 {
993        match &self.snapshot {
994            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.success_threshold(),
995            EpochMembershipSnapshot::NonEpoch(s) => s.success_threshold(),
996        }
997    }
998
999    pub fn da_success_threshold(&self) -> U256 {
1000        match &self.snapshot {
1001            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_success_threshold(),
1002            EpochMembershipSnapshot::NonEpoch(s) => s.da_success_threshold(),
1003        }
1004    }
1005
1006    pub fn failure_threshold(&self) -> U256 {
1007        match &self.snapshot {
1008            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.failure_threshold(),
1009            EpochMembershipSnapshot::NonEpoch(s) => s.failure_threshold(),
1010        }
1011    }
1012
1013    pub fn upgrade_threshold(&self) -> U256 {
1014        match &self.snapshot {
1015            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.upgrade_threshold(),
1016            EpochMembershipSnapshot::NonEpoch(s) => s.upgrade_threshold(),
1017        }
1018    }
1019
1020    pub fn stake_table_hash(&self) -> Option<Commitment<SnapshotStakeTableHash<TYPES>>> {
1021        match &self.snapshot {
1022            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.stake_table_hash(),
1023            EpochMembershipSnapshot::NonEpoch(_) => None,
1024        }
1025    }
1026}