hotshot/traits/election/
randomized_committee_members.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6use std::{
7    collections::{BTreeMap, BTreeSet},
8    marker::PhantomData,
9};
10
11use alloy::primitives::U256;
12use anyhow::Context;
13use hotshot_types::{
14    drb::DrbResult,
15    stake_table::HSStakeTable,
16    traits::{
17        election::{Membership, NoStakeTableHash},
18        node_implementation::{ConsensusTime, NodeType},
19        signature_key::{SignatureKey, StakeTableEntryType},
20    },
21    PeerConfig,
22};
23use hotshot_utils::anytrace::Result;
24use rand::{rngs::StdRng, Rng};
25use tracing::error;
26
27use crate::{traits::election::helpers::QuorumFilterConfig, Arc, RwLock};
28
29#[derive(Clone, Debug, Eq, PartialEq, Hash)]
30/// The static committee election
31pub struct RandomizedCommitteeMembers<
32    T: NodeType,
33    CONFIG: QuorumFilterConfig,
34    DaConfig: QuorumFilterConfig,
35> {
36    /// The nodes eligible for leadership.
37    /// NOTE: This is currently a hack because the DA leader needs to be the quorum
38    /// leader but without voting rights.
39    eligible_leaders: Vec<PeerConfig<T>>,
40
41    /// The nodes on the committee and their stake
42    stake_table: Vec<PeerConfig<T>>,
43
44    /// The nodes on the da committee and their stake
45    da_stake_table: Vec<PeerConfig<T>>,
46
47    /// The nodes on the committee and their stake, indexed by public key
48    indexed_stake_table: BTreeMap<T::SignatureKey, PeerConfig<T>>,
49
50    /// The nodes on the da committee and their stake, indexed by public key
51    indexed_da_stake_table: BTreeMap<T::SignatureKey, PeerConfig<T>>,
52
53    /// The first epoch which will be encountered. For testing, will panic if an epoch-carrying function is called
54    /// when first_epoch is None or is Some greater than that epoch.
55    first_epoch: Option<T::Epoch>,
56
57    /// `DrbResult`s indexed by epoch
58    drb_results: BTreeMap<T::Epoch, DrbResult>,
59
60    /// Phantom
61    _pd: PhantomData<CONFIG>,
62
63    /// Phantom
64    _da_pd: PhantomData<DaConfig>,
65}
66
67impl<TYPES: NodeType, CONFIG: QuorumFilterConfig, DaConfig: QuorumFilterConfig>
68    RandomizedCommitteeMembers<TYPES, CONFIG, DaConfig>
69{
70    /// Creates a set of indices into the stake_table which reference the nodes selected for this epoch's committee
71    fn make_quorum_filter(&self, epoch: <TYPES as NodeType>::Epoch) -> BTreeSet<usize> {
72        CONFIG::execute(epoch.u64(), self.stake_table.len())
73    }
74
75    /// Creates a set of indices into the da_stake_table which reference the nodes selected for this epoch's da committee
76    fn make_da_quorum_filter(&self, epoch: <TYPES as NodeType>::Epoch) -> BTreeSet<usize> {
77        DaConfig::execute(epoch.u64(), self.da_stake_table.len())
78    }
79
80    /// Writes the offsets used for the quorum filter and da_quorum filter to stdout
81    fn debug_display_offsets(&self) {
82        /// Ensures that the quorum filters are only displayed once
83        static START: std::sync::Once = std::sync::Once::new();
84
85        START.call_once(|| {
86            error!(
87                "{} offsets for Quorum filter:",
88                std::any::type_name::<CONFIG>()
89            );
90            for epoch in 1..=10 {
91                error!(
92                    "  epoch {epoch}: {:?}",
93                    self.make_quorum_filter(<TYPES as NodeType>::Epoch::new(epoch))
94                );
95            }
96
97            error!(
98                "{} offsets for DA Quorum filter:",
99                std::any::type_name::<DaConfig>()
100            );
101            for epoch in 1..=10 {
102                error!(
103                    "  epoch {epoch}: {:?}",
104                    self.make_da_quorum_filter(<TYPES as NodeType>::Epoch::new(epoch))
105                );
106            }
107        });
108    }
109}
110
111impl<TYPES: NodeType, CONFIG: QuorumFilterConfig, DaConfig: QuorumFilterConfig> Membership<TYPES>
112    for RandomizedCommitteeMembers<TYPES, CONFIG, DaConfig>
113{
114    type Error = hotshot_utils::anytrace::Error;
115    type StakeTableHash = NoStakeTableHash;
116    /// Create a new election
117    fn new(committee_members: Vec<PeerConfig<TYPES>>, da_members: Vec<PeerConfig<TYPES>>) -> Self {
118        // For each eligible leader, get the stake table entry
119        let eligible_leaders = committee_members
120            .iter()
121            .filter(|&member| member.stake_table_entry.stake() > U256::ZERO)
122            .cloned()
123            .collect();
124
125        // For each member, get the stake table entry
126        let members: Vec<PeerConfig<TYPES>> = committee_members
127            .iter()
128            .filter(|&entry| entry.stake_table_entry.stake() > U256::ZERO)
129            .cloned()
130            .collect();
131
132        // For each da member, get the stake table entry
133        let da_members: Vec<PeerConfig<TYPES>> = da_members
134            .iter()
135            .filter(|&entry| entry.stake_table_entry.stake() > U256::ZERO)
136            .cloned()
137            .collect();
138
139        // Index the stake table by public key
140        let indexed_stake_table = members
141            .iter()
142            .map(|entry| {
143                (
144                    TYPES::SignatureKey::public_key(&entry.stake_table_entry),
145                    entry.clone(),
146                )
147            })
148            .collect();
149
150        // Index the stake table by public key
151        let indexed_da_stake_table = da_members
152            .iter()
153            .map(|entry| {
154                (
155                    TYPES::SignatureKey::public_key(&entry.stake_table_entry),
156                    entry.clone(),
157                )
158            })
159            .collect();
160
161        let s = Self {
162            eligible_leaders,
163            stake_table: members,
164            da_stake_table: da_members,
165            indexed_stake_table,
166            indexed_da_stake_table,
167            first_epoch: None,
168            drb_results: BTreeMap::new(),
169            _pd: PhantomData,
170            _da_pd: PhantomData,
171        };
172
173        s.debug_display_offsets();
174
175        s
176    }
177
178    /// Get the stake table for the current view
179    fn stake_table(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> HSStakeTable<TYPES> {
180        if let Some(epoch) = epoch {
181            let filter = self.make_quorum_filter(epoch);
182            //self.stake_table.clone()s
183            self.stake_table
184                .iter()
185                .enumerate()
186                .filter(|(idx, _)| filter.contains(idx))
187                .map(|(_, v)| v.clone())
188                .collect()
189        } else {
190            self.stake_table.clone()
191        }
192        .into()
193    }
194
195    /// Get the da stake table for the current view
196    fn da_stake_table(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> HSStakeTable<TYPES> {
197        if let Some(epoch) = epoch {
198            let filter = self.make_da_quorum_filter(epoch);
199            //self.stake_table.clone()s
200            self.da_stake_table
201                .iter()
202                .enumerate()
203                .filter(|(idx, _)| filter.contains(idx))
204                .map(|(_, v)| v.clone())
205                .collect()
206        } else {
207            self.da_stake_table.clone()
208        }
209        .into()
210    }
211
212    /// Get all members of the committee for the current view
213    fn committee_members(
214        &self,
215        _view_number: <TYPES as NodeType>::View,
216        epoch: Option<<TYPES as NodeType>::Epoch>,
217    ) -> BTreeSet<<TYPES as NodeType>::SignatureKey> {
218        if let Some(epoch) = epoch {
219            let filter = self.make_quorum_filter(epoch);
220            self.stake_table
221                .iter()
222                .enumerate()
223                .filter(|(idx, _)| filter.contains(idx))
224                .map(|(_, v)| TYPES::SignatureKey::public_key(&v.stake_table_entry))
225                .collect()
226        } else {
227            self.stake_table
228                .iter()
229                .map(|config| TYPES::SignatureKey::public_key(&config.stake_table_entry))
230                .collect()
231        }
232    }
233
234    /// Get all members of the committee for the current view
235    fn da_committee_members(
236        &self,
237        _view_number: <TYPES as NodeType>::View,
238        epoch: Option<<TYPES as NodeType>::Epoch>,
239    ) -> BTreeSet<<TYPES as NodeType>::SignatureKey> {
240        if let Some(epoch) = epoch {
241            let filter = self.make_da_quorum_filter(epoch);
242            self.da_stake_table
243                .iter()
244                .enumerate()
245                .filter(|(idx, _)| filter.contains(idx))
246                .map(|(_, v)| TYPES::SignatureKey::public_key(&v.stake_table_entry))
247                .collect()
248        } else {
249            self.da_stake_table
250                .iter()
251                .map(|config| TYPES::SignatureKey::public_key(&config.stake_table_entry))
252                .collect()
253        }
254    }
255    /// Get the stake table entry for a public key
256    fn stake(
257        &self,
258        pub_key: &<TYPES as NodeType>::SignatureKey,
259        epoch: Option<<TYPES as NodeType>::Epoch>,
260    ) -> Option<PeerConfig<TYPES>> {
261        if let Some(epoch) = epoch {
262            let filter = self.make_quorum_filter(epoch);
263            let actual_members: BTreeSet<_> = self
264                .stake_table
265                .iter()
266                .enumerate()
267                .filter(|(idx, _)| filter.contains(idx))
268                .map(|(_, v)| TYPES::SignatureKey::public_key(&v.stake_table_entry))
269                .collect();
270
271            if actual_members.contains(pub_key) {
272                // Only return the stake if it is above zero
273                self.indexed_stake_table.get(pub_key).cloned()
274            } else {
275                // Skip members which aren't included based on the quorum filter
276                None
277            }
278        } else {
279            self.indexed_stake_table.get(pub_key).cloned()
280        }
281    }
282
283    /// Get the da stake table entry for a public key
284    fn da_stake(
285        &self,
286        pub_key: &<TYPES as NodeType>::SignatureKey,
287        epoch: Option<<TYPES as NodeType>::Epoch>,
288    ) -> Option<PeerConfig<TYPES>> {
289        if let Some(epoch) = epoch {
290            let filter = self.make_da_quorum_filter(epoch);
291            let actual_members: BTreeSet<_> = self
292                .da_stake_table
293                .iter()
294                .enumerate()
295                .filter(|(idx, _)| filter.contains(idx))
296                .map(|(_, v)| TYPES::SignatureKey::public_key(&v.stake_table_entry))
297                .collect();
298
299            if actual_members.contains(pub_key) {
300                // Only return the stake if it is above zero
301                self.indexed_da_stake_table.get(pub_key).cloned()
302            } else {
303                // Skip members which aren't included based on the quorum filter
304                None
305            }
306        } else {
307            self.indexed_da_stake_table.get(pub_key).cloned()
308        }
309    }
310
311    /// Check if a node has stake in the committee
312    fn has_stake(
313        &self,
314        pub_key: &<TYPES as NodeType>::SignatureKey,
315        epoch: Option<<TYPES as NodeType>::Epoch>,
316    ) -> bool {
317        if let Some(epoch) = epoch {
318            let filter = self.make_quorum_filter(epoch);
319            let actual_members: BTreeSet<_> = self
320                .stake_table
321                .iter()
322                .enumerate()
323                .filter(|(idx, _)| filter.contains(idx))
324                .map(|(_, v)| TYPES::SignatureKey::public_key(&v.stake_table_entry))
325                .collect();
326
327            if actual_members.contains(pub_key) {
328                self.indexed_stake_table
329                    .get(pub_key)
330                    .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
331            } else {
332                // Skip members which aren't included based on the quorum filter
333                false
334            }
335        } else {
336            self.indexed_stake_table
337                .get(pub_key)
338                .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
339        }
340    }
341
342    /// Check if a node has stake in the committee
343    fn has_da_stake(
344        &self,
345        pub_key: &<TYPES as NodeType>::SignatureKey,
346        epoch: Option<<TYPES as NodeType>::Epoch>,
347    ) -> bool {
348        if let Some(epoch) = epoch {
349            let filter = self.make_da_quorum_filter(epoch);
350            let actual_members: BTreeSet<_> = self
351                .da_stake_table
352                .iter()
353                .enumerate()
354                .filter(|(idx, _)| filter.contains(idx))
355                .map(|(_, v)| TYPES::SignatureKey::public_key(&v.stake_table_entry))
356                .collect();
357
358            if actual_members.contains(pub_key) {
359                self.indexed_da_stake_table
360                    .get(pub_key)
361                    .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
362            } else {
363                // Skip members which aren't included based on the quorum filter
364                false
365            }
366        } else {
367            self.indexed_da_stake_table
368                .get(pub_key)
369                .is_some_and(|x| x.stake_table_entry.stake() > U256::ZERO)
370        }
371    }
372
373    /// Index the vector of public keys with the current view number
374    fn lookup_leader(
375        &self,
376        view_number: <TYPES as NodeType>::View,
377        epoch: Option<<TYPES as NodeType>::Epoch>,
378    ) -> Result<TYPES::SignatureKey> {
379        if let Some(epoch) = epoch {
380            let filter = self.make_quorum_filter(epoch);
381            let leader_vec: Vec<_> = self
382                .stake_table
383                .iter()
384                .enumerate()
385                .filter(|(idx, _)| filter.contains(idx))
386                .map(|(idx, v)| (idx, v.clone()))
387                .collect();
388
389            let mut rng: StdRng = rand::SeedableRng::seed_from_u64(*view_number);
390
391            let randomized_view_number: u64 = rng.gen_range(0..=u64::MAX);
392            #[allow(clippy::cast_possible_truncation)]
393            let index = randomized_view_number as usize % leader_vec.len();
394
395            let res = leader_vec[index].clone();
396
397            tracing::debug!(
398                "RandomizedCommitteeMembers lookup_leader, view_number: {view_number}, epoch: \
399                 {epoch}, leader: {}",
400                res.0
401            );
402
403            Ok(TYPES::SignatureKey::public_key(&res.1.stake_table_entry))
404        } else {
405            let mut rng: StdRng = rand::SeedableRng::seed_from_u64(*view_number);
406
407            let randomized_view_number: u64 = rng.gen_range(0..=u64::MAX);
408            #[allow(clippy::cast_possible_truncation)]
409            let index = randomized_view_number as usize % self.eligible_leaders.len();
410
411            let res = self.eligible_leaders[index].clone();
412
413            Ok(TYPES::SignatureKey::public_key(&res.stake_table_entry))
414        }
415    }
416
417    /// Get the total number of nodes in the committee
418    fn total_nodes(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> usize {
419        if let Some(epoch) = epoch {
420            self.make_quorum_filter(epoch).len()
421        } else {
422            self.stake_table.len()
423        }
424    }
425
426    /// Get the total number of nodes in the committee
427    fn da_total_nodes(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> usize {
428        if let Some(epoch) = epoch {
429            self.make_da_quorum_filter(epoch).len()
430        } else {
431            self.da_stake_table.len()
432        }
433    }
434
435    /// Get the voting success threshold for the committee
436    fn success_threshold(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> U256 {
437        ((self.total_stake(epoch) * U256::from(2)) / U256::from(3)) + U256::from(1)
438    }
439
440    /// Get the voting success threshold for the committee
441    fn da_success_threshold(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> U256 {
442        ((self.total_da_stake(epoch) * U256::from(2)) / U256::from(3)) + U256::from(1)
443    }
444
445    /// Get the voting failure threshold for the committee
446    fn failure_threshold(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> U256 {
447        (self.total_stake(epoch) / U256::from(3)) + U256::from(1)
448    }
449
450    /// Get the voting upgrade threshold for the committee
451    fn upgrade_threshold(&self, epoch: Option<<TYPES as NodeType>::Epoch>) -> U256 {
452        let len = self.total_stake(epoch);
453
454        U256::max(
455            (len * U256::from(9)) / U256::from(10),
456            ((len * U256::from(2)) / U256::from(3)) + U256::from(1),
457        )
458    }
459    fn has_stake_table(&self, _epoch: TYPES::Epoch) -> bool {
460        true
461    }
462    fn has_randomized_stake_table(&self, _epoch: TYPES::Epoch) -> anyhow::Result<bool> {
463        Ok(true)
464    }
465
466    fn add_drb_result(&mut self, epoch: <TYPES as NodeType>::Epoch, drb_result: DrbResult) {
467        self.drb_results.insert(epoch, drb_result);
468    }
469
470    fn set_first_epoch(&mut self, epoch: TYPES::Epoch, initial_drb_result: DrbResult) {
471        self.first_epoch = Some(epoch);
472
473        self.add_drb_result(epoch, initial_drb_result);
474        self.add_drb_result(epoch + 1, initial_drb_result);
475    }
476
477    fn first_epoch(&self) -> Option<TYPES::Epoch> {
478        self.first_epoch
479    }
480
481    async fn get_epoch_drb(
482        membership: Arc<RwLock<Self>>,
483        epoch: TYPES::Epoch,
484    ) -> anyhow::Result<DrbResult> {
485        let membership_reader = membership.read().await;
486
487        membership_reader
488            .drb_results
489            .get(&epoch)
490            .context("DRB result missing")
491            .copied()
492    }
493}