Skip to main content

commonware_utils/
faults.rs

1//! Byzantine fault tolerance models for consensus protocols.
2//!
3//! This module provides abstractions over quorum calculations for different BFT
4//! fault models. The two primary models are:
5//!
6//! - [`N3f1`]: Fault model requiring `n >= 3f + 1` participants
7//! - [`N5f1`]: Fault model requiring `n >= 5f + 1` participants
8//!
9//! _`f` denotes the maximum number of faults that can be tolerated._
10//!
11//! # Example
12//!
13//! ```
14//! use commonware_utils::{Faults, N3f1, N5f1};
15//!
16//! // n >= 3f+1
17//! let n = 10;
18//! assert_eq!(N3f1::max_faults(n), 3);  // f = (n-1)/3 = 3
19//! assert_eq!(N3f1::quorum(n), 7);       // q = n - f = 7
20//!
21//! // n >= 5f+1
22//! assert_eq!(N5f1::max_faults(n), 1);  // f = (n-1)/5 = 1
23//! assert_eq!(N5f1::quorum(n), 9);       // q = n - f = 9
24//!
25//! // Works with any integer type
26//! let n_i32: i32 = 10;
27//! assert_eq!(N3f1::max_faults(n_i32), 3);
28//! assert_eq!(N3f1::quorum(n_i32), 7);
29//! ```
30
31use num_traits::ToPrimitive;
32
33/// A Byzantine fault tolerance model that defines quorum calculations.
34///
35/// Different consensus protocols require different fault tolerance guarantees.
36/// This trait abstracts over those requirements, allowing protocols to be
37/// parameterized by their fault model.
38///
39/// All methods accept any integer type that implements [`ToPrimitive`], allowing
40/// callers to use `u32`, `u64`, `i32`, `usize`, etc. without explicit conversion.
41/// Output is always `u32`.
42pub trait Faults {
43    /// Compute the maximum number of faults that can be tolerated for `n` participants.
44    ///
45    /// This is the maximum integer `f` such that the protocol's safety and liveness
46    /// properties hold when up to `f` participants are Byzantine.
47    ///
48    /// # Panics
49    ///
50    /// Panics if `n` is zero, negative, or exceeds `u32::MAX`.
51    fn max_faults(n: impl ToPrimitive) -> u32;
52
53    /// Compute the quorum size for `n` participants.
54    ///
55    /// This is the minimum number of participants that must agree for the protocol
56    /// to make progress. It equals `n - max_faults(n)`.
57    ///
58    /// # Panics
59    ///
60    /// Panics if `n` is zero, negative, or exceeds `u32::MAX`.
61    fn quorum(n: impl ToPrimitive) -> u32 {
62        let n = n
63            .to_u32()
64            .expect("n must be a non-negative integer that fits in u32");
65        assert!(n > 0, "n must not be zero");
66        n - Self::max_faults(n)
67    }
68}
69
70/// Fault model requiring `n >= 3f + 1` participants.
71///
72/// Tolerates up to `f = (n-1)/3` faults with quorum size `q = n - f`.
73///
74/// For any two quorums Q1 and Q2, there exists at least one honest participant
75/// in their intersection (since `|Q1| + |Q2| > n + f`).
76///
77/// # Example
78///
79/// | n  | f  | quorum |
80/// |----|----| -------|
81/// | 4  | 1  | 3      |
82/// | 7  | 2  | 5      |
83/// | 10 | 3  | 7      |
84/// | 13 | 4  | 9      |
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86pub struct N3f1;
87
88impl Faults for N3f1 {
89    fn max_faults(n: impl ToPrimitive) -> u32 {
90        let n = n
91            .to_u32()
92            .expect("n must be a non-negative integer that fits in u32");
93        assert!(n > 0, "n must not be zero");
94        (n - 1) / 3
95    }
96}
97
98/// Fault model requiring `n >= 5f + 1` participants.
99///
100/// Tolerates up to `f = (n-1)/5` faults with quorum size `q = n - f` (also
101/// provided as [`l_quorum`](Self::l_quorum)).
102///
103/// Also provides [`m_quorum`](Self::m_quorum) which computes `2f + 1`.
104///
105/// # Example
106///
107/// | n  | f  | quorum (n-f) | m-quorum (2f+1) |
108/// |----|----| -------------|-----------------|
109/// | 6  | 1  | 5            | 3               |
110/// | 11 | 2  | 9            | 5               |
111/// | 16 | 3  | 13           | 7               |
112/// | 21 | 4  | 17           | 9               |
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub struct N5f1;
115
116impl Faults for N5f1 {
117    fn max_faults(n: impl ToPrimitive) -> u32 {
118        let n = n
119            .to_u32()
120            .expect("n must be a non-negative integer that fits in u32");
121        assert!(n > 0, "n must not be zero");
122        (n - 1) / 5
123    }
124}
125
126impl N5f1 {
127    /// Compute `2f + 1`.
128    ///
129    /// # Panics
130    ///
131    /// Panics if `n` is zero, negative, or exceeds `u32::MAX`.
132    pub fn m_quorum(n: impl ToPrimitive) -> u32 {
133        let n = n
134            .to_u32()
135            .expect("n must be a non-negative integer that fits in u32");
136        assert!(n > 0, "n must not be zero");
137        2 * Self::max_faults(n) + 1
138    }
139
140    /// Compute `n - f`.
141    ///
142    /// This is equivalent to [`Self::quorum`].
143    ///
144    /// # Panics
145    ///
146    /// Panics if `n` is zero, negative, or exceeds `u32::MAX`.
147    pub fn l_quorum(n: impl ToPrimitive) -> u32 {
148        Self::quorum(n)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use proptest::prelude::*;
156    use rstest::rstest;
157
158    #[test]
159    #[should_panic(expected = "n must not be zero")]
160    fn test_bft3f1_max_faults_zero_panics() {
161        N3f1::max_faults(0);
162    }
163
164    #[test]
165    #[should_panic(expected = "n must not be zero")]
166    fn test_bft3f1_quorum_zero_panics() {
167        N3f1::quorum(0);
168    }
169
170    #[rstest]
171    #[case(1, 0, 1)]
172    #[case(2, 0, 2)]
173    #[case(3, 0, 3)]
174    #[case(4, 1, 3)]
175    #[case(5, 1, 4)]
176    #[case(6, 1, 5)]
177    #[case(7, 2, 5)]
178    #[case(8, 2, 6)]
179    #[case(9, 2, 7)]
180    #[case(10, 3, 7)]
181    #[case(11, 3, 8)]
182    #[case(12, 3, 9)]
183    #[case(13, 4, 9)]
184    #[case(14, 4, 10)]
185    #[case(15, 4, 11)]
186    #[case(16, 5, 11)]
187    #[case(17, 5, 12)]
188    #[case(18, 5, 13)]
189    #[case(19, 6, 13)]
190    #[case(20, 6, 14)]
191    #[case(21, 6, 15)]
192    fn test_bft3f1_quorum_and_max_faults(
193        #[case] n: u32,
194        #[case] expected_f: u32,
195        #[case] expected_q: u32,
196    ) {
197        assert_eq!(N3f1::max_faults(n), expected_f);
198        assert_eq!(N3f1::quorum(n), expected_q);
199        // Verify the invariant: n = f + q
200        assert_eq!(n, expected_f + expected_q);
201    }
202
203    #[test]
204    #[should_panic(expected = "n must not be zero")]
205    fn test_bft5f1_max_faults_zero_panics() {
206        N5f1::max_faults(0);
207    }
208
209    #[test]
210    #[should_panic(expected = "n must not be zero")]
211    fn test_bft5f1_quorum_zero_panics() {
212        N5f1::quorum(0);
213    }
214
215    #[test]
216    #[should_panic(expected = "n must not be zero")]
217    fn test_bft5f1_m_quorum_zero_panics() {
218        N5f1::m_quorum(0);
219    }
220
221    #[test]
222    #[should_panic(expected = "n must not be zero")]
223    fn test_bft5f1_l_quorum_zero_panics() {
224        N5f1::l_quorum(0);
225    }
226
227    #[rstest]
228    // n=1 to n=5: f=0
229    #[case(1, 0, 1, 1)]
230    #[case(2, 0, 2, 1)]
231    #[case(3, 0, 3, 1)]
232    #[case(4, 0, 4, 1)]
233    #[case(5, 0, 5, 1)]
234    // n=6 to n=10: f=1
235    #[case(6, 1, 5, 3)]
236    #[case(7, 1, 6, 3)]
237    #[case(8, 1, 7, 3)]
238    #[case(9, 1, 8, 3)]
239    #[case(10, 1, 9, 3)]
240    // n=11 to n=15: f=2
241    #[case(11, 2, 9, 5)]
242    #[case(12, 2, 10, 5)]
243    #[case(13, 2, 11, 5)]
244    #[case(14, 2, 12, 5)]
245    #[case(15, 2, 13, 5)]
246    // n=16 to n=20: f=3
247    #[case(16, 3, 13, 7)]
248    #[case(17, 3, 14, 7)]
249    #[case(18, 3, 15, 7)]
250    #[case(19, 3, 16, 7)]
251    #[case(20, 3, 17, 7)]
252    // n=21: f=4
253    #[case(21, 4, 17, 9)]
254    fn test_bft5f1_quorums(
255        #[case] n: u32,
256        #[case] expected_f: u32,
257        #[case] expected_l_quorum: u32,
258        #[case] expected_m_quorum: u32,
259    ) {
260        assert_eq!(N5f1::max_faults(n), expected_f);
261        assert_eq!(N5f1::quorum(n), expected_l_quorum);
262        assert_eq!(N5f1::l_quorum(n), expected_l_quorum);
263        assert_eq!(N5f1::m_quorum(n), expected_m_quorum);
264
265        // Verify invariants
266        assert_eq!(n, expected_f + expected_l_quorum); // n = f + q
267        assert_eq!(expected_m_quorum, 2 * expected_f + 1); // m = 2f + 1
268    }
269
270    #[test]
271    fn test_generic_integer_types() {
272        // Test with various integer types
273        assert_eq!(N3f1::max_faults(10u8), 3);
274        assert_eq!(N3f1::max_faults(10u16), 3);
275        assert_eq!(N3f1::max_faults(10u32), 3);
276        assert_eq!(N3f1::max_faults(10u64), 3);
277        assert_eq!(N3f1::max_faults(10usize), 3);
278        assert_eq!(N3f1::max_faults(10i32), 3);
279        assert_eq!(N3f1::max_faults(10i64), 3);
280
281        assert_eq!(N3f1::quorum(10u8), 7);
282        assert_eq!(N3f1::quorum(10u16), 7);
283        assert_eq!(N3f1::quorum(10u64), 7);
284        assert_eq!(N3f1::quorum(10usize), 7);
285        assert_eq!(N3f1::quorum(10i32), 7);
286        assert_eq!(N3f1::quorum(10i64), 7);
287
288        assert_eq!(N5f1::max_faults(10u64), 1);
289        assert_eq!(N5f1::quorum(10usize), 9);
290        assert_eq!(N5f1::m_quorum(10i32), 3);
291        assert_eq!(N5f1::l_quorum(10i64), 9);
292    }
293
294    #[test]
295    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
296    fn test_max_faults_negative_panics() {
297        N3f1::max_faults(-1i32);
298    }
299
300    #[test]
301    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
302    fn test_max_faults_overflow_panics() {
303        N3f1::max_faults(u64::MAX);
304    }
305
306    #[test]
307    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
308    fn test_quorum_negative_panics() {
309        N3f1::quorum(-1i32);
310    }
311
312    #[test]
313    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
314    fn test_quorum_overflow_panics() {
315        N3f1::quorum(u64::MAX);
316    }
317
318    proptest! {
319        /// N5f1 quorum relationships must hold for all valid participant counts.
320        ///
321        /// For n >= 6 (where f >= 1):
322        /// - M-quorum (2f+1) < L-quorum (n-f)
323        /// - Both quorums must be achievable (<= n)
324        #[test]
325        fn test_n5f1_quorum_relationships(n in 6u32..10_000) {
326            let m = N5f1::m_quorum(n);
327            let l = N5f1::l_quorum(n);
328
329            // M-quorum must be strictly less than L-quorum
330            prop_assert!(
331                m < l,
332                "M-quorum ({}) should be less than L-quorum ({}) for n={}",
333                m, l, n
334            );
335
336            // Both quorums must be achievable
337            prop_assert!(m <= n, "M-quorum ({}) should be <= n ({})", m, n);
338            prop_assert!(l <= n, "L-quorum ({}) should be <= n ({})", l, n);
339        }
340
341        /// BFT safety property: two quorums must intersect in at least one honest node.
342        ///
343        /// Mathematically: 2q - n > f, or equivalently: 2q > n + f
344        ///
345        /// This ensures that any two quorums share at least one honest participant,
346        /// which is fundamental for BFT consensus safety.
347        #[test]
348        fn test_bft_model_safety_property(n in 1u32..10_000) {
349            // N3f1 safety
350            let f_3f1 = N3f1::max_faults(n);
351            let q_3f1 = N3f1::quorum(n);
352            prop_assert!(
353                2 * q_3f1 > n + f_3f1,
354                "N3f1 safety violated for n={}: 2*{} <= {} + {}",
355                n, q_3f1, n, f_3f1
356            );
357
358            // N5f1 safety
359            let f_5f1 = N5f1::max_faults(n);
360            let q_5f1 = N5f1::quorum(n);
361            prop_assert!(
362                2 * q_5f1 > n + f_5f1,
363                "N5f1 safety violated for n={}: 2*{} <= {} + {}",
364                n, q_5f1, n, f_5f1
365            );
366        }
367    }
368}