Skip to main content

hotshot_example_types/membership/
strict_membership.rs

1use std::{
2    collections::HashSet,
3    fmt::{self, Debug},
4    sync::Arc,
5};
6
7use alloy::primitives::U256;
8use anyhow::anyhow;
9use async_broadcast::Receiver;
10use async_lock::RwLock as AsyncRwLock;
11use hotshot_types::{
12    PeerConfig,
13    data::{BlockNumber, EpochNumber, Leaf2, ViewNumber},
14    drb::DrbResult,
15    epoch_membership::EpochMembershipCoordinator,
16    event::Event,
17    traits::{
18        block_contents::BlockHeader,
19        election::{Membership, MembershipSnapshot, NoStakeTableHash, NonEpochMembershipSnapshot},
20        leaf_fetcher_network::LeafFetcherNetwork,
21        node_implementation::NodeType,
22        signature_key::StakeTableEntryType,
23    },
24    utils::{epoch_from_block_number, root_block_in_epoch, transition_block_for_epoch},
25};
26use parking_lot::RwLock;
27
28use crate::{
29    membership::{TestableMembership, fetcher::Leaf2Fetcher, stake_table::TestStakeTable},
30    storage_types::TestStorage,
31};
32
33#[derive(Clone)]
34pub struct StrictMembership<T, S>
35where
36    T: NodeType,
37    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
38{
39    inner: Arc<RwLock<Inner<T, S>>>,
40    epoch_height: BlockNumber,
41}
42
43struct Inner<T: NodeType, S> {
44    table: S,
45    epochs: HashSet<EpochNumber>,
46    drbs: HashSet<EpochNumber>,
47    fetcher: Option<Arc<AsyncRwLock<Leaf2Fetcher<T>>>>,
48}
49
50impl<T, S> Debug for StrictMembership<T, S>
51where
52    T: NodeType,
53    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
54{
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
56        let inner = self.inner.read();
57        f.debug_struct("StrictMembership")
58            .field("table", &inner.table)
59            .field("epochs", &inner.epochs)
60            .field("drbs", &inner.drbs)
61            .finish()
62    }
63}
64
65impl<T, S> TestableMembership<T> for StrictMembership<T, S>
66where
67    T: NodeType,
68    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
69{
70    fn new(
71        quorum_members: Vec<PeerConfig<T>>,
72        da_members: Vec<PeerConfig<T>>,
73        _public_key: T::SignatureKey,
74        epoch_height: u64,
75    ) -> Self {
76        Self {
77            inner: Arc::new(RwLock::new(Inner {
78                table: TestStakeTable::new(
79                    quorum_members.into_iter().map(Into::into).collect(),
80                    da_members.into_iter().map(Into::into).collect(),
81                ),
82                epochs: HashSet::new(),
83                drbs: HashSet::new(),
84                fetcher: None,
85            })),
86            epoch_height: epoch_height.into(),
87        }
88    }
89
90    fn set_leaf_fetcher(
91        &self,
92        network: Arc<dyn LeafFetcherNetwork<T>>,
93        storage: TestStorage<T>,
94        public_key: T::SignatureKey,
95        channel: Receiver<Event<T>>,
96    ) {
97        let mut fetcher = Leaf2Fetcher::new(network, storage, public_key);
98        fetcher.set_external_channel(channel);
99        self.inner.write().fetcher = Some(Arc::new(AsyncRwLock::new(fetcher)));
100    }
101}
102
103impl<T, S> StrictMembership<T, S>
104where
105    T: NodeType,
106    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
107{
108    /// Register a quorum committee effective from `first_epoch` (inclusive).
109    /// Test setup only — production membership derives quorum changes from
110    /// epoch roots.
111    pub fn add_quorum_committee(&self, first_epoch: EpochNumber, committee: Vec<PeerConfig<T>>) {
112        self.inner.write().table.add_quorum_committee(
113            *first_epoch,
114            committee.into_iter().map(Into::into).collect(),
115        );
116    }
117
118    /// Mark `epoch` as having a stake table and DRB result, bypassing the
119    /// epoch-root/DRB pipeline. For tests that need an epoch snapshot without
120    /// driving consensus to the epoch root.
121    pub fn register_epoch(&self, epoch: EpochNumber, drb: DrbResult) {
122        let mut inner = self.inner.write();
123        inner.epochs.insert(epoch);
124        inner.drbs.insert(epoch);
125        inner.table.add_epoch_root(*epoch);
126        inner.table.add_drb_result(*epoch, drb);
127    }
128}
129
130impl<T: NodeType, S> Inner<T, S> {
131    fn assert_has_stake_table(&self, epoch: Option<EpochNumber>) {
132        let Some(epoch) = epoch else {
133            return;
134        };
135        assert!(
136            self.epochs.contains(&epoch),
137            "Failed stake table check for epoch {epoch}"
138        );
139    }
140}
141
142impl<T, S> Membership<T> for StrictMembership<T, S>
143where
144    T: NodeType,
145    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
146{
147    type Error = StrictMembershipError;
148    type Snapshot = StrictEpochSnapshot<T, S>;
149    type NonEpochSnapshot = StrictNonEpochSnapshot<T, S>;
150
151    fn snapshot(&self, epoch: EpochNumber) -> Option<Self::Snapshot> {
152        let inner = self.inner.read();
153        if !inner.epochs.contains(&epoch) {
154            return None;
155        }
156        let has_drb = inner.drbs.contains(&epoch);
157        let first_epoch = inner.table.first_epoch().map(EpochNumber::new);
158        Some(StrictEpochSnapshot::build(
159            epoch,
160            first_epoch,
161            has_drb,
162            inner.table.clone(),
163        ))
164    }
165
166    fn non_epoch_snapshot(&self) -> Self::NonEpochSnapshot {
167        StrictNonEpochSnapshot::build(self.inner.read().table.clone())
168    }
169
170    fn add_drb_result(&self, e: EpochNumber, drb: DrbResult) {
171        let mut inner = self.inner.write();
172        inner.assert_has_stake_table(Some(e));
173        inner.drbs.insert(e);
174        inner.table.add_drb_result(*e, drb);
175    }
176
177    fn first_epoch(&self) -> Option<EpochNumber> {
178        self.inner.read().table.first_epoch().map(EpochNumber::new)
179    }
180
181    fn set_first_epoch(&self, e: EpochNumber, initial_drb_result: DrbResult) {
182        let mut inner = self.inner.write();
183        inner.epochs.insert(e);
184        inner.epochs.insert(e + 1);
185
186        inner.drbs.insert(e);
187        inner.drbs.insert(e + 1);
188
189        inner.table.set_first_epoch(*e, initial_drb_result);
190    }
191
192    async fn add_epoch_root(
193        &self,
194        hdr: T::BlockHeader,
195        _coordinator: &EpochMembershipCoordinator<T>,
196    ) -> Result<(), Self::Error> {
197        let epoch = epoch_from_block_number(hdr.block_number(), *self.epoch_height) + 2;
198
199        let mut inner = self.inner.write();
200        inner.epochs.insert(EpochNumber::new(epoch));
201        inner.table.add_epoch_root(epoch);
202
203        Ok(())
204    }
205
206    async fn get_epoch_root(
207        &self,
208        e: EpochNumber,
209        _coordinator: &EpochMembershipCoordinator<T>,
210    ) -> Result<Leaf2<T>, Self::Error> {
211        let block_height = root_block_in_epoch(*e, *self.epoch_height);
212
213        let (stake_table, fetcher) = {
214            let inner = self.inner.read();
215            let table = inner.table.stake_table(Some(*e));
216            let fetcher = inner
217                .fetcher
218                .clone()
219                .expect("get_epoch_root called before set_leaf_fetcher_network");
220            (table, fetcher)
221        };
222
223        for node in stake_table {
224            if let Ok(leaf) = fetcher
225                .read()
226                .await
227                .fetch_leaf(block_height, node.signature_key)
228                .await
229            {
230                return Ok(leaf);
231            }
232        }
233
234        Err(anyhow!("Failed to fetch epoch root from any peer").into())
235    }
236
237    async fn get_epoch_drb(
238        &self,
239        e: EpochNumber,
240        _coordinator: &EpochMembershipCoordinator<T>,
241    ) -> Result<DrbResult, Self::Error> {
242        let epoch_height = self.epoch_height;
243
244        let (epoch_drb, fetcher) = {
245            let state = self.inner.read();
246            let drb = state.table.get_epoch_drb(*e);
247            let fetcher = state.fetcher.clone();
248            (drb, fetcher)
249        };
250
251        if let Ok(drb_result) = epoch_drb {
252            Ok(drb_result)
253        } else {
254            let previous_epoch = match e.checked_sub(1) {
255                Some(epoch) => epoch,
256                None => {
257                    return Err(anyhow!("Missing initial DRB result for epoch {e:?}").into());
258                },
259            };
260
261            let drb_block_height = transition_block_for_epoch(previous_epoch, *epoch_height);
262            let stake_table = self.inner.read().table.stake_table(Some(previous_epoch));
263            let fetcher = fetcher.expect("get_epoch_drb called before set_leaf_fetcher_network");
264
265            let mut drb_leaf = None;
266
267            for node in stake_table {
268                if let Ok(leaf) = fetcher
269                    .read()
270                    .await
271                    .fetch_leaf(drb_block_height, node.signature_key)
272                    .await
273                {
274                    drb_leaf = Some(leaf);
275                    break;
276                }
277            }
278
279            match drb_leaf {
280                Some(leaf) => Ok(leaf.next_drb_result.expect(
281                    "We fetched a leaf that is missing a DRB result. This should be impossible.",
282                )),
283                None => Err(anyhow!(
284                    "Failed to fetch leaf from all nodes. Height: {drb_block_height}"
285                )
286                .into()),
287            }
288        }
289    }
290
291    fn add_da_committee(&self, first_epoch: EpochNumber, committee: Vec<PeerConfig<T>>) {
292        self.inner.write().table.add_da_committee(
293            *first_epoch,
294            committee.into_iter().map(Into::into).collect(),
295        );
296    }
297}
298
299#[derive(Debug, thiserror::Error)]
300#[error("strict membership error: {0}")]
301pub struct StrictMembershipError(#[from] anyhow::Error);
302
303/// Per-epoch snapshot for `StrictMembership`.
304///
305/// Materializes the stake-table views at construction time so accessors can
306/// return borrowed iterators.
307pub struct StrictEpochSnapshot<T, S>
308where
309    T: NodeType,
310    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
311{
312    epoch: EpochNumber,
313    first_epoch: Option<EpochNumber>,
314    has_drb: bool,
315    stake_table: Vec<PeerConfig<T>>,
316    da_stake_table: Vec<PeerConfig<T>>,
317    committee_keys: Vec<T::SignatureKey>,
318    da_committee_keys: Vec<T::SignatureKey>,
319    table: S,
320    _phantom: std::marker::PhantomData<T>,
321}
322
323impl<T, S> StrictEpochSnapshot<T, S>
324where
325    T: NodeType,
326    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
327{
328    fn build(
329        epoch: EpochNumber,
330        first_epoch: Option<EpochNumber>,
331        has_drb: bool,
332        table: S,
333    ) -> Self {
334        let stake_entries = table.stake_table(Some(*epoch));
335        let da_entries = table.da_stake_table(Some(*epoch));
336        let committee_keys = stake_entries
337            .iter()
338            .map(|e| e.signature_key.clone())
339            .collect();
340        let da_committee_keys = da_entries.iter().map(|e| e.signature_key.clone()).collect();
341        let stake_table = stake_entries.into_iter().map(Into::into).collect();
342        let da_stake_table = da_entries.into_iter().map(Into::into).collect();
343        Self {
344            epoch,
345            first_epoch,
346            has_drb,
347            stake_table,
348            da_stake_table,
349            committee_keys,
350            da_committee_keys,
351            table,
352            _phantom: std::marker::PhantomData,
353        }
354    }
355}
356
357impl<T, S> Clone for StrictEpochSnapshot<T, S>
358where
359    T: NodeType,
360    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
361{
362    fn clone(&self) -> Self {
363        Self {
364            epoch: self.epoch,
365            first_epoch: self.first_epoch,
366            has_drb: self.has_drb,
367            stake_table: self.stake_table.clone(),
368            da_stake_table: self.da_stake_table.clone(),
369            committee_keys: self.committee_keys.clone(),
370            da_committee_keys: self.da_committee_keys.clone(),
371            table: self.table.clone(),
372            _phantom: std::marker::PhantomData,
373        }
374    }
375}
376
377impl<T, S> Debug for StrictEpochSnapshot<T, S>
378where
379    T: NodeType,
380    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
381{
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
383        f.debug_struct("StrictEpochSnapshot")
384            .field("epoch", &self.epoch)
385            .field("first_epoch", &self.first_epoch)
386            .field("has_drb", &self.has_drb)
387            .field("table", &self.table)
388            .finish()
389    }
390}
391
392impl<T, S> MembershipSnapshot<T> for StrictEpochSnapshot<T, S>
393where
394    T: NodeType,
395    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
396{
397    type Error = StrictMembershipError;
398    type StakeTableHash = NoStakeTableHash;
399
400    fn epoch(&self) -> EpochNumber {
401        self.epoch
402    }
403
404    fn first_epoch(&self) -> Option<EpochNumber> {
405        self.first_epoch
406    }
407
408    fn has_drb(&self) -> bool {
409        self.has_drb
410    }
411
412    fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<T>> + Send {
413        self.stake_table.iter()
414    }
415
416    fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<T>> + Send {
417        self.da_stake_table.iter()
418    }
419
420    fn committee_members(
421        &self,
422        _: ViewNumber,
423    ) -> impl ExactSizeIterator<Item = &T::SignatureKey> + Send {
424        self.committee_keys.iter()
425    }
426
427    fn da_committee_members(
428        &self,
429        _: ViewNumber,
430    ) -> impl ExactSizeIterator<Item = &T::SignatureKey> + Send {
431        self.da_committee_keys.iter()
432    }
433
434    fn stake(&self, key: &T::SignatureKey) -> Option<PeerConfig<T>> {
435        self.table
436            .stake(key.clone(), Some(*self.epoch))
437            .map(Into::into)
438    }
439
440    fn da_stake(&self, key: &T::SignatureKey) -> Option<PeerConfig<T>> {
441        self.table
442            .da_stake(key.clone(), Some(*self.epoch))
443            .map(Into::into)
444    }
445
446    fn has_stake(&self, key: &T::SignatureKey) -> bool {
447        self.stake(key)
448            .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
449    }
450
451    fn has_da_stake(&self, key: &T::SignatureKey) -> bool {
452        self.da_stake(key)
453            .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
454    }
455
456    fn lookup_leader(&self, view: ViewNumber) -> Result<T::SignatureKey, Self::Error> {
457        Ok(self.table.lookup_leader(*view, Some(*self.epoch))?)
458    }
459}
460
461/// Pre-epoch snapshot for `StrictMembership`. Materializes views at
462/// construction so accessors can return borrowed iterators.
463pub struct StrictNonEpochSnapshot<T, S>
464where
465    T: NodeType,
466    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
467{
468    stake_table: Vec<PeerConfig<T>>,
469    da_stake_table: Vec<PeerConfig<T>>,
470    committee_keys: Vec<T::SignatureKey>,
471    da_committee_keys: Vec<T::SignatureKey>,
472    table: S,
473    _phantom: std::marker::PhantomData<T>,
474}
475
476impl<T, S> StrictNonEpochSnapshot<T, S>
477where
478    T: NodeType,
479    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
480{
481    fn build(table: S) -> Self {
482        let stake_entries = table.stake_table(None);
483        let da_entries = table.da_stake_table(None);
484        let committee_keys = stake_entries
485            .iter()
486            .map(|e| e.signature_key.clone())
487            .collect();
488        let da_committee_keys = da_entries.iter().map(|e| e.signature_key.clone()).collect();
489        let stake_table = stake_entries.into_iter().map(Into::into).collect();
490        let da_stake_table = da_entries.into_iter().map(Into::into).collect();
491        Self {
492            stake_table,
493            da_stake_table,
494            committee_keys,
495            da_committee_keys,
496            table,
497            _phantom: std::marker::PhantomData,
498        }
499    }
500}
501
502impl<T, S> Clone for StrictNonEpochSnapshot<T, S>
503where
504    T: NodeType,
505    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
506{
507    fn clone(&self) -> Self {
508        Self {
509            stake_table: self.stake_table.clone(),
510            da_stake_table: self.da_stake_table.clone(),
511            committee_keys: self.committee_keys.clone(),
512            da_committee_keys: self.da_committee_keys.clone(),
513            table: self.table.clone(),
514            _phantom: std::marker::PhantomData,
515        }
516    }
517}
518
519impl<T, S> Debug for StrictNonEpochSnapshot<T, S>
520where
521    T: NodeType,
522    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
523{
524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
525        f.debug_struct("StrictNonEpochSnapshot")
526            .field("table", &self.table)
527            .finish()
528    }
529}
530
531impl<T, S> NonEpochMembershipSnapshot<T> for StrictNonEpochSnapshot<T, S>
532where
533    T: NodeType,
534    S: TestStakeTable<T::SignatureKey, T::StateSignatureKey>,
535{
536    type Error = StrictMembershipError;
537
538    fn stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<T>> + Send + '_ {
539        self.stake_table.iter()
540    }
541
542    fn da_stake_table(&self) -> impl ExactSizeIterator<Item = &PeerConfig<T>> + Send + '_ {
543        self.da_stake_table.iter()
544    }
545
546    fn committee_members(
547        &self,
548        _: ViewNumber,
549    ) -> impl ExactSizeIterator<Item = &T::SignatureKey> + Send + '_ {
550        self.committee_keys.iter()
551    }
552
553    fn da_committee_members(
554        &self,
555        _: ViewNumber,
556    ) -> impl ExactSizeIterator<Item = &T::SignatureKey> + Send + '_ {
557        self.da_committee_keys.iter()
558    }
559
560    fn stake(&self, key: &T::SignatureKey) -> Option<PeerConfig<T>> {
561        self.table.stake(key.clone(), None).map(Into::into)
562    }
563
564    fn da_stake(&self, key: &T::SignatureKey) -> Option<PeerConfig<T>> {
565        self.table.da_stake(key.clone(), None).map(Into::into)
566    }
567
568    fn has_stake(&self, key: &T::SignatureKey) -> bool {
569        self.stake(key)
570            .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
571    }
572
573    fn has_da_stake(&self, key: &T::SignatureKey) -> bool {
574        self.da_stake(key)
575            .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
576    }
577
578    fn lookup_leader(&self, view: ViewNumber) -> Result<T::SignatureKey, Self::Error> {
579        Ok(self.table.lookup_leader(*view, None)?)
580    }
581}