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                || self.membership.load_stake_table(try_epoch).await;
342            if has_stake_table {
343                // We have this stake table but we need to make sure we have the
344                // epoch root of the requested epoch
345                if try_epoch <= EpochNumber::new(epoch.saturating_sub(2)) {
346                    break;
347                }
348                try_epoch = EpochNumber::new(try_epoch.saturating_sub(1));
349            } else {
350                if try_epoch <= first_epoch + 1 {
351                    let err = anytrace::error!(
352                        "We are trying to catchup to an epoch lower than the second epoch! This \
353                         means the initial stake table is missing!"
354                    );
355                    self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
356                    return;
357                }
358                // Lock the catchup map
359                let mut map_lock = self.catchup_map.lock();
360                match map_lock
361                    .get(&try_epoch)
362                    .map(InactiveReceiver::activate_cloned)
363                {
364                    Some(mut rx) => {
365                        // Somebody else is already fetching this epoch, drop
366                        // the lock and wait for them to finish
367                        drop(map_lock);
368                        if let Ok(Ok(_)) = rx.recv_direct().await {
369                            break;
370                        };
371                        // If we didn't receive the epoch then we need to try again
372                    },
373                    _ => {
374                        // Nobody else is fetching this epoch. We need to do it.
375                        // Put it in the map and move on to the next epoch
376                        let (mut tx, rx) = broadcast(1);
377                        tx.set_overflow(true);
378                        map_lock.insert(try_epoch, rx.deactivate());
379                        drop(map_lock);
380                        fetch_epochs.push((try_epoch, tx));
381                        try_epoch = EpochNumber::new(try_epoch.saturating_sub(1));
382                    },
383                }
384            };
385        }
386
387        let epochs = fetch_epochs.iter().map(|(e, _)| e).collect::<Vec<_>>();
388        tracing::warn!("Fetching stake tables for epochs: {epochs:?}");
389
390        // Iterate through the epochs we need to fetch in reverse, i.e. from the oldest to the newest
391        while let Some((current_fetch_epoch, tx)) = fetch_epochs.pop() {
392            match self.fetch_stake_table(current_fetch_epoch).await {
393                Ok(_) => {},
394                Err(err) => {
395                    fetch_epochs.push((current_fetch_epoch, tx));
396                    self.catchup_cleanup(epoch, epoch_tx, fetch_epochs, err);
397                    return;
398                },
399            };
400
401            // Signal the other tasks about the success. `fetch_stake_table`
402            // returned `Ok`, so a snapshot must be present. If it isn't,
403            // treat that as a catchup failure: push the in-flight epoch
404            // back and run cleanup so waiters get notified.
405            let Some(snapshot) = self.membership.snapshot(current_fetch_epoch) else {
406                let err = anytrace::error!(
407                    "snapshot for epoch {current_fetch_epoch} unavailable after fetch_stake_table"
408                );
409                fetch_epochs.push((current_fetch_epoch, tx));
410                self.catchup_cleanup(epoch, epoch_tx, fetch_epochs, err);
411                return;
412            };
413            let mem = EpochMembership {
414                coordinator: self.clone(),
415                snapshot: EpochMembershipSnapshot::Epoch {
416                    epoch: current_fetch_epoch,
417                    snapshot,
418                },
419            };
420            if let Ok(Some(res)) = tx.try_broadcast(Ok(mem)) {
421                tracing::warn!(
422                    "The catchup channel for epoch {} was overflown, dropped message {:?}",
423                    current_fetch_epoch,
424                    res.map(|em| em.epoch())
425                );
426            }
427
428            // Remove the epoch from the catchup map to indicate that the catchup is complete
429            self.catchup_map.lock().remove(&current_fetch_epoch);
430        }
431
432        let root_leaf = match self.fetch_stake_table(epoch).await {
433            Ok(root_leaf) => root_leaf,
434            Err(err) => {
435                tracing::error!("Failed to fetch stake table for epoch {epoch:?}: {err:?}");
436                self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
437                return;
438            },
439        };
440
441        match self.get_epoch_drb(epoch).await {
442            Ok(drb_result) => {
443                tracing::warn!(
444                    ?drb_result,
445                    "DRB result for epoch {epoch:?} retrieved from peers. Updating membership."
446                );
447                self.membership.add_drb_result(epoch, drb_result);
448            },
449            Err(err) => {
450                tracing::warn!(
451                    "Recalculating missing DRB result for epoch {}. Catchup failed with error: {}",
452                    epoch,
453                    err
454                );
455
456                let result = self.compute_drb_result(epoch, root_leaf).await;
457
458                log!(result);
459
460                if let Err(err) = result {
461                    self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
462                    return;
463                }
464            },
465        };
466
467        // Signal the other tasks about the success. As above, the snapshot
468        // must be present at this point — if not, treat as a catchup failure.
469        let Some(snapshot) = self.membership.snapshot(epoch) else {
470            let err = anytrace::error!(
471                "snapshot for epoch {epoch} unavailable after fetch_stake_table + DRB"
472            );
473            self.catchup_cleanup(epoch, epoch_tx.clone(), fetch_epochs, err);
474            return;
475        };
476        let mem = EpochMembership {
477            coordinator: self.clone(),
478            snapshot: EpochMembershipSnapshot::Epoch { epoch, snapshot },
479        };
480        if let Ok(Some(res)) = epoch_tx.try_broadcast(Ok(mem)) {
481            tracing::warn!(
482                "The catchup channel for epoch {} was overflown, dropped message {:?}",
483                epoch,
484                res.map(|em| em.epoch())
485            );
486        }
487
488        // Remove the epoch from the catchup map to indicate that the catchup is complete
489        self.catchup_map.lock().remove(&epoch);
490    }
491
492    /// Get the stake table for `epoch`, blocking on catchup if necessary.
493    ///
494    /// Unlike `stake_table_for_epoch`, this returns the result rather than
495    /// kicking off catchup and immediately returning an error. Used at startup
496    /// to drive the existing catchup chain synchronously before consensus is
497    /// running.
498    pub async fn wait_for_stake_table(&self, epoch: EpochNumber) -> Result<EpochMembership<TYPES>> {
499        match self.stake_table_for_epoch(Some(epoch)) {
500            Ok(mem) => Ok(mem),
501            Err(_) => self.wait_for_catchup(epoch).await,
502        }
503    }
504
505    /// Call this method if you think catchup is in progress for a given epoch
506    /// and you want to wait for it to finish and get the stake table.
507    /// If it's not, it will try to return the stake table if already available.
508    /// Returns an error if the catchup failed or the catchup is not in progress
509    /// and the stake table is not available.
510    pub async fn wait_for_catchup(&self, epoch: EpochNumber) -> Result<EpochMembership<TYPES>> {
511        let maybe_receiver = self
512            .catchup_map
513            .lock()
514            .get(&epoch)
515            .map(InactiveReceiver::activate_cloned);
516        let Some(mut rx) = maybe_receiver else {
517            // There is no catchup in progress, maybe the epoch is already finalized
518            if let Some(snapshot) = self.membership.snapshot(epoch) {
519                return Ok(EpochMembership {
520                    coordinator: self.clone(),
521                    snapshot: EpochMembershipSnapshot::Epoch { epoch, snapshot },
522                });
523            }
524            return Err(anytrace::error!(
525                "No catchup in progress for epoch {epoch} and we don't have a stake table for it"
526            ));
527        };
528        let Ok(Ok(mem)) = rx.recv_direct().await else {
529            return Err(anytrace::error!("Catchup for epoch {epoch} failed"));
530        };
531        Ok(mem)
532    }
533
534    /// Clean up after a failed catchup attempt.
535    ///
536    /// This method is called when a catchup attempt fails. It cleans up the state of the
537    /// `EpochMembershipCoordinator` by removing the failed epochs from the
538    /// `catchup_map` and broadcasting the error to any tasks that are waiting for the
539    /// catchup to complete.
540    fn catchup_cleanup(
541        &self,
542        req_epoch: EpochNumber,
543        epoch_tx: Sender<Result<EpochMembership<TYPES>>>,
544        mut cancel_epochs: Vec<EpochSender<TYPES>>,
545        err: Error,
546    ) {
547        // Cleanup in case of error
548        cancel_epochs.push((req_epoch, epoch_tx));
549
550        tracing::error!(
551            "catchup for epoch {req_epoch:?} failed: {err:?}. Canceling catchup for epochs: {:?}",
552            cancel_epochs.iter().map(|(e, _)| e).collect::<Vec<_>>()
553        );
554
555        {
556            let mut map_lock = self.catchup_map.lock();
557            for (epoch, _) in cancel_epochs.iter() {
558                // Remove the failed epochs from the catchup map
559                map_lock.remove(epoch);
560            }
561        }
562
563        for (cancel_epoch, tx) in cancel_epochs {
564            // Signal the other tasks about the failures
565            if let Ok(Some(res)) = tx.try_broadcast(Err(err.clone())) {
566                tracing::warn!(
567                    "The catchup channel for epoch {} was overflown during cleanup, dropped \
568                     message {:?}",
569                    cancel_epoch,
570                    res.map(|em| em.epoch())
571                );
572            }
573        }
574    }
575
576    /// A helper method to the `catchup` method.
577    ///
578    /// It tries to fetch the requested stake table from the root epoch,
579    /// and updates the membership accordingly.
580    ///
581    /// # Arguments
582    ///
583    /// * `epoch` - The epoch for which to fetch the stake table.
584    ///
585    /// # Returns
586    ///
587    /// * `Ok(Leaf2<TYPES>)` containing the epoch root leaf if successful.
588    /// * `Err(Error)` if the root membership or root leaf cannot be found, or if
589    ///   updating the membership fails.
590    async fn fetch_stake_table(&self, epoch: EpochNumber) -> Result<Leaf2<TYPES>> {
591        let root_epoch = EpochNumber::new(epoch.saturating_sub(2));
592        let Ok(root_membership) = self.stake_table_for_epoch(Some(root_epoch)) else {
593            return Err(anytrace::error!(
594                "We tried to fetch stake table for epoch {epoch:?} but we don't have its root \
595                 epoch {root_epoch:?}. This should not happen"
596            ));
597        };
598
599        // Get the epoch root headers and update our membership with them, finally sync them
600        // Verification of the root is handled in get_epoch_root_and_drb
601        let Ok(root_leaf) = root_membership.get_epoch_root().await else {
602            return Err(anytrace::error!(
603                "get epoch root leaf failed for epoch {root_epoch:?}"
604            ));
605        };
606
607        self.add_epoch_root(root_leaf.block_header().clone())
608            .await
609            .map_err(|e| {
610                anytrace::error!("Failed to add epoch root for epoch {epoch:?} to membership: {e}")
611            })?;
612
613        Ok(root_leaf)
614    }
615
616    pub async fn compute_drb_result(
617        &self,
618        epoch: EpochNumber,
619        root_leaf: Leaf2<TYPES>,
620    ) -> Result<DrbResult> {
621        let cancel_token = {
622            let mut drb_calculation_map_lock = self.drb_calculation_map.lock();
623
624            if drb_calculation_map_lock.contains(&epoch) {
625                return Err(anytrace::debug!(
626                    "DRB calculation for epoch {} already in progress",
627                    epoch
628                ));
629            }
630            drb_calculation_map_lock.insert(epoch);
631
632            let token = CancellationToken::new();
633            self.drb_cancel_map.lock().insert(epoch, token.clone());
634            token
635        };
636
637        let Ok(drb_seed_input_vec) = bincode::serialize(&root_leaf.justify_qc().signatures) else {
638            self.clear_drb_state(epoch);
639            return Err(anytrace::error!(
640                "Failed to serialize the QC signature for leaf {root_leaf:?}"
641            ));
642        };
643
644        let Some(drb_difficulty_selector) = self.drb_difficulty_selector.read().clone() else {
645            self.clear_drb_state(epoch);
646            return Err(anytrace::error!(
647                "The DRB difficulty selector is missing from the epoch membership coordinator. \
648                 This node will not be able to spawn any DRB calculation tasks from catchup."
649            ));
650        };
651
652        let drb_difficulty = drb_difficulty_selector(root_leaf.block_header().version()).await;
653
654        let mut drb_seed_input = [0u8; 32];
655
656        if root_leaf.block_header().version() >= DRB_FIX_VERSION {
657            drb_seed_input = Sha256::digest(&drb_seed_input_vec).into();
658        } else {
659            let len = drb_seed_input_vec.len().min(32);
660            drb_seed_input[..len].copy_from_slice(&drb_seed_input_vec[..len]);
661        }
662
663        let drb_input = DrbInput {
664            epoch: *epoch,
665            iteration: 0,
666            value: drb_seed_input,
667            difficulty_level: drb_difficulty,
668        };
669
670        let store_drb_progress_fn = self.store_drb_progress_fn.clone();
671        let load_drb_progress_fn = self.load_drb_progress_fn.clone();
672
673        let drb = match compute_drb_result(
674            drb_input,
675            store_drb_progress_fn,
676            load_drb_progress_fn,
677            cancel_token,
678        )
679        .await
680        {
681            Some(drb) => drb,
682            None => {
683                self.clear_drb_state(epoch);
684                return self.get_epoch_drb(epoch).await.map_err(|e| {
685                    anytrace::error!(
686                        "DRB calculation for epoch {epoch} was cancelled but no externally \
687                         supplied result is available: {e}"
688                    )
689                });
690            },
691        };
692
693        self.clear_drb_state(epoch);
694
695        tracing::info!("Writing drb result from catchup to storage for epoch {epoch}: {drb:?}");
696        if let Err(e) = (self.store_drb_result_fn)(epoch, drb).await {
697            tracing::warn!("Failed to add drb result to storage: {e}");
698        }
699        self.membership.add_drb_result(epoch, drb);
700
701        Ok(drb)
702    }
703
704    /// Supply a DRB result obtained from an external source (e.g. a decided
705    /// leaf carrying `next_drb_result`). Adds the result to membership,
706    /// persists it to storage, and cancels any in-flight local computation
707    /// for `epoch`.
708    ///
709    /// If the stake table for `epoch` has not yet been loaded (e.g. the async
710    /// catchup that registers it is still in flight), this logs an error and
711    /// returns; the in-flight catchup will compute the DRB itself once it
712    /// completes.
713    pub fn supply_drb(&self, epoch: EpochNumber, drb: DrbResult) {
714        if self.membership.snapshot(epoch).is_none() {
715            tracing::error!(
716                "supply_drb called for epoch {epoch} but stake table not yet loaded; dropping \
717                 externally-supplied DRB and relying on in-flight catchup"
718            );
719            return;
720        }
721        self.membership.add_drb_result(epoch, drb);
722        let maybe_token = self.drb_cancel_map.lock().remove(&epoch);
723        if let Some(token) = maybe_token {
724            token.cancel();
725        }
726        let store_drb_result_fn = self.store_drb_result_fn.clone();
727        tokio::spawn(async move {
728            tracing::info!(
729                "Writing externally supplied drb result to storage for epoch {epoch}: {drb:?}"
730            );
731            if let Err(e) = store_drb_result_fn(epoch, drb).await {
732                tracing::warn!("Failed to add externally supplied drb result to storage: {e}");
733            }
734        });
735    }
736
737    /// Remove per-epoch DRB bookkeeping after a computation finishes or is
738    /// cancelled. Safe to call multiple times.
739    fn clear_drb_state(&self, epoch: EpochNumber) {
740        self.drb_calculation_map.lock().remove(&epoch);
741        self.drb_cancel_map.lock().remove(&epoch);
742    }
743
744    /// Cancel all in-flight DRB calculations (e.g. on shutdown).
745    pub fn cancel_all_drb(&self) {
746        let tokens: Vec<_> = self.drb_cancel_map.lock().drain().map(|(_, t)| t).collect();
747        for token in tokens {
748            token.cancel();
749        }
750    }
751}
752
753fn spawn_catchup<T: NodeType>(
754    coordinator: EpochMembershipCoordinator<T>,
755    epoch: EpochNumber,
756    epoch_tx: Sender<Result<EpochMembership<T>>>,
757) {
758    tokio::spawn(async move {
759        coordinator.clone().catchup(epoch, epoch_tx).await;
760    });
761}
762
763/// Wrapper around a membership that holds a captured snapshot for a given
764/// epoch (or the pre-epoch state). All accessors observe one consistent
765/// view because the snapshot is held inline.
766pub struct EpochMembership<TYPES: NodeType> {
767    /// The captured snapshot, either per-epoch or pre-epoch.
768    snapshot: EpochMembershipSnapshot<TYPES>,
769    /// Underlying coordinator, retained so navigation methods like
770    /// `next_epoch` can construct fresh snapshots.
771    pub coordinator: EpochMembershipCoordinator<TYPES>,
772}
773
774enum EpochMembershipSnapshot<TYPES: NodeType> {
775    Epoch {
776        epoch: EpochNumber,
777        snapshot: <TYPES::Membership as Membership<TYPES>>::Snapshot,
778    },
779    NonEpoch(<TYPES::Membership as Membership<TYPES>>::NonEpochSnapshot),
780}
781
782impl<TYPES: NodeType> Clone for EpochMembershipSnapshot<TYPES> {
783    fn clone(&self) -> Self {
784        match self {
785            Self::Epoch { epoch, snapshot } => Self::Epoch {
786                epoch: *epoch,
787                snapshot: snapshot.clone(),
788            },
789            Self::NonEpoch(s) => Self::NonEpoch(s.clone()),
790        }
791    }
792}
793
794impl<TYPES: NodeType> Clone for EpochMembership<TYPES> {
795    fn clone(&self) -> Self {
796        Self {
797            coordinator: self.coordinator.clone(),
798            snapshot: self.snapshot.clone(),
799        }
800    }
801}
802
803impl<TYPES: NodeType> EpochMembership<TYPES> {
804    pub fn epoch(&self) -> Option<EpochNumber> {
805        match &self.snapshot {
806            EpochMembershipSnapshot::Epoch { epoch, .. } => Some(*epoch),
807            EpochMembershipSnapshot::NonEpoch(_) => None,
808        }
809    }
810
811    pub fn next_epoch(&self) -> Result<Self> {
812        let epoch = self
813            .epoch()
814            .ok_or_else(|| anytrace::error!("No next epoch because epoch is None"))?;
815        self.coordinator.membership_for_epoch(Some(epoch + 1))
816    }
817
818    pub fn next_epoch_stake_table(&self) -> Result<Self> {
819        let epoch = self
820            .epoch()
821            .ok_or_else(|| anytrace::error!("No next epoch because epoch is None"))?;
822        self.coordinator.stake_table_for_epoch(Some(epoch + 1))
823    }
824
825    pub fn get_new_epoch(&self, epoch: Option<EpochNumber>) -> Result<Self> {
826        self.coordinator.membership_for_epoch(epoch)
827    }
828
829    async fn get_epoch_root(&self) -> anyhow::Result<Leaf2<TYPES>> {
830        let Some(epoch) = self.epoch() else {
831            anyhow::bail!("Cannot get root for None epoch");
832        };
833        let leaf = self.coordinator.get_epoch_root(epoch).await?;
834        Ok(leaf)
835    }
836
837    pub async fn get_epoch_drb(&self) -> Result<DrbResult> {
838        let Some(epoch) = self.epoch() else {
839            return Err(anytrace::warn!("Cannot get drb for None epoch"));
840        };
841        self.coordinator.get_epoch_drb(epoch).await.wrap()
842    }
843
844    /// Borrow the per-epoch snapshot, or `None` for the pre-epoch case.
845    pub fn snapshot(&self) -> Option<&<TYPES::Membership as Membership<TYPES>>::Snapshot> {
846        match &self.snapshot {
847            EpochMembershipSnapshot::Epoch { snapshot, .. } => Some(snapshot),
848            EpochMembershipSnapshot::NonEpoch(_) => None,
849        }
850    }
851
852    /// Borrow the pre-epoch snapshot, or `None` if this is a per-epoch
853    /// membership.
854    pub fn non_epoch_snapshot(
855        &self,
856    ) -> Option<&<TYPES::Membership as Membership<TYPES>>::NonEpochSnapshot> {
857        match &self.snapshot {
858            EpochMembershipSnapshot::NonEpoch(s) => Some(s),
859            EpochMembershipSnapshot::Epoch { .. } => None,
860        }
861    }
862
863    /// Add the DRB result for this epoch to the membership.
864    pub fn add_drb_result(&self, drb_result: DrbResult) {
865        if let Some(epoch) = self.epoch() {
866            self.coordinator
867                .membership
868                .add_drb_result(epoch, drb_result);
869        }
870    }
871
872    // ---------------------------------------------------------------------
873    // Single-call convenience accessors. Each delegates to whichever
874    // snapshot was captured at construction time, so a single accessor
875    // call observes one consistent view. For *sequences* of related reads
876    // that must observe the same view, take a snapshot via
877    // [`Self::snapshot`] / [`Self::non_epoch_snapshot`] and call methods
878    // on it directly.
879    // ---------------------------------------------------------------------
880
881    pub fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<TYPES>> + Send {
882        match &self.snapshot {
883            EpochMembershipSnapshot::Epoch { snapshot, .. } => Either::Left(snapshot.stake_table()),
884            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.stake_table()),
885        }
886    }
887
888    pub fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<TYPES>> + Send {
889        match &self.snapshot {
890            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
891                Either::Left(snapshot.da_stake_table())
892            },
893            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.da_stake_table()),
894        }
895    }
896
897    pub fn committee_members(
898        &self,
899        view: ViewNumber,
900    ) -> impl ExactSizeIterator<Item = &TYPES::SignatureKey> + Send {
901        match &self.snapshot {
902            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
903                Either::Left(snapshot.committee_members(view))
904            },
905            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.committee_members(view)),
906        }
907    }
908
909    pub fn da_committee_members(
910        &self,
911        view: ViewNumber,
912    ) -> impl ExactSizeIterator<Item = &TYPES::SignatureKey> + Send {
913        match &self.snapshot {
914            EpochMembershipSnapshot::Epoch { snapshot, .. } => {
915                Either::Left(snapshot.da_committee_members(view))
916            },
917            EpochMembershipSnapshot::NonEpoch(s) => Either::Right(s.da_committee_members(view)),
918        }
919    }
920
921    pub fn stake(&self, key: &TYPES::SignatureKey) -> Option<PeerConfig<TYPES>> {
922        match &self.snapshot {
923            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.stake(key),
924            EpochMembershipSnapshot::NonEpoch(s) => s.stake(key),
925        }
926    }
927
928    pub fn da_stake(&self, key: &TYPES::SignatureKey) -> Option<PeerConfig<TYPES>> {
929        match &self.snapshot {
930            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_stake(key),
931            EpochMembershipSnapshot::NonEpoch(s) => s.da_stake(key),
932        }
933    }
934
935    pub fn has_stake(&self, key: &TYPES::SignatureKey) -> bool {
936        match &self.snapshot {
937            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.has_stake(key),
938            EpochMembershipSnapshot::NonEpoch(s) => s.has_stake(key),
939        }
940    }
941
942    pub fn has_da_stake(&self, key: &TYPES::SignatureKey) -> bool {
943        match &self.snapshot {
944            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.has_da_stake(key),
945            EpochMembershipSnapshot::NonEpoch(s) => s.has_da_stake(key),
946        }
947    }
948
949    /// The leader for `view`, returning a HotShot-internal error type.
950    ///
951    /// # Errors
952    ///
953    /// Returns an error if the leader cannot be calculated.
954    pub fn leader(&self, view: ViewNumber) -> Result<TYPES::SignatureKey> {
955        match &self.snapshot {
956            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.leader(view),
957            EpochMembershipSnapshot::NonEpoch(s) => s.leader(view),
958        }
959    }
960
961    /// The leader for `view`, returning the membership-impl error type.
962    ///
963    /// # Errors
964    ///
965    /// Returns the membership-impl error if the leader cannot be calculated.
966    pub fn lookup_leader(
967        &self,
968        view: ViewNumber,
969    ) -> std::result::Result<
970        TYPES::SignatureKey,
971        <<TYPES as NodeType>::Membership as Membership<TYPES>>::Error,
972    > {
973        match &self.snapshot {
974            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.lookup_leader(view),
975            EpochMembershipSnapshot::NonEpoch(s) => s.lookup_leader(view),
976        }
977    }
978
979    pub fn total_nodes(&self) -> usize {
980        match &self.snapshot {
981            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.total_nodes(),
982            EpochMembershipSnapshot::NonEpoch(s) => s.total_nodes(),
983        }
984    }
985
986    pub fn da_total_nodes(&self) -> usize {
987        match &self.snapshot {
988            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_total_nodes(),
989            EpochMembershipSnapshot::NonEpoch(s) => s.da_total_nodes(),
990        }
991    }
992
993    pub fn success_threshold(&self) -> U256 {
994        match &self.snapshot {
995            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.success_threshold(),
996            EpochMembershipSnapshot::NonEpoch(s) => s.success_threshold(),
997        }
998    }
999
1000    pub fn da_success_threshold(&self) -> U256 {
1001        match &self.snapshot {
1002            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.da_success_threshold(),
1003            EpochMembershipSnapshot::NonEpoch(s) => s.da_success_threshold(),
1004        }
1005    }
1006
1007    pub fn failure_threshold(&self) -> U256 {
1008        match &self.snapshot {
1009            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.failure_threshold(),
1010            EpochMembershipSnapshot::NonEpoch(s) => s.failure_threshold(),
1011        }
1012    }
1013
1014    pub fn upgrade_threshold(&self) -> U256 {
1015        match &self.snapshot {
1016            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.upgrade_threshold(),
1017            EpochMembershipSnapshot::NonEpoch(s) => s.upgrade_threshold(),
1018        }
1019    }
1020
1021    pub fn stake_table_hash(&self) -> Option<Commitment<SnapshotStakeTableHash<TYPES>>> {
1022        match &self.snapshot {
1023            EpochMembershipSnapshot::Epoch { snapshot, .. } => snapshot.stake_table_hash(),
1024            EpochMembershipSnapshot::NonEpoch(_) => None,
1025        }
1026    }
1027}