iota_types/
committee.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6use std::{
7    collections::{BTreeMap, BTreeSet, HashMap},
8    fmt::{Display, Formatter, Write},
9    hash::{Hash, Hasher},
10};
11
12use fastcrypto::traits::KeyPair;
13pub use iota_protocol_config::ProtocolVersion;
14use once_cell::sync::OnceCell;
15use rand::{
16    Rng, SeedableRng,
17    rngs::{StdRng, ThreadRng},
18    seq::SliceRandom,
19};
20use serde::{Deserialize, Serialize};
21
22use super::base_types::*;
23use crate::{
24    crypto::{
25        AuthorityKeyPair, AuthorityPublicKey, NetworkPublicKey, random_committee_key_pairs_of_size,
26    },
27    error::{IotaError, IotaResult},
28    multiaddr::Multiaddr,
29};
30
31pub type EpochId = u64;
32
33// TODO: the stake and voting power of a validator can be different so
34// in some places when we are actually referring to the voting power, we
35// should use a different type alias, field name, etc.
36pub type StakeUnit = u64;
37
38pub type CommitteeDigest = [u8; 32];
39
40// The voting power, quorum threshold and max voting power are defined in the
41// `voting_power.move` module. We're following the very same convention in the
42// validator binaries.
43
44/// Set total_voting_power as 10_000 by convention. Individual voting powers can
45/// be interpreted as easily understandable basis points (e.g., voting_power:
46/// 100 = 1%, voting_power: 1 = 0.01%). Fixing the total voting power allows
47/// clients to hardcode the quorum threshold and total_voting power rather
48/// than recomputing these.
49pub const TOTAL_VOTING_POWER: StakeUnit = 10_000;
50
51/// Quorum threshold for our fixed voting power--any message signed by this much
52/// voting power can be trusted up to BFT assumptions
53pub const QUORUM_THRESHOLD: StakeUnit = 6_667;
54
55/// Validity threshold defined by f+1
56pub const VALIDITY_THRESHOLD: StakeUnit = 3_334;
57
58#[derive(Clone, Debug, Serialize, Deserialize, Eq)]
59pub struct Committee {
60    pub epoch: EpochId,
61    pub voting_rights: Vec<(AuthorityName, StakeUnit)>,
62    expanded_keys: HashMap<AuthorityName, AuthorityPublicKey>,
63    index_map: HashMap<AuthorityName, usize>,
64}
65
66impl Committee {
67    pub fn new(epoch: EpochId, voting_rights: BTreeMap<AuthorityName, StakeUnit>) -> Self {
68        let mut voting_rights: Vec<(AuthorityName, StakeUnit)> =
69            voting_rights.iter().map(|(a, s)| (*a, *s)).collect();
70
71        assert!(!voting_rights.is_empty());
72        assert!(voting_rights.iter().any(|(_, s)| *s != 0));
73
74        voting_rights.sort_by_key(|(a, _)| *a);
75        let total_votes: StakeUnit = voting_rights.iter().map(|(_, votes)| *votes).sum();
76        assert_eq!(total_votes, TOTAL_VOTING_POWER);
77
78        let (expanded_keys, index_map) = Self::load_inner(&voting_rights);
79
80        Committee {
81            epoch,
82            voting_rights,
83            expanded_keys,
84            index_map,
85        }
86    }
87
88    /// Normalize the given weights to TOTAL_VOTING_POWER and create the
89    /// committee. Used for testing only: a production system is using the
90    /// voting weights of the Iota System object.
91    pub fn new_for_testing_with_normalized_voting_power(
92        epoch: EpochId,
93        mut voting_weights: BTreeMap<AuthorityName, StakeUnit>,
94    ) -> Self {
95        let num_nodes = voting_weights.len();
96        let total_votes: StakeUnit = voting_weights.values().cloned().sum();
97
98        let normalization_coef = TOTAL_VOTING_POWER as f64 / total_votes as f64;
99        let mut total_sum = 0;
100        for (idx, (_auth, weight)) in voting_weights.iter_mut().enumerate() {
101            if idx < num_nodes - 1 {
102                *weight = (*weight as f64 * normalization_coef).floor() as u64; // adjust the weights following the normalization coef
103                total_sum += *weight;
104            } else {
105                // the last element is taking all the rest
106                *weight = TOTAL_VOTING_POWER - total_sum;
107            }
108        }
109
110        Self::new(epoch, voting_weights)
111    }
112
113    // We call this if these have not yet been computed
114    pub fn load_inner(
115        voting_rights: &[(AuthorityName, StakeUnit)],
116    ) -> (
117        HashMap<AuthorityName, AuthorityPublicKey>,
118        HashMap<AuthorityName, usize>,
119    ) {
120        let expanded_keys: HashMap<AuthorityName, AuthorityPublicKey> = voting_rights
121            .iter()
122            .map(|(addr, _)| {
123                (
124                    *addr,
125                    (*addr)
126                        .try_into()
127                        .expect("Validator pubkey is always verified on-chain"),
128                )
129            })
130            .collect();
131
132        let index_map: HashMap<AuthorityName, usize> = voting_rights
133            .iter()
134            .enumerate()
135            .map(|(index, (addr, _))| (*addr, index))
136            .collect();
137        (expanded_keys, index_map)
138    }
139
140    pub fn authority_index(&self, author: &AuthorityName) -> Option<u32> {
141        self.index_map.get(author).map(|i| *i as u32)
142    }
143
144    pub fn authority_by_index(&self, index: u32) -> Option<&AuthorityName> {
145        self.voting_rights.get(index as usize).map(|(name, _)| name)
146    }
147
148    pub fn epoch(&self) -> EpochId {
149        self.epoch
150    }
151
152    pub fn public_key(&self, authority: &AuthorityName) -> IotaResult<&AuthorityPublicKey> {
153        debug_assert_eq!(self.expanded_keys.len(), self.voting_rights.len());
154        match self.expanded_keys.get(authority) {
155            Some(v) => Ok(v),
156            None => Err(IotaError::InvalidCommittee(format!(
157                "Authority #{} not found, committee size {}",
158                authority,
159                self.expanded_keys.len()
160            ))),
161        }
162    }
163
164    /// Samples authorities by weight
165    pub fn sample(&self) -> &AuthorityName {
166        // unwrap safe unless committee is empty
167        Self::choose_multiple_weighted(&self.voting_rights[..], 1, &mut ThreadRng::default())
168            .next()
169            .unwrap()
170    }
171
172    fn choose_multiple_weighted<'a>(
173        slice: &'a [(AuthorityName, StakeUnit)],
174        count: usize,
175        rng: &mut impl Rng,
176    ) -> impl Iterator<Item = &'a AuthorityName> {
177        // unwrap is safe because we validate the committee composition in `new` above.
178        // See https://docs.rs/rand/latest/rand/distributions/weighted/enum.WeightedError.html
179        // for possible errors.
180        slice
181            .choose_multiple_weighted(rng, count, |(_, weight)| *weight as f64)
182            .unwrap()
183            .map(|(a, _)| a)
184    }
185
186    pub fn choose_multiple_weighted_iter(
187        &self,
188        count: usize,
189    ) -> impl Iterator<Item = &AuthorityName> {
190        self.voting_rights
191            .choose_multiple_weighted(&mut ThreadRng::default(), count, |(_, weight)| {
192                *weight as f64
193            })
194            .unwrap()
195            .map(|(a, _)| a)
196    }
197
198    pub fn total_votes(&self) -> StakeUnit {
199        TOTAL_VOTING_POWER
200    }
201
202    pub fn quorum_threshold(&self) -> StakeUnit {
203        QUORUM_THRESHOLD
204    }
205
206    pub fn validity_threshold(&self) -> StakeUnit {
207        VALIDITY_THRESHOLD
208    }
209
210    pub fn threshold<const STRENGTH: bool>(&self) -> StakeUnit {
211        if STRENGTH {
212            QUORUM_THRESHOLD
213        } else {
214            VALIDITY_THRESHOLD
215        }
216    }
217
218    /// Calculates the effective threshold for protocol version selection.
219    /// This is the quorum threshold plus a buffer stake based on the given
220    /// basis points.
221    ///
222    /// The buffer is calculated as: f * buffer_stake_bps / 10000, rounded up
223    /// where f = total_votes - quorum_threshold
224    ///
225    /// buffer_stake_bps is clamped to a maximum of 10000.
226    pub fn effective_threshold(&self, mut buffer_stake_bps: u64) -> StakeUnit {
227        if buffer_stake_bps > 10000 {
228            buffer_stake_bps = 10000;
229        }
230        let quorum_threshold = self.quorum_threshold();
231        let f = self.total_votes() - quorum_threshold;
232        let buffer_stake = (f * buffer_stake_bps).div_ceil(10000);
233        quorum_threshold + buffer_stake
234    }
235
236    pub fn num_members(&self) -> usize {
237        self.voting_rights.len()
238    }
239
240    pub fn members(&self) -> impl Iterator<Item = &(AuthorityName, StakeUnit)> {
241        self.voting_rights.iter()
242    }
243
244    pub fn names(&self) -> impl Iterator<Item = &AuthorityName> {
245        self.voting_rights.iter().map(|(name, _)| name)
246    }
247
248    pub fn stakes(&self) -> impl Iterator<Item = StakeUnit> + '_ {
249        self.voting_rights.iter().map(|(_, stake)| *stake)
250    }
251
252    pub fn authority_exists(&self, name: &AuthorityName) -> bool {
253        self.voting_rights
254            .binary_search_by_key(name, |(a, _)| *a)
255            .is_ok()
256    }
257
258    /// Derive a seed deterministically from the transaction digest and shuffle
259    /// the validators.
260    pub fn shuffle_by_stake_from_tx_digest(
261        &self,
262        tx_digest: &TransactionDigest,
263    ) -> Vec<AuthorityName> {
264        // the 32 is as requirement of the default StdRng::from_seed choice
265        let digest_bytes = tx_digest.into_inner();
266
267        // permute the validators deterministically, based on the digest
268        let mut rng = StdRng::from_seed(digest_bytes);
269        self.shuffle_by_stake_with_rng(None, None, &mut rng)
270    }
271
272    // ===== Testing-only methods =====
273    //
274    pub fn new_simple_test_committee_of_size(size: usize) -> (Self, Vec<AuthorityKeyPair>) {
275        let key_pairs: Vec<_> = random_committee_key_pairs_of_size(size)
276            .into_iter()
277            .collect();
278        let committee = Self::new_for_testing_with_normalized_voting_power(
279            0,
280            key_pairs
281                .iter()
282                .map(|key| {
283                    (AuthorityName::from(key.public()), /* voting right */ 1)
284                })
285                .collect(),
286        );
287        (committee, key_pairs)
288    }
289
290    /// Generate a simple committee with 4 validators each with equal voting
291    /// stake of 1.
292    pub fn new_simple_test_committee() -> (Self, Vec<AuthorityKeyPair>) {
293        Self::new_simple_test_committee_of_size(4)
294    }
295}
296
297impl CommitteeTrait<AuthorityName> for Committee {
298    fn shuffle_by_stake_with_rng(
299        &self,
300        // try these authorities first
301        preferences: Option<&BTreeSet<AuthorityName>>,
302        // only attempt from these authorities.
303        restrict_to: Option<&BTreeSet<AuthorityName>>,
304        rng: &mut impl Rng,
305    ) -> Vec<AuthorityName> {
306        let restricted = self
307            .voting_rights
308            .iter()
309            .filter(|(name, _)| {
310                if let Some(restrict_to) = restrict_to {
311                    restrict_to.contains(name)
312                } else {
313                    true
314                }
315            })
316            .cloned();
317
318        let (preferred, rest): (Vec<_>, Vec<_>) = if let Some(preferences) = preferences {
319            restricted.partition(|(name, _)| preferences.contains(name))
320        } else {
321            (Vec::new(), restricted.collect())
322        };
323
324        Self::choose_multiple_weighted(&preferred, preferred.len(), rng)
325            .chain(Self::choose_multiple_weighted(&rest, rest.len(), rng))
326            .cloned()
327            .collect()
328    }
329
330    fn weight(&self, author: &AuthorityName) -> StakeUnit {
331        match self.voting_rights.binary_search_by_key(author, |(a, _)| *a) {
332            Err(_) => 0,
333            Ok(idx) => self.voting_rights[idx].1,
334        }
335    }
336}
337
338impl PartialEq for Committee {
339    fn eq(&self, other: &Self) -> bool {
340        self.epoch == other.epoch && self.voting_rights == other.voting_rights
341    }
342}
343
344impl Hash for Committee {
345    fn hash<H: Hasher>(&self, state: &mut H) {
346        self.epoch.hash(state);
347        self.voting_rights.hash(state);
348    }
349}
350
351impl Display for Committee {
352    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
353        let mut voting_rights = String::new();
354        for (name, vote) in &self.voting_rights {
355            write!(voting_rights, "{}: {}, ", name.concise(), vote)?;
356        }
357        write!(
358            f,
359            "Committee (epoch={:?}, voting_rights=[{}])",
360            self.epoch, voting_rights
361        )
362    }
363}
364
365pub trait CommitteeTrait<K: Ord> {
366    fn shuffle_by_stake_with_rng(
367        &self,
368        // try these authorities first
369        preferences: Option<&BTreeSet<K>>,
370        // only attempt from these authorities.
371        restrict_to: Option<&BTreeSet<K>>,
372        rng: &mut impl Rng,
373    ) -> Vec<K>;
374
375    fn shuffle_by_stake(
376        &self,
377        // try these authorities first
378        preferences: Option<&BTreeSet<K>>,
379        // only attempt from these authorities.
380        restrict_to: Option<&BTreeSet<K>>,
381    ) -> Vec<K> {
382        self.shuffle_by_stake_with_rng(preferences, restrict_to, &mut ThreadRng::default())
383    }
384
385    fn weight(&self, author: &K) -> StakeUnit;
386}
387
388#[derive(Clone, Debug, Serialize, Deserialize)]
389pub struct NetworkMetadata {
390    pub network_address: Multiaddr,
391    pub primary_address: Multiaddr,
392    pub network_public_key: Option<NetworkPublicKey>,
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize)]
396pub struct CommitteeWithNetworkMetadata {
397    epoch_id: EpochId,
398    validators: BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)>,
399
400    #[serde(skip)]
401    committee: OnceCell<Committee>,
402}
403
404impl CommitteeWithNetworkMetadata {
405    pub fn new(
406        epoch_id: EpochId,
407        validators: BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)>,
408    ) -> Self {
409        Self {
410            epoch_id,
411            validators,
412            committee: OnceCell::new(),
413        }
414    }
415    pub fn epoch(&self) -> EpochId {
416        self.epoch_id
417    }
418
419    pub fn validators(&self) -> &BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)> {
420        &self.validators
421    }
422
423    pub fn committee(&self) -> &Committee {
424        self.committee.get_or_init(|| {
425            Committee::new(
426                self.epoch_id,
427                self.validators
428                    .iter()
429                    .map(|(name, (stake, _))| (*name, *stake))
430                    .collect(),
431            )
432        })
433    }
434}
435
436impl Display for CommitteeWithNetworkMetadata {
437    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
438        write!(
439            f,
440            "CommitteeWithNetworkMetadata (epoch={}, validators={:?})",
441            self.epoch_id, self.validators
442        )
443    }
444}
445
446#[cfg(test)]
447mod test {
448    use fastcrypto::traits::KeyPair;
449
450    use super::*;
451    use crate::crypto::{AuthorityKeyPair, get_key_pair};
452
453    #[test]
454    fn test_shuffle_by_weight() {
455        let (_, sec1): (_, AuthorityKeyPair) = get_key_pair();
456        let (_, sec2): (_, AuthorityKeyPair) = get_key_pair();
457        let (_, sec3): (_, AuthorityKeyPair) = get_key_pair();
458        let a1: AuthorityName = sec1.public().into();
459        let a2: AuthorityName = sec2.public().into();
460        let a3: AuthorityName = sec3.public().into();
461
462        let mut authorities = BTreeMap::new();
463        authorities.insert(a1, 1);
464        authorities.insert(a2, 1);
465        authorities.insert(a3, 1);
466
467        let committee = Committee::new_for_testing_with_normalized_voting_power(0, authorities);
468
469        assert_eq!(committee.shuffle_by_stake(None, None).len(), 3);
470
471        let mut pref = BTreeSet::new();
472        pref.insert(a2);
473
474        // preference always comes first
475        for _ in 0..100 {
476            assert_eq!(
477                a2,
478                *committee
479                    .shuffle_by_stake(Some(&pref), None)
480                    .first()
481                    .unwrap()
482            );
483        }
484
485        let mut restrict = BTreeSet::new();
486        restrict.insert(a2);
487
488        for _ in 0..100 {
489            let res = committee.shuffle_by_stake(None, Some(&restrict));
490            assert_eq!(1, res.len());
491            assert_eq!(a2, res[0]);
492        }
493
494        // empty preferences are valid
495        let res = committee.shuffle_by_stake(Some(&BTreeSet::new()), None);
496        assert_eq!(3, res.len());
497
498        let res = committee.shuffle_by_stake(None, Some(&BTreeSet::new()));
499        assert_eq!(0, res.len());
500    }
501}