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    /// Compute the quorum size from a slice length.
70    ///
71    /// Convenience method that converts the slice length to `u32` and calls [`Self::quorum`].
72    ///
73    /// # Panics
74    ///
75    /// Panics if the slice is empty or its length exceeds `u32::MAX`.
76    fn quorum_from_slice<T>(slice: &[T]) -> u32 {
77        let n: u32 = slice
78            .len()
79            .try_into()
80            .expect("slice length must be less than u32::MAX");
81        Self::quorum(n)
82    }
83}
84
85/// Fault model requiring `n >= 3f + 1` participants.
86///
87/// Tolerates up to `f = (n-1)/3` faults with quorum size `q = n - f`.
88///
89/// For any two quorums Q1 and Q2, there exists at least one honest participant
90/// in their intersection (since `|Q1| + |Q2| > n + f`).
91///
92/// # Example
93///
94/// | n  | f  | quorum |
95/// |----|----| -------|
96/// | 4  | 1  | 3      |
97/// | 7  | 2  | 5      |
98/// | 10 | 3  | 7      |
99/// | 13 | 4  | 9      |
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub struct N3f1;
102
103impl Faults for N3f1 {
104    fn max_faults(n: impl ToPrimitive) -> u32 {
105        let n = n
106            .to_u32()
107            .expect("n must be a non-negative integer that fits in u32");
108        assert!(n > 0, "n must not be zero");
109        (n - 1) / 3
110    }
111}
112
113/// Fault model requiring `n >= 5f + 1` participants.
114///
115/// Tolerates up to `f = (n-1)/5` faults with quorum size `q = n - f` (also
116/// provided as [`l_quorum`](Self::l_quorum)).
117///
118/// Also provides [`m_quorum`](Self::m_quorum) which computes `2f + 1`.
119///
120/// # Example
121///
122/// | n  | f  | quorum (n-f) | m-quorum (2f+1) |
123/// |----|----| -------------|-----------------|
124/// | 6  | 1  | 5            | 3               |
125/// | 11 | 2  | 9            | 5               |
126/// | 16 | 3  | 13           | 7               |
127/// | 21 | 4  | 17           | 9               |
128#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
129pub struct N5f1;
130
131impl Faults for N5f1 {
132    fn max_faults(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        (n - 1) / 5
138    }
139}
140
141impl N5f1 {
142    /// Compute `2f + 1`.
143    ///
144    /// # Panics
145    ///
146    /// Panics if `n` is zero, negative, or exceeds `u32::MAX`.
147    pub fn m_quorum(n: impl ToPrimitive) -> u32 {
148        let n = n
149            .to_u32()
150            .expect("n must be a non-negative integer that fits in u32");
151        assert!(n > 0, "n must not be zero");
152        2 * Self::max_faults(n) + 1
153    }
154
155    /// Compute `n - f`.
156    ///
157    /// This is equivalent to [`Self::quorum`].
158    ///
159    /// # Panics
160    ///
161    /// Panics if `n` is zero, negative, or exceeds `u32::MAX`.
162    pub fn l_quorum(n: impl ToPrimitive) -> u32 {
163        Self::quorum(n)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use proptest::prelude::*;
171    use rstest::rstest;
172
173    #[test]
174    #[should_panic(expected = "n must not be zero")]
175    fn test_bft3f1_max_faults_zero_panics() {
176        N3f1::max_faults(0);
177    }
178
179    #[test]
180    #[should_panic(expected = "n must not be zero")]
181    fn test_bft3f1_quorum_zero_panics() {
182        N3f1::quorum(0);
183    }
184
185    #[rstest]
186    #[case(1, 0, 1)]
187    #[case(2, 0, 2)]
188    #[case(3, 0, 3)]
189    #[case(4, 1, 3)]
190    #[case(5, 1, 4)]
191    #[case(6, 1, 5)]
192    #[case(7, 2, 5)]
193    #[case(8, 2, 6)]
194    #[case(9, 2, 7)]
195    #[case(10, 3, 7)]
196    #[case(11, 3, 8)]
197    #[case(12, 3, 9)]
198    #[case(13, 4, 9)]
199    #[case(14, 4, 10)]
200    #[case(15, 4, 11)]
201    #[case(16, 5, 11)]
202    #[case(17, 5, 12)]
203    #[case(18, 5, 13)]
204    #[case(19, 6, 13)]
205    #[case(20, 6, 14)]
206    #[case(21, 6, 15)]
207    fn test_bft3f1_quorum_and_max_faults(
208        #[case] n: u32,
209        #[case] expected_f: u32,
210        #[case] expected_q: u32,
211    ) {
212        assert_eq!(N3f1::max_faults(n), expected_f);
213        assert_eq!(N3f1::quorum(n), expected_q);
214        // Verify the invariant: n = f + q
215        assert_eq!(n, expected_f + expected_q);
216    }
217
218    #[test]
219    #[should_panic(expected = "n must not be zero")]
220    fn test_bft5f1_max_faults_zero_panics() {
221        N5f1::max_faults(0);
222    }
223
224    #[test]
225    #[should_panic(expected = "n must not be zero")]
226    fn test_bft5f1_quorum_zero_panics() {
227        N5f1::quorum(0);
228    }
229
230    #[test]
231    #[should_panic(expected = "n must not be zero")]
232    fn test_bft5f1_m_quorum_zero_panics() {
233        N5f1::m_quorum(0);
234    }
235
236    #[test]
237    #[should_panic(expected = "n must not be zero")]
238    fn test_bft5f1_l_quorum_zero_panics() {
239        N5f1::l_quorum(0);
240    }
241
242    #[rstest]
243    // n=1 to n=5: f=0
244    #[case(1, 0, 1, 1)]
245    #[case(2, 0, 2, 1)]
246    #[case(3, 0, 3, 1)]
247    #[case(4, 0, 4, 1)]
248    #[case(5, 0, 5, 1)]
249    // n=6 to n=10: f=1
250    #[case(6, 1, 5, 3)]
251    #[case(7, 1, 6, 3)]
252    #[case(8, 1, 7, 3)]
253    #[case(9, 1, 8, 3)]
254    #[case(10, 1, 9, 3)]
255    // n=11 to n=15: f=2
256    #[case(11, 2, 9, 5)]
257    #[case(12, 2, 10, 5)]
258    #[case(13, 2, 11, 5)]
259    #[case(14, 2, 12, 5)]
260    #[case(15, 2, 13, 5)]
261    // n=16 to n=20: f=3
262    #[case(16, 3, 13, 7)]
263    #[case(17, 3, 14, 7)]
264    #[case(18, 3, 15, 7)]
265    #[case(19, 3, 16, 7)]
266    #[case(20, 3, 17, 7)]
267    // n=21: f=4
268    #[case(21, 4, 17, 9)]
269    fn test_bft5f1_quorums(
270        #[case] n: u32,
271        #[case] expected_f: u32,
272        #[case] expected_l_quorum: u32,
273        #[case] expected_m_quorum: u32,
274    ) {
275        assert_eq!(N5f1::max_faults(n), expected_f);
276        assert_eq!(N5f1::quorum(n), expected_l_quorum);
277        assert_eq!(N5f1::l_quorum(n), expected_l_quorum);
278        assert_eq!(N5f1::m_quorum(n), expected_m_quorum);
279
280        // Verify invariants
281        assert_eq!(n, expected_f + expected_l_quorum); // n = f + q
282        assert_eq!(expected_m_quorum, 2 * expected_f + 1); // m = 2f + 1
283    }
284
285    #[test]
286    fn test_quorum_from_slice() {
287        let items = vec![1, 2, 3, 4, 5, 6, 7];
288        assert_eq!(N3f1::quorum_from_slice(&items), 5); // n=7, f=2, q=5
289        assert_eq!(N5f1::quorum_from_slice(&items), 6); // n=7, f=1, q=6
290    }
291
292    #[test]
293    #[should_panic(expected = "n must not be zero")]
294    fn test_quorum_from_empty_slice_panics() {
295        let items: Vec<u8> = vec![];
296        N3f1::quorum_from_slice(&items);
297    }
298
299    #[test]
300    fn test_generic_integer_types() {
301        // Test with various integer types
302        assert_eq!(N3f1::max_faults(10u8), 3);
303        assert_eq!(N3f1::max_faults(10u16), 3);
304        assert_eq!(N3f1::max_faults(10u32), 3);
305        assert_eq!(N3f1::max_faults(10u64), 3);
306        assert_eq!(N3f1::max_faults(10usize), 3);
307        assert_eq!(N3f1::max_faults(10i32), 3);
308        assert_eq!(N3f1::max_faults(10i64), 3);
309
310        assert_eq!(N3f1::quorum(10u8), 7);
311        assert_eq!(N3f1::quorum(10u16), 7);
312        assert_eq!(N3f1::quorum(10u64), 7);
313        assert_eq!(N3f1::quorum(10usize), 7);
314        assert_eq!(N3f1::quorum(10i32), 7);
315        assert_eq!(N3f1::quorum(10i64), 7);
316
317        assert_eq!(N5f1::max_faults(10u64), 1);
318        assert_eq!(N5f1::quorum(10usize), 9);
319        assert_eq!(N5f1::m_quorum(10i32), 3);
320        assert_eq!(N5f1::l_quorum(10i64), 9);
321    }
322
323    #[test]
324    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
325    fn test_max_faults_negative_panics() {
326        N3f1::max_faults(-1i32);
327    }
328
329    #[test]
330    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
331    fn test_max_faults_overflow_panics() {
332        N3f1::max_faults(u64::MAX);
333    }
334
335    #[test]
336    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
337    fn test_quorum_negative_panics() {
338        N3f1::quorum(-1i32);
339    }
340
341    #[test]
342    #[should_panic(expected = "n must be a non-negative integer that fits in u32")]
343    fn test_quorum_overflow_panics() {
344        N3f1::quorum(u64::MAX);
345    }
346
347    proptest! {
348        /// N5f1 quorum relationships must hold for all valid participant counts.
349        ///
350        /// For n >= 6 (where f >= 1):
351        /// - M-quorum (2f+1) < L-quorum (n-f)
352        /// - Both quorums must be achievable (<= n)
353        #[test]
354        fn test_n5f1_quorum_relationships(n in 6u32..10_000) {
355            let m = N5f1::m_quorum(n);
356            let l = N5f1::l_quorum(n);
357
358            // M-quorum must be strictly less than L-quorum
359            prop_assert!(
360                m < l,
361                "M-quorum ({}) should be less than L-quorum ({}) for n={}",
362                m, l, n
363            );
364
365            // Both quorums must be achievable
366            prop_assert!(m <= n, "M-quorum ({}) should be <= n ({})", m, n);
367            prop_assert!(l <= n, "L-quorum ({}) should be <= n ({})", l, n);
368        }
369
370        /// BFT safety property: two quorums must intersect in at least one honest node.
371        ///
372        /// Mathematically: 2q - n > f, or equivalently: 2q > n + f
373        ///
374        /// This ensures that any two quorums share at least one honest participant,
375        /// which is fundamental for BFT consensus safety.
376        #[test]
377        fn test_bft_model_safety_property(n in 1u32..10_000) {
378            // N3f1 safety
379            let f_3f1 = N3f1::max_faults(n);
380            let q_3f1 = N3f1::quorum(n);
381            prop_assert!(
382                2 * q_3f1 > n + f_3f1,
383                "N3f1 safety violated for n={}: 2*{} <= {} + {}",
384                n, q_3f1, n, f_3f1
385            );
386
387            // N5f1 safety
388            let f_5f1 = N5f1::max_faults(n);
389            let q_5f1 = N5f1::quorum(n);
390            prop_assert!(
391                2 * q_5f1 > n + f_5f1,
392                "N5f1 safety violated for n={}: 2*{} <= {} + {}",
393                n, q_5f1, n, f_5f1
394            );
395        }
396    }
397}