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
276impl AuthorityIndex {
277    pub fn new_for_test(index: u32) -> Self {
278        Self(index)
279    }
280}
281
282impl Display for AuthorityIndex {
283    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
284        write!(f, "[{}]", self.value())
285    }
286}
287
288impl<T, const N: usize> Index<AuthorityIndex> for [T; N] {
289    type Output = T;
290
291    fn index(&self, index: AuthorityIndex) -> &Self::Output {
292        self.get(index.value()).unwrap()
293    }
294}
295
296impl<T> Index<AuthorityIndex> for Vec<T> {
297    type Output = T;
298
299    fn index(&self, index: AuthorityIndex) -> &Self::Output {
300        self.get(index.value()).unwrap()
301    }
302}
303
304impl<T, const N: usize> IndexMut<AuthorityIndex> for [T; N] {
305    fn index_mut(&mut self, index: AuthorityIndex) -> &mut Self::Output {
306        self.get_mut(index.value()).unwrap()
307    }
308}
309
310impl<T> IndexMut<AuthorityIndex> for Vec<T> {
311    fn index_mut(&mut self, index: AuthorityIndex) -> &mut Self::Output {
312        self.get_mut(index.value()).unwrap()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::{local_committee_and_keys, local_committee_and_keys_with_test_options};
320
321    #[test]
322    fn committee_basic() {
323        // GIVEN
324        let epoch = 100;
325        let num_of_authorities = 10;
326        let authority_stakes = (1..=num_of_authorities).map(|s| s as Stake).collect();
327        let (committee, _) = local_committee_and_keys(epoch, authority_stakes);
328
329        // THEN make sure the output Committee fields are populated correctly.
330        assert_eq!(committee.size(), num_of_authorities);
331        for (i, authority) in committee.authorities() {
332            assert_eq!((i.value() + 1) as Stake, authority.stake);
333        }
334
335        // AND ensure thresholds are calculated correctly.
336        assert_eq!(committee.total_stake(), 55);
337        assert_eq!(committee.quorum_threshold(), 37);
338        assert_eq!(committee.validity_threshold(), 19);
339    }
340
341    #[test]
342    fn committee_thresholds_across_sizes() {
343        struct Case {
344            n: usize,
345            stake: Stake,
346            total: Stake,
347            quorum: Stake,
348            validity: Stake,
349        }
350        let cases = [
351            Case {
352                n: 11,
353                stake: 1,
354                total: 11,
355                quorum: 8,
356                validity: 4,
357            },
358            Case {
359                n: 12,
360                stake: 10,
361                total: 120,
362                quorum: 81,
363                validity: 40,
364            },
365        ];
366
367        for case in cases {
368            let stakes = vec![case.stake; case.n];
369            let (committee, _) = local_committee_and_keys(100, stakes);
370            assert_eq!(committee.total_stake(), case.total);
371            assert_eq!(committee.quorum_threshold(), case.quorum);
372            assert_eq!(committee.validity_threshold(), case.validity);
373        }
374    }
375
376    fn create_committee_with_total_stake(num_authorities: usize, total_stake: Stake) -> Committee {
377        // Spreads `total_stake` across `num_authorities` (sandbox-safe addresses).
378        // The last authority absorbs the remainder so the sum is exact.
379        assert!(num_authorities > 0);
380        let per = total_stake / num_authorities as Stake;
381        let mut stakes = vec![per; num_authorities];
382        *stakes.last_mut().unwrap() = total_stake - per * (num_authorities as Stake - 1);
383        let (committee, _) = local_committee_and_keys_with_test_options(0, stakes, false);
384        assert_eq!(committee.total_stake(), total_stake);
385        committee
386    }
387
388    #[test]
389    fn committee_v3_thresholds_across_actual_stakes() {
390        // Thresholds follow:
391        //   f_scaled = floor(f_nominal * (actual - 1) / (5f + 3c))
392        //   c_scaled likewise
393        //   validity      = f_scaled + 1
394        //   certification = 2 * f_scaled + c_scaled + 1
395        //   quorum        = actual - f_scaled - c_scaled
396        // The formulas depend on total stake and the nominal f, c — not on the
397        // number of authorities, so cases below also vary `num_authorities`.
398        // `new_v3` asserts stake-safety invariants internally, so each case
399        // below exercises those invariants too.
400        struct Case {
401            name: &'static str,
402            num_authorities: usize,
403            actual: Stake,
404            malicious: Stake,
405            crash: Stake,
406            validity: Stake,
407            cert: Stake,
408            quorum: Stake,
409        }
410        let cases = [
411            // Actual == nominal budget (5f + 3c + 1 with f=c=1250): no scaling.
412            Case {
413                name: "no scaling",
414                num_authorities: 4,
415                actual: 10_001,
416                malicious: 1_250,
417                crash: 1_250,
418                validity: 1_251,
419                cert: 3_751,
420                quorum: 7_501,
421            },
422            // Tight boundary: actual == nominal + 1, truncation keeps f and c
423            // at the nominal values.
424            Case {
425                name: "tight boundary",
426                num_authorities: 7,
427                actual: 10_002,
428                malicious: 1_250,
429                crash: 1_250,
430                validity: 1_251,
431                cert: 3_751,
432                quorum: 7_502,
433            },
434            // Non-integer scale factor.
435            Case {
436                name: "scale with remainder",
437                num_authorities: 10,
438                actual: 15_000,
439                malicious: 1_250,
440                crash: 1_250,
441                validity: 1_875,
442                cert: 5_623,
443                quorum: 11_252,
444            },
445            // Aggressive scaling: tiny nominal f=c=1 with large actual stake
446            // forces f_scaled = c_scaled = 2500.
447            Case {
448                name: "aggressive scaling",
449                num_authorities: 5,
450                actual: 20_002,
451                malicious: 1,
452                crash: 1,
453                validity: 2_501,
454                cert: 7_501,
455                quorum: 15_002,
456            },
457            // Crash-only: f_nominal=0 ⇒ f_scaled=0; only crash faults scaled.
458            Case {
459                name: "crash-only (f=0)",
460                num_authorities: 4,
461                actual: 10_000,
462                malicious: 0,
463                crash: 1_000,
464                validity: 1,
465                cert: 3_334,
466                quorum: 6_667,
467            },
468            // Byzantine-only: c_nominal=0 ⇒ c_scaled=0; only malicious faults
469            // scaled.
470            Case {
471                name: "byzantine-only (c=0)",
472                num_authorities: 6,
473                actual: 10_000,
474                malicious: 1_000,
475                crash: 0,
476                validity: 2_000,
477                cert: 3_999,
478                quorum: 8_001,
479            },
480        ];
481
482        for case in cases {
483            let seed = create_committee_with_total_stake(case.num_authorities, case.actual);
484            let committee = Committee::new_v3(
485                seed.epoch(),
486                seed.authorities_slice().to_vec(),
487                case.malicious,
488                case.crash,
489            );
490            assert_eq!(committee.size(), case.num_authorities, "{}", case.name);
491            assert_eq!(committee.total_stake(), case.actual, "{}", case.name);
492            assert_eq!(
493                committee.validity_threshold(),
494                case.validity,
495                "{}",
496                case.name
497            );
498            assert_eq!(
499                committee.certification_threshold(),
500                case.cert,
501                "{}",
502                case.name
503            );
504            assert_eq!(committee.quorum_threshold(), case.quorum, "{}", case.name);
505        }
506    }
507
508    #[test]
509    fn committee_v3_no_fault_budget_single_authority() {
510        // f=c=0: single trusted authority, all thresholds collapse to 1.
511        let (seed, _) = local_committee_and_keys_with_test_options(0, vec![100 as Stake], false);
512        let committee = Committee::new_v3(seed.epoch(), seed.authorities_slice().to_vec(), 0, 0);
513        assert_eq!(committee.validity_threshold(), 1);
514        assert_eq!(committee.certification_threshold(), 1);
515        assert_eq!(committee.quorum_threshold(), 100);
516    }
517
518    #[test]
519    #[should_panic(expected = "Total stake cannot be zero!")]
520    fn committee_v3_zero_actual_stake_panics() {
521        let zero_stake_authorities: Vec<Authority> = {
522            let (seed, _) =
523                local_committee_and_keys_with_test_options(0, vec![1 as Stake; 4], false);
524            seed.authorities_slice()
525                .iter()
526                .map(|a| Authority {
527                    stake: 0,
528                    ..a.clone()
529                })
530                .collect()
531        };
532        Committee::new_v3(0, zero_stake_authorities, 1_250, 1_250);
533    }
534}