Skip to main content

consensus_config/
committee.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    fmt::{Display, Formatter},
6    ops::{Index, IndexMut},
7};
8
9use mysten_network::Multiaddr;
10use serde::{Deserialize, Serialize};
11
12use crate::{AuthorityName, NetworkPublicKey, ProtocolPublicKey};
13
14/// Committee of the consensus protocol is updated each epoch.
15pub type Epoch = u64;
16
17/// Voting power of an authority, roughly proportional to the actual amount of Sui staked
18/// by the authority.
19/// Total stake / voting power of all authorities should sum to 10,000.
20pub type Stake = u64;
21
22/// Committee is the set of authorities that participate in the consensus protocol for this epoch.
23/// Its configuration is stored and computed on chain.
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct Committee {
26    /// The epoch number of this committee
27    epoch: Epoch,
28    /// Protocol and network info of each authority.
29    authorities: Vec<Authority>,
30    /// Total stakes in the committee.
31    total_stake: Stake,
32
33    /// Thresholds related to different fault tolerances.
34    quorum_threshold: Stake,
35    certification_threshold: Stake,
36    validity_threshold: Stake,
37}
38
39impl Committee {
40    pub fn new(epoch: Epoch, authorities: Vec<Authority>) -> Self {
41        assert!(!authorities.is_empty(), "Committee cannot be empty!");
42        assert!(
43            authorities.len() < u32::MAX as usize,
44            "Too many authorities ({})!",
45            authorities.len()
46        );
47
48        let total_stake: Stake = authorities.iter().map(|a| a.stake).sum();
49        assert_ne!(total_stake, 0, "Total stake cannot be zero!");
50
51        // Tolerate integer f faults when total stake is 3f+1.
52        let fault_tolerance = (total_stake - 1) / 3;
53        let quorum_threshold = total_stake - fault_tolerance;
54        let validity_threshold = fault_tolerance + 1;
55        assert!(
56            2 * quorum_threshold - fault_tolerance > total_stake,
57            "Quorum must intersect under maxim equivocations! Quorum: {quorum_threshold}, Fault tolerance: {fault_tolerance}, Total: {total_stake}"
58        );
59
60        Self {
61            epoch,
62            authorities,
63            total_stake,
64            quorum_threshold,
65            validity_threshold,
66
67            // Equivalent to quorum_threshold in v2, and unused anyway.
68            certification_threshold: quorum_threshold,
69        }
70    }
71
72    /// Constructs a committee with thresholds derived from a hybrid fault budget
73    /// (`malicious_stake = f`, `crash_stake = c`).
74    ///
75    /// Nominally, the total stake is `nominal_total_stake = 5f + 3c + 1`;
76    /// and the thresholds evaluate to:
77    ///
78    /// - `validity_threshold       = f + 1`
79    /// - `certification_threshold  = 2f + c + 1`
80    /// - `quorum_threshold         = 4f + 2c + 1`
81    ///
82    /// But the actual total stakes specified by the authorities may differ
83    /// from the nominal total stake computed above. We will scale `f` and `c`
84    /// from the nominal value to the largest possible values where intersection
85    /// properties still hold. Then the thresholds are computed with scaled `f` and `c`.
86    #[allow(clippy::int_plus_one)] // Avoid clippy warning about `+ 1` in threshold asserts.
87    pub fn new_v3(
88        epoch: Epoch,
89        authorities: Vec<Authority>,
90        malicious_stake: Stake,
91        crash_stake: Stake,
92    ) -> Self {
93        assert!(!authorities.is_empty(), "Committee cannot be empty!");
94        assert!(
95            authorities.len() < u32::MAX as usize,
96            "Too many authorities ({})!",
97            authorities.len()
98        );
99
100        let actual_total_stake: Stake = authorities.iter().map(|a| a.stake).sum();
101        assert_ne!(actual_total_stake, 0, "Total stake cannot be zero!");
102
103        // Compute v3 thresholds.
104        let base_stake = 5 * malicious_stake + 3 * crash_stake;
105        let (f, c) = if base_stake > 0 {
106            // Scale malicious and crash stakes to the real committee stake.
107            // Use truncating division to get realistic fault budgets.
108            let scale = |nominal: Stake| -> Stake {
109                nominal
110                    .checked_mul(actual_total_stake - 1)
111                    .unwrap_or_else(|| panic!("Overflowed: {} {}", nominal, actual_total_stake - 1))
112                    .checked_div(base_stake)
113                    .unwrap_or_else(|| panic!("Division error: {} {}", nominal, base_stake))
114            };
115            (scale(malicious_stake), scale(crash_stake))
116        } else {
117            // If both fault budgets are zero, there's nothing to scale.
118            (0, 0)
119        };
120
121        let validity_threshold = f + 1;
122        let certification_threshold = 2 * f + c + 1;
123        let quorum_threshold = actual_total_stake - f - c;
124
125        // Ensure intersection between committed certification and quorum thresholds.
126        assert!(
127            certification_threshold + quorum_threshold >= actual_total_stake + f + 1,
128            "Stake-safety invariant violated: \
129                committed_cert ({certification_threshold}) + \
130                quorum ({quorum_threshold}) < \
131                actual_total_stake ({actual_total_stake}) + f ({f}) + 1"
132        );
133
134        // Ensure a committed certificate survives with the intersection between quorum thresholds.
135        assert!(
136            quorum_threshold * 2 >= actual_total_stake + f + certification_threshold,
137            "Stake-safety invariant violated: \
138                quorum_threshold ({quorum_threshold}) * 2 < \
139                actual_total_stake ({actual_total_stake}) + f ({f}) + \
140                certification_threshold ({certification_threshold})"
141        );
142
143        Self {
144            epoch,
145            total_stake: actual_total_stake,
146            quorum_threshold,
147            certification_threshold,
148            validity_threshold,
149            authorities,
150        }
151    }
152
153    // -----------------------------------------------------------------------
154    // Accessors to Committee fields.
155
156    pub fn epoch(&self) -> Epoch {
157        self.epoch
158    }
159
160    pub fn total_stake(&self) -> Stake {
161        self.total_stake
162    }
163
164    pub fn quorum_threshold(&self) -> Stake {
165        self.quorum_threshold
166    }
167
168    pub fn certification_threshold(&self) -> Stake {
169        self.certification_threshold
170    }
171
172    pub fn validity_threshold(&self) -> Stake {
173        self.validity_threshold
174    }
175
176    pub fn stake(&self, authority_index: AuthorityIndex) -> Stake {
177        self.authorities[authority_index].stake
178    }
179
180    pub fn authority(&self, authority_index: AuthorityIndex) -> &Authority {
181        &self.authorities[authority_index]
182    }
183
184    pub fn authorities(&self) -> impl Iterator<Item = (AuthorityIndex, &Authority)> {
185        self.authorities
186            .iter()
187            .enumerate()
188            .map(|(i, a)| (AuthorityIndex(i as u32), a))
189    }
190
191    /// Returns the authorities as a slice, preserving their order (and hence
192    /// their `AuthorityIndex` values). Useful for rebuilding a `Committee` with
193    /// different threshold parameters while keeping the same authority set.
194    pub fn authorities_slice(&self) -> &[Authority] {
195        &self.authorities
196    }
197
198    // -----------------------------------------------------------------------
199    // Helpers for Committee properties.
200
201    /// Returns true if the provided stake has reached quorum (2f+1).
202    pub fn reached_quorum(&self, stake: Stake) -> bool {
203        stake >= self.quorum_threshold()
204    }
205
206    /// Returns true if the provided stake has reached validity (f+1).
207    pub fn reached_validity(&self, stake: Stake) -> bool {
208        stake >= self.validity_threshold()
209    }
210
211    /// Converts an index to an AuthorityIndex, if valid.
212    /// Returns None if index is out of bound.
213    pub fn to_authority_index(&self, index: usize) -> Option<AuthorityIndex> {
214        if index < self.authorities.len() {
215            Some(AuthorityIndex(index as u32))
216        } else {
217            None
218        }
219    }
220
221    /// Returns true if the provided index is valid.
222    pub fn is_valid_index(&self, index: AuthorityIndex) -> bool {
223        index.value() < self.size()
224    }
225
226    /// Returns number of authorities in the committee.
227    pub fn size(&self) -> usize {
228        self.authorities.len()
229    }
230}
231
232/// Represents one authority in the committee.
233///
234/// NOTE: this is intentionally un-cloneable, to encourage only copying relevant fields.
235/// AuthorityIndex should be used to reference an authority instead.
236#[derive(Clone, Debug, Serialize, Deserialize)]
237pub struct Authority {
238    /// Voting power of the authority in the committee.
239    pub stake: Stake,
240    /// Network address for communicating with the authority.
241    pub address: Multiaddr,
242    /// The authority's hostname, for metrics and logging.
243    pub hostname: String,
244    /// The authority's name, matching AuthorityName on the Sui side.
245    pub authority_name: AuthorityName,
246    /// The authority's public key for verifying blocks.
247    pub protocol_key: ProtocolPublicKey,
248    /// The authority's public key for TLS and as network identity.
249    pub network_key: NetworkPublicKey,
250}
251
252/// Each authority is uniquely identified by its AuthorityIndex in the Committee.
253/// AuthorityIndex is between 0 (inclusive) and the total number of authorities (exclusive).
254///
255/// NOTE: for safety, invalid AuthorityIndex should be impossible to create. So AuthorityIndex
256/// should not be created or incremented outside of this file. AuthorityIndex received from peers
257/// should be validated before use.
258#[derive(
259    Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Debug, Default, Hash, Serialize, Deserialize,
260)]
261pub struct AuthorityIndex(u32);
262
263impl AuthorityIndex {
264    // Minimum committee size is 1, so 0 index is always valid.
265    pub const ZERO: Self = Self(0);
266
267    // Only for scanning rows in the database. Invalid elsewhere.
268    pub const MIN: Self = Self::ZERO;
269    pub const MAX: Self = Self(u32::MAX);
270
271    pub fn value(&self) -> usize {
272        self.0 as usize
273    }
274
275    pub fn value_u32(&self) -> u32 {
276        self.0
277    }
278}
279
280impl AuthorityIndex {
281    pub fn new_for_test(index: u32) -> Self {
282        Self(index)
283    }
284}
285
286impl Display for AuthorityIndex {
287    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
288        write!(f, "[{}]", self.value())
289    }
290}
291
292impl<T, const N: usize> Index<AuthorityIndex> for [T; N] {
293    type Output = T;
294
295    fn index(&self, index: AuthorityIndex) -> &Self::Output {
296        self.get(index.value()).unwrap()
297    }
298}
299
300impl<T> Index<AuthorityIndex> for Vec<T> {
301    type Output = T;
302
303    fn index(&self, index: AuthorityIndex) -> &Self::Output {
304        self.get(index.value()).unwrap()
305    }
306}
307
308impl<T, const N: usize> IndexMut<AuthorityIndex> for [T; N] {
309    fn index_mut(&mut self, index: AuthorityIndex) -> &mut Self::Output {
310        self.get_mut(index.value()).unwrap()
311    }
312}
313
314impl<T> IndexMut<AuthorityIndex> for Vec<T> {
315    fn index_mut(&mut self, index: AuthorityIndex) -> &mut Self::Output {
316        self.get_mut(index.value()).unwrap()
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::{local_committee_and_keys, local_committee_and_keys_with_test_options};
324
325    #[test]
326    fn committee_basic() {
327        // GIVEN
328        let epoch = 100;
329        let num_of_authorities = 10;
330        let authority_stakes = (1..=num_of_authorities).map(|s| s as Stake).collect();
331        let (committee, _) = local_committee_and_keys(epoch, authority_stakes);
332
333        // THEN make sure the output Committee fields are populated correctly.
334        assert_eq!(committee.size(), num_of_authorities);
335        for (i, authority) in committee.authorities() {
336            assert_eq!((i.value() + 1) as Stake, authority.stake);
337        }
338
339        // AND ensure thresholds are calculated correctly.
340        assert_eq!(committee.total_stake(), 55);
341        assert_eq!(committee.quorum_threshold(), 37);
342        assert_eq!(committee.validity_threshold(), 19);
343    }
344
345    #[test]
346    fn committee_thresholds_across_sizes() {
347        struct Case {
348            n: usize,
349            stake: Stake,
350            total: Stake,
351            quorum: Stake,
352            validity: Stake,
353        }
354        let cases = [
355            Case {
356                n: 11,
357                stake: 1,
358                total: 11,
359                quorum: 8,
360                validity: 4,
361            },
362            Case {
363                n: 12,
364                stake: 10,
365                total: 120,
366                quorum: 81,
367                validity: 40,
368            },
369        ];
370
371        for case in cases {
372            let stakes = vec![case.stake; case.n];
373            let (committee, _) = local_committee_and_keys(100, stakes);
374            assert_eq!(committee.total_stake(), case.total);
375            assert_eq!(committee.quorum_threshold(), case.quorum);
376            assert_eq!(committee.validity_threshold(), case.validity);
377        }
378    }
379
380    fn create_committee_with_total_stake(num_authorities: usize, total_stake: Stake) -> Committee {
381        // Spreads `total_stake` across `num_authorities` (sandbox-safe addresses).
382        // The last authority absorbs the remainder so the sum is exact.
383        assert!(num_authorities > 0);
384        let per = total_stake / num_authorities as Stake;
385        let mut stakes = vec![per; num_authorities];
386        *stakes.last_mut().unwrap() = total_stake - per * (num_authorities as Stake - 1);
387        let (committee, _) = local_committee_and_keys_with_test_options(0, stakes, false);
388        assert_eq!(committee.total_stake(), total_stake);
389        committee
390    }
391
392    #[test]
393    fn committee_v3_thresholds_across_actual_stakes() {
394        // Thresholds follow:
395        //   f_scaled = floor(f_nominal * (actual - 1) / (5f + 3c))
396        //   c_scaled likewise
397        //   validity      = f_scaled + 1
398        //   certification = 2 * f_scaled + c_scaled + 1
399        //   quorum        = actual - f_scaled - c_scaled
400        // The formulas depend on total stake and the nominal f, c — not on the
401        // number of authorities, so cases below also vary `num_authorities`.
402        // `new_v3` asserts stake-safety invariants internally, so each case
403        // below exercises those invariants too.
404        struct Case {
405            name: &'static str,
406            num_authorities: usize,
407            actual: Stake,
408            malicious: Stake,
409            crash: Stake,
410            validity: Stake,
411            cert: Stake,
412            quorum: Stake,
413        }
414        let cases = [
415            // Actual == nominal budget (5f + 3c + 1 with f=c=1250): no scaling.
416            Case {
417                name: "no scaling",
418                num_authorities: 4,
419                actual: 10_001,
420                malicious: 1_250,
421                crash: 1_250,
422                validity: 1_251,
423                cert: 3_751,
424                quorum: 7_501,
425            },
426            // Tight boundary: actual == nominal + 1, truncation keeps f and c
427            // at the nominal values.
428            Case {
429                name: "tight boundary",
430                num_authorities: 7,
431                actual: 10_002,
432                malicious: 1_250,
433                crash: 1_250,
434                validity: 1_251,
435                cert: 3_751,
436                quorum: 7_502,
437            },
438            // Non-integer scale factor.
439            Case {
440                name: "scale with remainder",
441                num_authorities: 10,
442                actual: 15_000,
443                malicious: 1_250,
444                crash: 1_250,
445                validity: 1_875,
446                cert: 5_623,
447                quorum: 11_252,
448            },
449            // Aggressive scaling: tiny nominal f=c=1 with large actual stake
450            // forces f_scaled = c_scaled = 2500.
451            Case {
452                name: "aggressive scaling",
453                num_authorities: 5,
454                actual: 20_002,
455                malicious: 1,
456                crash: 1,
457                validity: 2_501,
458                cert: 7_501,
459                quorum: 15_002,
460            },
461            // Crash-only: f_nominal=0 ⇒ f_scaled=0; only crash faults scaled.
462            Case {
463                name: "crash-only (f=0)",
464                num_authorities: 4,
465                actual: 10_000,
466                malicious: 0,
467                crash: 1_000,
468                validity: 1,
469                cert: 3_334,
470                quorum: 6_667,
471            },
472            // Byzantine-only: c_nominal=0 ⇒ c_scaled=0; only malicious faults
473            // scaled.
474            Case {
475                name: "byzantine-only (c=0)",
476                num_authorities: 6,
477                actual: 10_000,
478                malicious: 1_000,
479                crash: 0,
480                validity: 2_000,
481                cert: 3_999,
482                quorum: 8_001,
483            },
484        ];
485
486        for case in cases {
487            let seed = create_committee_with_total_stake(case.num_authorities, case.actual);
488            let committee = Committee::new_v3(
489                seed.epoch(),
490                seed.authorities_slice().to_vec(),
491                case.malicious,
492                case.crash,
493            );
494            assert_eq!(committee.size(), case.num_authorities, "{}", case.name);
495            assert_eq!(committee.total_stake(), case.actual, "{}", case.name);
496            assert_eq!(
497                committee.validity_threshold(),
498                case.validity,
499                "{}",
500                case.name
501            );
502            assert_eq!(
503                committee.certification_threshold(),
504                case.cert,
505                "{}",
506                case.name
507            );
508            assert_eq!(committee.quorum_threshold(), case.quorum, "{}", case.name);
509        }
510    }
511
512    #[test]
513    fn committee_v3_no_fault_budget_single_authority() {
514        // f=c=0: single trusted authority, all thresholds collapse to 1.
515        let (seed, _) = local_committee_and_keys_with_test_options(0, vec![100 as Stake], false);
516        let committee = Committee::new_v3(seed.epoch(), seed.authorities_slice().to_vec(), 0, 0);
517        assert_eq!(committee.validity_threshold(), 1);
518        assert_eq!(committee.certification_threshold(), 1);
519        assert_eq!(committee.quorum_threshold(), 100);
520    }
521
522    #[test]
523    #[should_panic(expected = "Total stake cannot be zero!")]
524    fn committee_v3_zero_actual_stake_panics() {
525        let zero_stake_authorities: Vec<Authority> = {
526            let (seed, _) =
527                local_committee_and_keys_with_test_options(0, vec![1 as Stake; 4], false);
528            seed.authorities_slice()
529                .iter()
530                .map(|a| Authority {
531                    stake: 0,
532                    ..a.clone()
533                })
534                .collect()
535        };
536        Committee::new_v3(0, zero_stake_authorities, 1_250, 1_250);
537    }
538}