1use std::collections::HashMap;
25
26#[derive(Clone, Debug, PartialEq)]
32pub enum PrReputationEvent {
33 BlockDelivered {
35 bytes: u64,
37 },
38 BlockTimeout,
40 ProtocolViolation,
42 GoodProof,
44 BadProof,
46 ConnectionDropped,
48}
49
50#[derive(Clone, Debug)]
56pub struct PrReputationScore {
57 pub score: f64,
59 pub total_events: u64,
61 pub violations: u32,
63}
64
65impl PrReputationScore {
66 fn new() -> Self {
67 Self {
68 score: 0.5,
69 total_events: 0,
70 violations: 0,
71 }
72 }
73
74 pub fn is_trusted(&self) -> bool {
76 self.score >= 0.7
77 }
78
79 pub fn is_banned(&self) -> bool {
82 self.score < 0.1 || self.violations >= 10
83 }
84
85 pub fn tier(&self) -> &'static str {
94 if self.score < 0.1 {
95 "banned"
96 } else if self.score < 0.4 {
97 "poor"
98 } else if self.score < 0.7 {
99 "neutral"
100 } else {
101 "trusted"
102 }
103 }
104}
105
106#[derive(Clone, Debug)]
112pub struct PrReputationConfig {
113 pub block_delivered_delta: f64,
115 pub block_timeout_delta: f64,
117 pub protocol_violation_delta: f64,
119 pub good_proof_delta: f64,
121 pub bad_proof_delta: f64,
123 pub connection_dropped_delta: f64,
125 pub decay_rate: f64,
127}
128
129impl Default for PrReputationConfig {
130 fn default() -> Self {
131 Self {
132 block_delivered_delta: 0.02,
133 block_timeout_delta: -0.05,
134 protocol_violation_delta: -0.15,
135 good_proof_delta: 0.05,
136 bad_proof_delta: -0.25,
137 connection_dropped_delta: -0.03,
138 decay_rate: 0.001,
139 }
140 }
141}
142
143impl PrReputationConfig {
144 fn delta_for(&self, event: &PrReputationEvent) -> f64 {
145 match event {
146 PrReputationEvent::BlockDelivered { .. } => self.block_delivered_delta,
147 PrReputationEvent::BlockTimeout => self.block_timeout_delta,
148 PrReputationEvent::ProtocolViolation => self.protocol_violation_delta,
149 PrReputationEvent::GoodProof => self.good_proof_delta,
150 PrReputationEvent::BadProof => self.bad_proof_delta,
151 PrReputationEvent::ConnectionDropped => self.connection_dropped_delta,
152 }
153 }
154
155 fn is_violation(event: &PrReputationEvent) -> bool {
156 matches!(
157 event,
158 PrReputationEvent::ProtocolViolation | PrReputationEvent::BadProof
159 )
160 }
161}
162
163#[derive(Clone, Debug)]
169pub struct PrReputationStats {
170 pub total_peers: usize,
172 pub trusted_count: usize,
174 pub banned_count: usize,
176 pub avg_score: f64,
178}
179
180pub struct PeerReputationManager {
189 scores: HashMap<String, PrReputationScore>,
190 config: PrReputationConfig,
191}
192
193impl PeerReputationManager {
194 pub fn new(config: PrReputationConfig) -> Self {
196 Self {
197 scores: HashMap::new(),
198 config,
199 }
200 }
201
202 pub fn record_event(&mut self, peer_id: &str, event: PrReputationEvent) {
208 let entry = self
209 .scores
210 .entry(peer_id.to_owned())
211 .or_insert_with(PrReputationScore::new);
212
213 let delta = self.config.delta_for(&event);
214 entry.score = (entry.score + delta).clamp(0.0, 1.0);
215
216 if PrReputationConfig::is_violation(&event) {
217 entry.violations = entry.violations.saturating_add(1);
218 }
219
220 entry.total_events = entry.total_events.saturating_add(1);
221 }
222
223 pub fn decay_all(&mut self) {
225 let rate = self.config.decay_rate;
226 for entry in self.scores.values_mut() {
227 if entry.score > 0.5 {
228 entry.score = (entry.score - rate).max(0.5);
229 } else if entry.score < 0.5 {
230 entry.score = (entry.score + rate).min(0.5);
231 }
232 }
233 }
234
235 pub fn score(&self, peer_id: &str) -> Option<&PrReputationScore> {
237 self.scores.get(peer_id)
238 }
239
240 pub fn trusted_peers(&self) -> Vec<&str> {
242 let mut trusted: Vec<(&str, f64)> = self
243 .scores
244 .iter()
245 .filter(|(_, s)| s.is_trusted())
246 .map(|(id, s)| (id.as_str(), s.score))
247 .collect();
248
249 trusted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
250 trusted.into_iter().map(|(id, _)| id).collect()
251 }
252
253 pub fn banned_peers(&self) -> Vec<&str> {
255 self.scores
256 .iter()
257 .filter(|(_, s)| s.is_banned())
258 .map(|(id, _)| id.as_str())
259 .collect()
260 }
261
262 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
266 self.scores.remove(peer_id).is_some()
267 }
268
269 pub fn stats(&self) -> PrReputationStats {
271 let total_peers = self.scores.len();
272 if total_peers == 0 {
273 return PrReputationStats {
274 total_peers: 0,
275 trusted_count: 0,
276 banned_count: 0,
277 avg_score: 0.0,
278 };
279 }
280
281 let mut trusted_count = 0usize;
282 let mut banned_count = 0usize;
283 let mut score_sum = 0.0f64;
284
285 for s in self.scores.values() {
286 if s.is_trusted() {
287 trusted_count += 1;
288 }
289 if s.is_banned() {
290 banned_count += 1;
291 }
292 score_sum += s.score;
293 }
294
295 PrReputationStats {
296 total_peers,
297 trusted_count,
298 banned_count,
299 avg_score: score_sum / total_peers as f64,
300 }
301 }
302}
303
304#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn default_mgr() -> PeerReputationManager {
313 PeerReputationManager::new(PrReputationConfig::default())
314 }
315
316 #[test]
318 fn test_new_manager_is_empty() {
319 let mgr = default_mgr();
320 assert!(mgr.scores.is_empty());
321 assert_eq!(mgr.stats().total_peers, 0);
322 }
323
324 #[test]
326 fn test_auto_create_peer_at_neutral_score() {
327 let mut mgr = default_mgr();
328 mgr.record_event("peer-X", PrReputationEvent::BlockDelivered { bytes: 100 });
329 let s = mgr.score("peer-X").expect("peer must exist");
330 assert!((s.score - 0.52).abs() < 1e-9);
332 }
333
334 #[test]
336 fn test_block_delivered_increases_score() {
337 let mut mgr = default_mgr();
338 mgr.record_event("p", PrReputationEvent::BlockDelivered { bytes: 512 });
339 let s = mgr
340 .score("p")
341 .expect("test: peer should exist after recording event");
342 assert!(s.score > 0.5);
343 }
344
345 #[test]
347 fn test_block_timeout_decreases_score() {
348 let mut mgr = default_mgr();
349 mgr.record_event("p", PrReputationEvent::BlockTimeout);
350 let s = mgr
351 .score("p")
352 .expect("test: peer should exist after recording event");
353 assert!(s.score < 0.5);
354 }
355
356 #[test]
358 fn test_protocol_violation_increments_violations() {
359 let mut mgr = default_mgr();
360 mgr.record_event("p", PrReputationEvent::ProtocolViolation);
361 let s = mgr
362 .score("p")
363 .expect("test: peer 'p' should exist after recording ProtocolViolation event");
364 assert_eq!(s.violations, 1);
365 }
366
367 #[test]
369 fn test_bad_proof_increments_violations() {
370 let mut mgr = default_mgr();
371 mgr.record_event("p", PrReputationEvent::BadProof);
372 let s = mgr
373 .score("p")
374 .expect("test: peer 'p' should exist after recording BadProof event");
375 assert_eq!(s.violations, 1);
376 }
377
378 #[test]
380 fn test_score_clamped_at_upper_bound() {
381 let mut mgr = default_mgr();
382 for _ in 0..100 {
383 mgr.record_event("p", PrReputationEvent::BlockDelivered { bytes: 1 });
384 }
385 let s = mgr
386 .score("p")
387 .expect("test: peer 'p' should exist after recording BlockDelivered events");
388 assert!(s.score <= 1.0);
389 }
390
391 #[test]
393 fn test_score_clamped_at_lower_bound() {
394 let mut mgr = default_mgr();
395 for _ in 0..100 {
396 mgr.record_event("p", PrReputationEvent::BadProof);
397 }
398 let s = mgr
399 .score("p")
400 .expect("test: peer 'p' should exist after recording BadProof events");
401 assert!(s.score >= 0.0);
402 }
403
404 #[test]
406 fn test_is_trusted_at_threshold() {
407 let s = PrReputationScore {
408 score: 0.7,
409 total_events: 1,
410 violations: 0,
411 };
412 assert!(s.is_trusted());
413 }
414
415 #[test]
417 fn test_is_trusted_below_threshold() {
418 let s = PrReputationScore {
419 score: 0.699,
420 total_events: 1,
421 violations: 0,
422 };
423 assert!(!s.is_trusted());
424 }
425
426 #[test]
428 fn test_is_banned_low_score() {
429 let s = PrReputationScore {
430 score: 0.05,
431 total_events: 5,
432 violations: 0,
433 };
434 assert!(s.is_banned());
435 }
436
437 #[test]
439 fn test_is_banned_high_violations() {
440 let s = PrReputationScore {
441 score: 0.5,
442 total_events: 10,
443 violations: 10,
444 };
445 assert!(s.is_banned());
446 }
447
448 #[test]
450 fn test_tier_labels() {
451 let cases = [
452 (0.05, "banned"),
453 (0.1, "poor"),
454 (0.39, "poor"),
455 (0.4, "neutral"),
456 (0.69, "neutral"),
457 (0.7, "trusted"),
458 (1.0, "trusted"),
459 ];
460 for (score, expected) in cases {
461 let s = PrReputationScore {
462 score,
463 total_events: 0,
464 violations: 0,
465 };
466 assert_eq!(s.tier(), expected, "score={score}");
467 }
468 }
469
470 #[test]
472 fn test_decay_all_moves_high_score_toward_neutral() {
473 let mut mgr = default_mgr();
474 mgr.scores.insert(
476 "p".to_owned(),
477 PrReputationScore {
478 score: 0.9,
479 total_events: 0,
480 violations: 0,
481 },
482 );
483 mgr.decay_all();
484 let s = mgr
485 .score("p")
486 .expect("test: peer 'p' should exist after decay_all");
487 assert!(s.score < 0.9, "score should have decreased toward 0.5");
488 assert!(s.score >= 0.5, "score should not go below 0.5 in one tick");
489 }
490
491 #[test]
493 fn test_decay_all_moves_low_score_toward_neutral() {
494 let mut mgr = default_mgr();
495 mgr.scores.insert(
496 "p".to_owned(),
497 PrReputationScore {
498 score: 0.2,
499 total_events: 0,
500 violations: 0,
501 },
502 );
503 mgr.decay_all();
504 let s = mgr
505 .score("p")
506 .expect("test: peer 'p' should exist after decay_all");
507 assert!(s.score > 0.2, "score should have increased toward 0.5");
508 assert!(s.score <= 0.5, "score should not exceed 0.5 in one tick");
509 }
510
511 #[test]
513 fn test_trusted_peers_sorted_desc() {
514 let mut mgr = default_mgr();
515 for (id, score) in [("a", 0.8_f64), ("b", 0.95), ("c", 0.72)] {
516 mgr.scores.insert(
517 id.to_owned(),
518 PrReputationScore {
519 score,
520 total_events: 1,
521 violations: 0,
522 },
523 );
524 }
525 let trusted = mgr.trusted_peers();
526 assert_eq!(trusted.len(), 3);
527 assert_eq!(trusted[0], "b");
529 assert_eq!(trusted[1], "a");
530 assert_eq!(trusted[2], "c");
531 }
532
533 #[test]
535 fn test_banned_peers_correct() {
536 let mut mgr = default_mgr();
537 mgr.scores.insert(
538 "good".to_owned(),
539 PrReputationScore {
540 score: 0.8,
541 total_events: 1,
542 violations: 0,
543 },
544 );
545 mgr.scores.insert(
546 "bad".to_owned(),
547 PrReputationScore {
548 score: 0.05,
549 total_events: 5,
550 violations: 0,
551 },
552 );
553 let banned = mgr.banned_peers();
554 assert_eq!(banned.len(), 1);
555 assert_eq!(banned[0], "bad");
556 }
557
558 #[test]
560 fn test_remove_peer_returns_correct_bool() {
561 let mut mgr = default_mgr();
562 mgr.record_event("p", PrReputationEvent::GoodProof);
563 assert!(mgr.remove_peer("p"));
564 assert!(!mgr.remove_peer("p")); assert!(!mgr.remove_peer("never-existed"));
566 }
567
568 #[test]
570 fn test_stats_counts_correct() {
571 let mut mgr = default_mgr();
572 mgr.scores.insert(
573 "trusted".to_owned(),
574 PrReputationScore {
575 score: 0.8,
576 total_events: 1,
577 violations: 0,
578 },
579 );
580 mgr.scores.insert(
581 "neutral".to_owned(),
582 PrReputationScore {
583 score: 0.5,
584 total_events: 1,
585 violations: 0,
586 },
587 );
588 mgr.scores.insert(
589 "banned".to_owned(),
590 PrReputationScore {
591 score: 0.05,
592 total_events: 1,
593 violations: 0,
594 },
595 );
596 let stats = mgr.stats();
597 assert_eq!(stats.total_peers, 3);
598 assert_eq!(stats.trusted_count, 1);
599 assert_eq!(stats.banned_count, 1);
600 }
601
602 #[test]
604 fn test_avg_score_computed_correctly() {
605 let mut mgr = default_mgr();
606 for (id, score) in [("a", 0.6_f64), ("b", 0.8)] {
607 mgr.scores.insert(
608 id.to_owned(),
609 PrReputationScore {
610 score,
611 total_events: 0,
612 violations: 0,
613 },
614 );
615 }
616 let stats = mgr.stats();
617 let expected = (0.6 + 0.8) / 2.0;
618 assert!((stats.avg_score - expected).abs() < 1e-9);
619 }
620
621 #[test]
623 fn test_avg_score_zero_when_empty() {
624 let mgr = default_mgr();
625 assert_eq!(mgr.stats().avg_score, 0.0);
626 }
627
628 #[test]
630 fn test_total_events_increments() {
631 let mut mgr = default_mgr();
632 mgr.record_event("p", PrReputationEvent::GoodProof);
633 mgr.record_event("p", PrReputationEvent::GoodProof);
634 mgr.record_event("p", PrReputationEvent::BlockTimeout);
635 let s = mgr
636 .score("p")
637 .expect("test: peer 'p' should exist after recording 3 events");
638 assert_eq!(s.total_events, 3);
639 }
640
641 #[test]
643 fn test_good_proof_no_violation() {
644 let mut mgr = default_mgr();
645 mgr.record_event("p", PrReputationEvent::GoodProof);
646 let s = mgr
647 .score("p")
648 .expect("test: peer 'p' should exist after GoodProof event");
649 assert_eq!(s.violations, 0);
650 }
651
652 #[test]
654 fn test_connection_dropped_no_violation() {
655 let mut mgr = default_mgr();
656 mgr.record_event("p", PrReputationEvent::ConnectionDropped);
657 let s = mgr
658 .score("p")
659 .expect("test: peer 'p' should exist after ConnectionDropped event");
660 assert_eq!(s.violations, 0);
661 }
662
663 #[test]
665 fn test_decay_leaves_neutral_unchanged() {
666 let mut mgr = default_mgr();
667 mgr.scores.insert(
668 "p".to_owned(),
669 PrReputationScore {
670 score: 0.5,
671 total_events: 0,
672 violations: 0,
673 },
674 );
675 mgr.decay_all();
676 let s = mgr
677 .score("p")
678 .expect("test: peer 'p' should exist after decay_all on neutral score");
679 assert_eq!(s.score, 0.5);
680 }
681}