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