agentic_payments/consensus/
reputation.rs1use super::AuthorityId;
7use crate::error::{Error, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ReputationConfig {
14 pub initial_reputation: f64,
16 pub min_reputation: f64,
18 pub max_reputation: f64,
20 pub correct_vote_reward: f64,
22 pub incorrect_vote_penalty: f64,
24 pub timeout_penalty: f64,
26 pub byzantine_penalty: f64,
28 pub decay_rate: f64,
30}
31
32impl Default for ReputationConfig {
33 fn default() -> Self {
34 ReputationConfig {
35 initial_reputation: 1.0,
36 min_reputation: 0.1,
37 max_reputation: 2.0,
38 correct_vote_reward: 0.01,
39 incorrect_vote_penalty: 0.05,
40 timeout_penalty: 0.02,
41 byzantine_penalty: 0.5,
42 decay_rate: 0.001,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ReputationEntry {
50 pub authority: AuthorityId,
51 pub reputation: f64,
52 pub correct_votes: u64,
53 pub incorrect_votes: u64,
54 pub timeouts: u64,
55 pub byzantine_faults: u64,
56 pub total_rounds: u64,
57}
58
59impl ReputationEntry {
60 pub fn new(authority: AuthorityId, initial_reputation: f64) -> Self {
61 ReputationEntry {
62 authority,
63 reputation: initial_reputation,
64 correct_votes: 0,
65 incorrect_votes: 0,
66 timeouts: 0,
67 byzantine_faults: 0,
68 total_rounds: 0,
69 }
70 }
71
72 pub fn accuracy(&self) -> f64 {
74 let total = self.correct_votes + self.incorrect_votes;
75 if total == 0 {
76 1.0
77 } else {
78 self.correct_votes as f64 / total as f64
79 }
80 }
81
82 pub fn reliability(&self) -> f64 {
84 if self.total_rounds == 0 {
85 1.0
86 } else {
87 let participated = self.total_rounds - self.timeouts;
88 participated as f64 / self.total_rounds as f64
89 }
90 }
91
92 pub fn is_trustworthy(&self, min_reputation: f64) -> bool {
94 self.reputation >= min_reputation && self.byzantine_faults == 0
95 }
96}
97
98pub struct ReputationSystem {
100 config: ReputationConfig,
101 reputations: HashMap<AuthorityId, ReputationEntry>,
102}
103
104impl ReputationSystem {
105 pub fn new(config: ReputationConfig) -> Self {
106 ReputationSystem {
107 config,
108 reputations: HashMap::new(),
109 }
110 }
111
112 pub fn register_authority(&mut self, authority: AuthorityId) {
114 if !self.reputations.contains_key(&authority) {
115 self.reputations.insert(
116 authority.clone(),
117 ReputationEntry::new(authority, self.config.initial_reputation),
118 );
119 }
120 }
121
122 pub fn get_reputation(&self, authority: &AuthorityId) -> Result<f64> {
124 self.reputations
125 .get(authority)
126 .map(|e| e.reputation)
127 .ok_or_else(|| Error::AuthorityNotFound {
128 authority: authority.0.clone(),
129 })
130 }
131
132 pub fn get_entry(&self, authority: &AuthorityId) -> Result<&ReputationEntry> {
134 self.reputations
135 .get(authority)
136 .ok_or_else(|| Error::AuthorityNotFound {
137 authority: authority.0.clone(),
138 })
139 }
140
141 pub fn record_correct_vote(&mut self, authority: &AuthorityId) -> Result<()> {
143 let entry = self.reputations.get_mut(authority).ok_or_else(|| {
144 Error::AuthorityNotFound {
145 authority: authority.0.clone(),
146 }
147 })?;
148
149 entry.correct_votes += 1;
150 entry.total_rounds += 1;
151 entry.reputation =
152 (entry.reputation + self.config.correct_vote_reward).min(self.config.max_reputation);
153
154 Ok(())
155 }
156
157 pub fn record_incorrect_vote(&mut self, authority: &AuthorityId) -> Result<()> {
159 let entry = self.reputations.get_mut(authority).ok_or_else(|| {
160 Error::AuthorityNotFound {
161 authority: authority.0.clone(),
162 }
163 })?;
164
165 entry.incorrect_votes += 1;
166 entry.total_rounds += 1;
167 entry.reputation =
168 (entry.reputation - self.config.incorrect_vote_penalty).max(self.config.min_reputation);
169
170 Ok(())
171 }
172
173 pub fn record_timeout(&mut self, authority: &AuthorityId) -> Result<()> {
175 let entry = self.reputations.get_mut(authority).ok_or_else(|| {
176 Error::AuthorityNotFound {
177 authority: authority.0.clone(),
178 }
179 })?;
180
181 entry.timeouts += 1;
182 entry.total_rounds += 1;
183 entry.reputation =
184 (entry.reputation - self.config.timeout_penalty).max(self.config.min_reputation);
185
186 Ok(())
187 }
188
189 pub fn record_byzantine_fault(&mut self, authority: &AuthorityId) -> Result<()> {
191 let entry = self.reputations.get_mut(authority).ok_or_else(|| {
192 Error::AuthorityNotFound {
193 authority: authority.0.clone(),
194 }
195 })?;
196
197 entry.byzantine_faults += 1;
198 entry.reputation =
199 (entry.reputation - self.config.byzantine_penalty).max(self.config.min_reputation);
200
201 Ok(())
202 }
203
204 pub fn apply_decay(&mut self) {
206 for entry in self.reputations.values_mut() {
207 let decay = entry.reputation * self.config.decay_rate;
208 entry.reputation = (entry.reputation - decay).max(self.config.min_reputation);
209 }
210 }
211
212 pub fn get_trustworthy_authorities(&self) -> Vec<AuthorityId> {
214 self.reputations
215 .values()
216 .filter(|e| e.is_trustworthy(self.config.min_reputation))
217 .map(|e| e.authority.clone())
218 .collect()
219 }
220
221 pub fn calculate_weighted_reputation(&self, authority: &AuthorityId) -> Result<f64> {
223 let entry = self.get_entry(authority)?;
224
225 let base_reputation = entry.reputation;
227 let accuracy_weight = entry.accuracy();
228 let reliability_weight = entry.reliability();
229
230 Ok(base_reputation * accuracy_weight * reliability_weight)
231 }
232
233 pub fn get_ranked_authorities(&self) -> Vec<(AuthorityId, f64)> {
235 let mut ranked: Vec<_> = self
236 .reputations
237 .values()
238 .map(|e| (e.authority.clone(), e.reputation))
239 .collect();
240
241 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
242 ranked
243 }
244
245 pub fn get_statistics(&self) -> ReputationStatistics {
247 let reputations: Vec<f64> = self.reputations.values().map(|e| e.reputation).collect();
248
249 let avg_reputation = if reputations.is_empty() {
250 0.0
251 } else {
252 reputations.iter().sum::<f64>() / reputations.len() as f64
253 };
254
255 let total_byzantine = self
256 .reputations
257 .values()
258 .filter(|e| e.byzantine_faults > 0)
259 .count();
260
261 let total_trustworthy = self
262 .reputations
263 .values()
264 .filter(|e| e.is_trustworthy(self.config.min_reputation))
265 .count();
266
267 ReputationStatistics {
268 total_authorities: self.reputations.len(),
269 avg_reputation,
270 min_reputation: reputations.iter().cloned().fold(f64::INFINITY, f64::min),
271 max_reputation: reputations.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
272 byzantine_count: total_byzantine,
273 trustworthy_count: total_trustworthy,
274 }
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct ReputationStatistics {
281 pub total_authorities: usize,
282 pub avg_reputation: f64,
283 pub min_reputation: f64,
284 pub max_reputation: f64,
285 pub byzantine_count: usize,
286 pub trustworthy_count: usize,
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_reputation_entry() {
295 let entry = ReputationEntry::new(AuthorityId::from("auth-1"), 1.0);
296 assert_eq!(entry.reputation, 1.0);
297 assert_eq!(entry.accuracy(), 1.0);
298 assert_eq!(entry.reliability(), 1.0);
299 }
300
301 #[test]
302 fn test_accuracy_calculation() {
303 let mut entry = ReputationEntry::new(AuthorityId::from("auth-1"), 1.0);
304 entry.correct_votes = 8;
305 entry.incorrect_votes = 2;
306 assert_eq!(entry.accuracy(), 0.8);
307 }
308
309 #[test]
310 fn test_reliability_calculation() {
311 let mut entry = ReputationEntry::new(AuthorityId::from("auth-1"), 1.0);
312 entry.total_rounds = 10;
313 entry.timeouts = 2;
314 assert_eq!(entry.reliability(), 0.8);
315 }
316
317 #[test]
318 fn test_register_authority() {
319 let mut system = ReputationSystem::new(ReputationConfig::default());
320 let auth = AuthorityId::from("auth-1");
321
322 system.register_authority(auth.clone());
323 assert_eq!(system.get_reputation(&auth).unwrap(), 1.0);
324 }
325
326 #[test]
327 fn test_record_correct_vote() {
328 let mut system = ReputationSystem::new(ReputationConfig::default());
329 let auth = AuthorityId::from("auth-1");
330 system.register_authority(auth.clone());
331
332 system.record_correct_vote(&auth).unwrap();
333 assert!(system.get_reputation(&auth).unwrap() > 1.0);
334
335 let entry = system.get_entry(&auth).unwrap();
336 assert_eq!(entry.correct_votes, 1);
337 assert_eq!(entry.total_rounds, 1);
338 }
339
340 #[test]
341 fn test_record_incorrect_vote() {
342 let mut system = ReputationSystem::new(ReputationConfig::default());
343 let auth = AuthorityId::from("auth-1");
344 system.register_authority(auth.clone());
345
346 system.record_incorrect_vote(&auth).unwrap();
347 assert!(system.get_reputation(&auth).unwrap() < 1.0);
348
349 let entry = system.get_entry(&auth).unwrap();
350 assert_eq!(entry.incorrect_votes, 1);
351 }
352
353 #[test]
354 fn test_record_timeout() {
355 let mut system = ReputationSystem::new(ReputationConfig::default());
356 let auth = AuthorityId::from("auth-1");
357 system.register_authority(auth.clone());
358
359 system.record_timeout(&auth).unwrap();
360 assert!(system.get_reputation(&auth).unwrap() < 1.0);
361
362 let entry = system.get_entry(&auth).unwrap();
363 assert_eq!(entry.timeouts, 1);
364 }
365
366 #[test]
367 fn test_record_byzantine_fault() {
368 let mut system = ReputationSystem::new(ReputationConfig::default());
369 let auth = AuthorityId::from("auth-1");
370 system.register_authority(auth.clone());
371
372 let initial = system.get_reputation(&auth).unwrap();
373 system.record_byzantine_fault(&auth).unwrap();
374
375 assert!(system.get_reputation(&auth).unwrap() < initial);
376 let entry = system.get_entry(&auth).unwrap();
377 assert_eq!(entry.byzantine_faults, 1);
378 assert!(!entry.is_trustworthy(0.1));
379 }
380
381 #[test]
382 fn test_reputation_decay() {
383 let config = ReputationConfig {
384 decay_rate: 0.1,
385 ..Default::default()
386 };
387 let mut system = ReputationSystem::new(config);
388 let auth = AuthorityId::from("auth-1");
389 system.register_authority(auth.clone());
390
391 let initial = system.get_reputation(&auth).unwrap();
392 system.apply_decay();
393
394 assert!(system.get_reputation(&auth).unwrap() < initial);
395 }
396
397 #[test]
398 fn test_reputation_bounds() {
399 let mut system = ReputationSystem::new(ReputationConfig::default());
400 let auth = AuthorityId::from("auth-1");
401 system.register_authority(auth.clone());
402
403 for _ in 0..100 {
405 system.record_correct_vote(&auth).unwrap();
406 }
407 assert!(system.get_reputation(&auth).unwrap() <= 2.0);
408
409 for _ in 0..100 {
411 system.record_incorrect_vote(&auth).unwrap();
412 }
413 assert!(system.get_reputation(&auth).unwrap() >= 0.1);
414 }
415
416 #[test]
417 fn test_weighted_reputation() {
418 let mut system = ReputationSystem::new(ReputationConfig::default());
419 let auth = AuthorityId::from("auth-1");
420 system.register_authority(auth.clone());
421
422 system.record_correct_vote(&auth).unwrap();
423 system.record_correct_vote(&auth).unwrap();
424 system.record_timeout(&auth).unwrap();
425
426 let weighted = system.calculate_weighted_reputation(&auth).unwrap();
427 let base = system.get_reputation(&auth).unwrap();
428
429 assert!(weighted < base);
431 }
432
433 #[test]
434 fn test_ranked_authorities() {
435 let mut system = ReputationSystem::new(ReputationConfig::default());
436
437 for i in 0..3 {
438 let auth = AuthorityId::from(format!("auth-{}", i));
439 system.register_authority(auth.clone());
440
441 for _ in 0..i {
442 system.record_correct_vote(&auth).unwrap();
443 }
444 }
445
446 let ranked = system.get_ranked_authorities();
447 assert_eq!(ranked.len(), 3);
448
449 assert!(ranked[0].1 >= ranked[1].1);
451 assert!(ranked[1].1 >= ranked[2].1);
452 }
453
454 #[test]
455 fn test_statistics() {
456 let mut system = ReputationSystem::new(ReputationConfig::default());
457
458 for i in 0..5 {
459 let auth = AuthorityId::from(format!("auth-{}", i));
460 system.register_authority(auth.clone());
461 }
462
463 system.record_byzantine_fault(&AuthorityId::from("auth-0")).unwrap();
464
465 let stats = system.get_statistics();
466 assert_eq!(stats.total_authorities, 5);
467 assert_eq!(stats.byzantine_count, 1);
468 assert_eq!(stats.trustworthy_count, 4);
469 }
470}