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