agentic_payments/consensus/
quorum.rs1use super::{Authority, AuthorityId};
7use crate::error::{Error, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct QuorumConfig {
14 pub threshold: f64,
16 pub min_authorities: usize,
18 pub max_faults: usize,
20 pub use_weights: bool,
22}
23
24impl Default for QuorumConfig {
25 fn default() -> Self {
26 QuorumConfig {
27 threshold: 0.67, min_authorities: 4, max_faults: 1,
30 use_weights: true,
31 }
32 }
33}
34
35impl QuorumConfig {
36 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 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
70pub 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 pub fn required_weight(&self) -> u64 {
111 (self.total_weight as f64 * self.config.threshold).ceil() as u64
112 }
113
114 pub fn has_quorum(&self, weight: u64) -> bool {
116 weight >= self.required_weight()
117 }
118
119 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 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 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 pub fn total_weight(&self) -> u64 {
156 self.total_weight
157 }
158
159 pub fn authorities(&self) -> Vec<&Authority> {
161 self.authorities.values().collect()
162 }
163
164 pub fn authority_count(&self) -> usize {
166 self.authorities.len()
167 }
168
169 pub fn max_byzantine_faults(&self) -> usize {
171 (self.authorities.len() - 1) / 3
172 }
173
174 pub fn can_reach_quorum(&self, byzantine_count: usize) -> bool {
176 byzantine_count <= self.max_byzantine_faults()
177 }
178
179 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 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 pub fn byzantine_count(&self) -> usize {
209 self.authorities
210 .values()
211 .filter(|a| a.is_byzantine)
212 .count()
213 }
214
215 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); assert_eq!(config.max_faults, 1);
248
249 let config = QuorumConfig::byzantine(2);
250 assert_eq!(config.min_authorities, 7); }
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); 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 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); 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); }
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()); }
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); 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); assert_eq!(quorum.get_weight(&AuthorityId::from("auth-0")).unwrap(), 1);
360 }
361}