agentic_payments/consensus/
quorum.rs

1//! Quorum Management
2//!
3//! Implements Byzantine Fault Tolerant quorum calculations with
4//! weighted voting and dynamic threshold management.
5
6use super::{Authority, AuthorityId};
7use crate::error::{Error, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Quorum configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct QuorumConfig {
14    /// Minimum quorum threshold (e.g., 0.67 for 2/3)
15    pub threshold: f64,
16    /// Minimum number of authorities required
17    pub min_authorities: usize,
18    /// Maximum Byzantine faults tolerated (f in 3f+1)
19    pub max_faults: usize,
20    /// Use weighted voting
21    pub use_weights: bool,
22}
23
24impl Default for QuorumConfig {
25    fn default() -> Self {
26        QuorumConfig {
27            threshold: 0.67, // 2/3 threshold
28            min_authorities: 4, // Minimum 3f+1 = 4 for f=1
29            max_faults: 1,
30            use_weights: true,
31        }
32    }
33}
34
35impl QuorumConfig {
36    /// Create config for Byzantine fault tolerance
37    /// Requires 3f+1 authorities to tolerate f faults
38    pub fn byzantine(max_faults: usize) -> Self {
39        QuorumConfig {
40            threshold: 0.67,
41            min_authorities: 3 * max_faults + 1,
42            max_faults,
43            use_weights: true,
44        }
45    }
46
47    /// Validate configuration
48    pub fn validate(&self) -> Result<()> {
49        if self.threshold <= 0.5 || self.threshold > 1.0 {
50            return Err(Error::InvalidState {
51                message: format!("Invalid threshold: {}", self.threshold),
52            });
53        }
54
55        if self.min_authorities < 3 * self.max_faults + 1 {
56            return Err(Error::InvalidState {
57                message: format!(
58                    "Insufficient authorities for {} faults: need {}, got {}",
59                    self.max_faults,
60                    3 * self.max_faults + 1,
61                    self.min_authorities
62                ),
63            });
64        }
65
66        Ok(())
67    }
68}
69
70/// Quorum manager
71pub struct Quorum {
72    config: QuorumConfig,
73    authorities: HashMap<AuthorityId, Authority>,
74    total_weight: u64,
75}
76
77impl Quorum {
78    pub fn new(config: QuorumConfig, authorities: Vec<Authority>) -> Result<Self> {
79        config.validate()?;
80
81        if authorities.len() < config.min_authorities {
82            return Err(Error::InvalidState {
83                message: format!(
84                    "Insufficient authorities: need {}, got {}",
85                    config.min_authorities,
86                    authorities.len()
87                ),
88            });
89        }
90
91        let total_weight: u64 = if config.use_weights {
92            authorities.iter().map(|a| a.weight).sum()
93        } else {
94            authorities.len() as u64
95        };
96
97        let authorities: HashMap<_, _> = authorities
98            .into_iter()
99            .map(|a| (a.id.clone(), a))
100            .collect();
101
102        Ok(Quorum {
103            config,
104            authorities,
105            total_weight,
106        })
107    }
108
109    /// Calculate required quorum weight
110    pub fn required_weight(&self) -> u64 {
111        (self.total_weight as f64 * self.config.threshold).ceil() as u64
112    }
113
114    /// Check if vote weight meets quorum
115    pub fn has_quorum(&self, weight: u64) -> bool {
116        weight >= self.required_weight()
117    }
118
119    /// Get authority weight
120    pub fn get_weight(&self, authority: &AuthorityId) -> Result<u64> {
121        self.authorities
122            .get(authority)
123            .map(|a| {
124                if self.config.use_weights {
125                    a.weight
126                } else {
127                    1
128                }
129            })
130            .ok_or_else(|| Error::AuthorityNotFound {
131                authority: authority.0.clone(),
132            })
133    }
134
135    /// Get authority
136    pub fn get_authority(&self, authority: &AuthorityId) -> Result<&Authority> {
137        self.authorities
138            .get(authority)
139            .ok_or_else(|| Error::AuthorityNotFound {
140                authority: authority.0.clone(),
141            })
142    }
143
144    /// Calculate weight for a set of authorities
145    pub fn calculate_weight<'a>(
146        &self,
147        authorities: impl Iterator<Item = &'a AuthorityId>,
148    ) -> u64 {
149        authorities
150            .filter_map(|id| self.get_weight(id).ok())
151            .sum()
152    }
153
154    /// Get total weight
155    pub fn total_weight(&self) -> u64 {
156        self.total_weight
157    }
158
159    /// Get all authorities
160    pub fn authorities(&self) -> Vec<&Authority> {
161        self.authorities.values().collect()
162    }
163
164    /// Get number of authorities
165    pub fn authority_count(&self) -> usize {
166        self.authorities.len()
167    }
168
169    /// Calculate maximum tolerated Byzantine faults
170    pub fn max_byzantine_faults(&self) -> usize {
171        (self.authorities.len() - 1) / 3
172    }
173
174    /// Check if quorum can still be reached given known Byzantine nodes
175    pub fn can_reach_quorum(&self, byzantine_count: usize) -> bool {
176        byzantine_count <= self.max_byzantine_faults()
177    }
178
179    /// Update authority weight
180    pub fn update_weight(&mut self, authority: &AuthorityId, new_weight: u64) -> Result<()> {
181        let auth = self.authorities.get_mut(authority).ok_or_else(|| {
182            Error::AuthorityNotFound {
183                authority: authority.0.clone(),
184            }
185        })?;
186
187        if self.config.use_weights {
188            self.total_weight = self.total_weight - auth.weight + new_weight;
189            auth.weight = new_weight;
190        }
191
192        Ok(())
193    }
194
195    /// Mark authority as Byzantine
196    pub fn mark_byzantine(&mut self, authority: &AuthorityId) -> Result<()> {
197        let auth = self.authorities.get_mut(authority).ok_or_else(|| {
198            Error::AuthorityNotFound {
199                authority: authority.0.clone(),
200            }
201        })?;
202
203        auth.is_byzantine = true;
204        Ok(())
205    }
206
207    /// Get Byzantine authority count
208    pub fn byzantine_count(&self) -> usize {
209        self.authorities
210            .values()
211            .filter(|a| a.is_byzantine)
212            .count()
213    }
214
215    /// Check if system is still Byzantine fault tolerant
216    pub fn is_fault_tolerant(&self) -> bool {
217        self.can_reach_quorum(self.byzantine_count())
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn create_test_authorities(count: usize, weight: u64) -> Vec<Authority> {
226        (0..count)
227            .map(|i| Authority::new(AuthorityId::from(format!("auth-{}", i)), weight))
228            .collect()
229    }
230
231    #[test]
232    fn test_quorum_config_validation() {
233        let config = QuorumConfig::default();
234        assert!(config.validate().is_ok());
235
236        let invalid_config = QuorumConfig {
237            threshold: 0.4,
238            ..Default::default()
239        };
240        assert!(invalid_config.validate().is_err());
241    }
242
243    #[test]
244    fn test_byzantine_config() {
245        let config = QuorumConfig::byzantine(1);
246        assert_eq!(config.min_authorities, 4); // 3*1 + 1
247        assert_eq!(config.max_faults, 1);
248
249        let config = QuorumConfig::byzantine(2);
250        assert_eq!(config.min_authorities, 7); // 3*2 + 1
251    }
252
253    #[test]
254    fn test_quorum_creation() {
255        let authorities = create_test_authorities(4, 100);
256        let config = QuorumConfig::default();
257        let quorum = Quorum::new(config, authorities).unwrap();
258
259        assert_eq!(quorum.total_weight(), 400);
260        assert_eq!(quorum.authority_count(), 4);
261    }
262
263    #[test]
264    fn test_insufficient_authorities() {
265        let authorities = create_test_authorities(2, 100);
266        let config = QuorumConfig::byzantine(1); // Needs 4
267        let result = Quorum::new(config, authorities);
268
269        assert!(result.is_err());
270    }
271
272    #[test]
273    fn test_required_weight() {
274        let authorities = create_test_authorities(4, 100);
275        let config = QuorumConfig {
276            threshold: 0.67,
277            ..Default::default()
278        };
279        let quorum = Quorum::new(config, authorities).unwrap();
280
281        // 400 * 0.67 = 268, ceiling = 268
282        assert_eq!(quorum.required_weight(), 268);
283    }
284
285    #[test]
286    fn test_has_quorum() {
287        let authorities = create_test_authorities(4, 100);
288        let quorum = Quorum::new(QuorumConfig::default(), authorities).unwrap();
289
290        assert!(!quorum.has_quorum(250));
291        assert!(quorum.has_quorum(268));
292        assert!(quorum.has_quorum(300));
293    }
294
295    #[test]
296    fn test_calculate_weight() {
297        let authorities = create_test_authorities(4, 100);
298        let quorum = Quorum::new(QuorumConfig::default(), authorities).unwrap();
299
300        let ids: Vec<_> = (0..3)
301            .map(|i| AuthorityId::from(format!("auth-{}", i)))
302            .collect();
303        let weight = quorum.calculate_weight(ids.iter());
304
305        assert_eq!(weight, 300);
306    }
307
308    #[test]
309    fn test_max_byzantine_faults() {
310        let authorities = create_test_authorities(4, 100);
311        let quorum = Quorum::new(QuorumConfig::default(), authorities).unwrap();
312
313        assert_eq!(quorum.max_byzantine_faults(), 1); // (4-1)/3 = 1
314
315        let authorities = create_test_authorities(7, 100);
316        let quorum = Quorum::new(QuorumConfig::default(), authorities).unwrap();
317
318        assert_eq!(quorum.max_byzantine_faults(), 2); // (7-1)/3 = 2
319    }
320
321    #[test]
322    fn test_mark_byzantine() {
323        let authorities = create_test_authorities(4, 100);
324        let mut quorum = Quorum::new(QuorumConfig::default(), authorities).unwrap();
325
326        let auth_id = AuthorityId::from("auth-0");
327        quorum.mark_byzantine(&auth_id).unwrap();
328
329        assert_eq!(quorum.byzantine_count(), 1);
330        assert!(quorum.is_fault_tolerant());
331
332        quorum.mark_byzantine(&AuthorityId::from("auth-1")).unwrap();
333        assert_eq!(quorum.byzantine_count(), 2);
334        assert!(!quorum.is_fault_tolerant()); // 2 faults with 4 nodes
335    }
336
337    #[test]
338    fn test_update_weight() {
339        let authorities = create_test_authorities(4, 100);
340        let mut quorum = Quorum::new(QuorumConfig::default(), authorities).unwrap();
341
342        let auth_id = AuthorityId::from("auth-0");
343        quorum.update_weight(&auth_id, 200).unwrap();
344
345        assert_eq!(quorum.total_weight(), 500); // 200 + 100 + 100 + 100
346        assert_eq!(quorum.get_weight(&auth_id).unwrap(), 200);
347    }
348
349    #[test]
350    fn test_unweighted_voting() {
351        let authorities = create_test_authorities(4, 100);
352        let config = QuorumConfig {
353            use_weights: false,
354            ..Default::default()
355        };
356        let quorum = Quorum::new(config, authorities).unwrap();
357
358        assert_eq!(quorum.total_weight(), 4); // Count, not sum of weights
359        assert_eq!(quorum.get_weight(&AuthorityId::from("auth-0")).unwrap(), 1);
360    }
361}