1use std::collections::{HashMap, VecDeque};
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum TrustDimension {
41 Uptime,
43 ContentValidity,
45 ProtocolCompliance,
47 ResponseLatency,
49 DataAvailability,
51}
52
53impl TrustDimension {
54 pub fn as_key(&self) -> &'static str {
56 match self {
57 TrustDimension::Uptime => "Uptime",
58 TrustDimension::ContentValidity => "ContentValidity",
59 TrustDimension::ProtocolCompliance => "ProtocolCompliance",
60 TrustDimension::ResponseLatency => "ResponseLatency",
61 TrustDimension::DataAvailability => "DataAvailability",
62 }
63 }
64
65 pub fn all() -> &'static [TrustDimension] {
67 &[
68 TrustDimension::Uptime,
69 TrustDimension::ContentValidity,
70 TrustDimension::ProtocolCompliance,
71 TrustDimension::ResponseLatency,
72 TrustDimension::DataAvailability,
73 ]
74 }
75}
76
77#[derive(Debug, Clone)]
85pub struct TrustEvent {
86 pub peer_id: String,
89 pub dimension: TrustDimension,
91 pub delta: f64,
93 pub timestamp: u64,
95 pub description: String,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
105pub enum TrustBand {
106 Blocked = 0,
108 Untrusted = 1,
110 Neutral = 2,
112 Trusted = 3,
114 HighlyTrusted = 4,
116}
117
118impl TrustBand {
119 pub fn from_score(score: f64) -> TrustBand {
121 if score < 0.10 {
122 TrustBand::Blocked
123 } else if score < 0.30 {
124 TrustBand::Untrusted
125 } else if score < 0.60 {
126 TrustBand::Neutral
127 } else if score < 0.85 {
128 TrustBand::Trusted
129 } else {
130 TrustBand::HighlyTrusted
131 }
132 }
133
134 pub fn ordinal(&self) -> u8 {
136 match self {
137 TrustBand::Blocked => 0,
138 TrustBand::Untrusted => 1,
139 TrustBand::Neutral => 2,
140 TrustBand::Trusted => 3,
141 TrustBand::HighlyTrusted => 4,
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
152pub struct TrustConfig {
153 pub dimension_weights: HashMap<String, f64>,
156 pub decay_rate: f64,
158 pub min_score: f64,
160 pub max_score: f64,
162 pub events_per_page: usize,
164}
165
166impl Default for TrustConfig {
167 fn default() -> Self {
168 let mut weights = HashMap::new();
169 weights.insert(TrustDimension::Uptime.as_key().to_string(), 0.20);
170 weights.insert(TrustDimension::ContentValidity.as_key().to_string(), 0.30);
171 weights.insert(
172 TrustDimension::ProtocolCompliance.as_key().to_string(),
173 0.20,
174 );
175 weights.insert(TrustDimension::ResponseLatency.as_key().to_string(), 0.15);
176 weights.insert(TrustDimension::DataAvailability.as_key().to_string(), 0.15);
177 TrustConfig {
178 dimension_weights: weights,
179 decay_rate: 0.01,
180 min_score: 0.0,
181 max_score: 1.0,
182 events_per_page: 50,
183 }
184 }
185}
186
187impl TrustConfig {
188 pub fn is_valid(&self) -> bool {
190 let total: f64 = self.dimension_weights.values().sum();
191 (total - 1.0).abs() < 1e-9
192 && TrustDimension::all()
193 .iter()
194 .all(|d| self.dimension_weights.contains_key(d.as_key()))
195 }
196
197 pub fn weight_for(&self, dimension: &TrustDimension) -> f64 {
199 *self
200 .dimension_weights
201 .get(dimension.as_key())
202 .unwrap_or(&0.0)
203 }
204}
205
206#[derive(Debug, Clone)]
212pub struct PeerTrustProfile {
213 pub peer_id: String,
215 pub scores: HashMap<String, f64>,
217 pub composite_score: f64,
219 pub event_count: u64,
221 pub last_updated: u64,
223 pub band: TrustBand,
225}
226
227impl PeerTrustProfile {
228 fn new(peer_id: &str, now: u64) -> Self {
230 let mut scores = HashMap::new();
231 for d in TrustDimension::all() {
232 scores.insert(d.as_key().to_string(), 0.5);
233 }
234 PeerTrustProfile {
235 peer_id: peer_id.to_string(),
236 scores,
237 composite_score: 0.5,
238 event_count: 0,
239 last_updated: now,
240 band: TrustBand::Neutral,
241 }
242 }
243
244 fn recompute_composite(&mut self, config: &TrustConfig) {
247 let mut total_weight = 0.0_f64;
248 let mut weighted_sum = 0.0_f64;
249 for d in TrustDimension::all() {
250 let weight = config.weight_for(d);
251 let score = self.scores.get(d.as_key()).copied().unwrap_or(0.5);
252 weighted_sum += weight * score;
253 total_weight += weight;
254 }
255 self.composite_score = if total_weight > 0.0 {
256 weighted_sum / total_weight
257 } else {
258 0.5
259 };
260 self.band = TrustBand::from_score(self.composite_score);
261 }
262}
263
264#[derive(Debug, Clone)]
270pub struct TrustScorerStats {
271 pub total_peers: usize,
273 pub trusted_count: usize,
275 pub blocked_count: usize,
277 pub total_events: u64,
279 pub avg_composite_score: f64,
281}
282
283pub struct PeerTrustScorer {
293 pub config: TrustConfig,
295 profiles: HashMap<String, PeerTrustProfile>,
297 event_log: VecDeque<TrustEvent>,
299 max_events: usize,
301 total_events: u64,
303}
304
305impl PeerTrustScorer {
306 pub fn new(config: TrustConfig, max_events: usize) -> Self {
313 PeerTrustScorer {
314 config,
315 profiles: HashMap::new(),
316 event_log: VecDeque::with_capacity(max_events.min(4096)),
317 max_events: max_events.max(1),
318 total_events: 0,
319 }
320 }
321
322 pub fn dimension_key(dimension: &TrustDimension) -> String {
326 dimension.as_key().to_string()
327 }
328
329 pub fn get_or_create_profile(&mut self, peer_id: &str, now: u64) -> &mut PeerTrustProfile {
332 self.profiles
333 .entry(peer_id.to_string())
334 .or_insert_with(|| PeerTrustProfile::new(peer_id, now))
335 }
336
337 pub fn get_profile(&self, peer_id: &str) -> Option<&PeerTrustProfile> {
339 self.profiles.get(peer_id)
340 }
341
342 pub fn record_event(&mut self, event: TrustEvent) {
351 let peer_id = event.peer_id.clone();
352 let timestamp = event.timestamp;
353 let dimension_key = event.dimension.as_key().to_string();
354 let delta = event.delta;
355 let min = self.config.min_score;
356 let max = self.config.max_score;
357
358 let profile = self.get_or_create_profile(&peer_id, timestamp);
360 let current = *profile.scores.get(&dimension_key).unwrap_or(&0.5);
361 let updated = (current + delta).clamp(min, max);
362 profile.scores.insert(dimension_key, updated);
363 profile.event_count += 1;
364 profile.last_updated = timestamp;
365
366 let config = self.config.clone();
368 let profile = self.profiles.get_mut(&peer_id).unwrap_or_else(|| {
369 panic!("profile vanished unexpectedly")
372 });
373 profile.recompute_composite(&config);
374
375 if self.event_log.len() >= self.max_events {
377 self.event_log.pop_front();
378 }
379 self.event_log.push_back(event);
380 self.total_events += 1;
381 }
382
383 pub fn apply_decay(&mut self, now: u64) {
397 let decay_rate = self.config.decay_rate;
398 let min = self.config.min_score;
399 let max = self.config.max_score;
400 let config = self.config.clone();
401
402 for profile in self.profiles.values_mut() {
403 let elapsed_ms = now.saturating_sub(profile.last_updated);
404 let elapsed_hours = elapsed_ms as f64 / 3_600_000.0;
405 let factor = (1.0 - decay_rate * elapsed_hours).max(0.0);
406
407 for score in profile.scores.values_mut() {
408 *score = (*score * factor).clamp(min, max);
409 }
410 profile.recompute_composite(&config);
411 profile.last_updated = now;
412 }
413 }
414
415 pub fn trusted_peers(&self, min_band: TrustBand) -> Vec<&PeerTrustProfile> {
420 let mut result: Vec<&PeerTrustProfile> = self
421 .profiles
422 .values()
423 .filter(|p| p.band >= min_band)
424 .collect();
425 result.sort_by(|a, b| {
426 b.composite_score
427 .partial_cmp(&a.composite_score)
428 .unwrap_or(std::cmp::Ordering::Equal)
429 });
430 result
431 }
432
433 pub fn blocked_peers(&self) -> Vec<&str> {
435 self.profiles
436 .values()
437 .filter(|p| p.band == TrustBand::Blocked)
438 .map(|p| p.peer_id.as_str())
439 .collect()
440 }
441
442 pub fn peer_events(&self, peer_id: &str, page: usize) -> Vec<&TrustEvent> {
447 let per_page = self.config.events_per_page;
448 let skip = page * per_page;
449 self.event_log
450 .iter()
451 .filter(|e| e.peer_id == peer_id)
452 .skip(skip)
453 .take(per_page)
454 .collect()
455 }
456
457 pub fn ban_peer(&mut self, peer_id: &str, now: u64) {
462 let config = self.config.clone();
463 let profile = self.get_or_create_profile(peer_id, now);
464 for score in profile.scores.values_mut() {
465 *score = 0.0;
466 }
467 profile.composite_score = 0.0;
468 profile.band = TrustBand::Blocked;
469 profile.last_updated = now;
470 let profile = self
472 .profiles
473 .get_mut(peer_id)
474 .expect("profile was just created");
475 profile.recompute_composite(&config);
476 profile.band = TrustBand::Blocked;
478 }
479
480 pub fn rehabilitate_peer(&mut self, peer_id: &str, now: u64) {
483 let config = self.config.clone();
484 let profile = self.get_or_create_profile(peer_id, now);
485 for score in profile.scores.values_mut() {
486 *score = 0.3;
487 }
488 profile.last_updated = now;
489 let profile = self
490 .profiles
491 .get_mut(peer_id)
492 .expect("profile was just created");
493 profile.recompute_composite(&config);
494 }
495
496 pub fn scorer_stats(&self) -> TrustScorerStats {
500 let total_peers = self.profiles.len();
501 let mut trusted_count = 0_usize;
502 let mut blocked_count = 0_usize;
503 let mut score_sum = 0.0_f64;
504
505 for profile in self.profiles.values() {
506 if profile.band >= TrustBand::Trusted {
507 trusted_count += 1;
508 }
509 if profile.band == TrustBand::Blocked {
510 blocked_count += 1;
511 }
512 score_sum += profile.composite_score;
513 }
514
515 let avg_composite_score = if total_peers > 0 {
516 score_sum / total_peers as f64
517 } else {
518 0.5
519 };
520
521 TrustScorerStats {
522 total_peers,
523 trusted_count,
524 blocked_count,
525 total_events: self.total_events,
526 avg_composite_score,
527 }
528 }
529
530 pub fn peer_count(&self) -> usize {
534 self.profiles.len()
535 }
536
537 pub fn event_log_len(&self) -> usize {
539 self.event_log.len()
540 }
541
542 pub fn total_events(&self) -> u64 {
544 self.total_events
545 }
546}
547
548#[cfg(test)]
553mod tests {
554 use crate::peer_trust_scorer::{
555 PeerTrustScorer, TrustBand, TrustConfig, TrustDimension, TrustEvent, TrustScorerStats,
556 };
557 use std::collections::HashMap;
558
559 fn default_scorer() -> PeerTrustScorer {
562 PeerTrustScorer::new(TrustConfig::default(), 200)
563 }
564
565 fn make_event(peer: &str, dim: TrustDimension, delta: f64, ts: u64) -> TrustEvent {
566 TrustEvent {
567 peer_id: peer.to_string(),
568 dimension: dim,
569 delta,
570 timestamp: ts,
571 description: "test event".to_string(),
572 }
573 }
574
575 #[test]
578 fn band_from_score_blocked() {
579 assert_eq!(TrustBand::from_score(0.0), TrustBand::Blocked);
580 assert_eq!(TrustBand::from_score(0.09), TrustBand::Blocked);
581 }
582
583 #[test]
584 fn band_from_score_untrusted() {
585 assert_eq!(TrustBand::from_score(0.10), TrustBand::Untrusted);
586 assert_eq!(TrustBand::from_score(0.29), TrustBand::Untrusted);
587 }
588
589 #[test]
590 fn band_from_score_neutral() {
591 assert_eq!(TrustBand::from_score(0.30), TrustBand::Neutral);
592 assert_eq!(TrustBand::from_score(0.59), TrustBand::Neutral);
593 }
594
595 #[test]
596 fn band_from_score_trusted() {
597 assert_eq!(TrustBand::from_score(0.60), TrustBand::Trusted);
598 assert_eq!(TrustBand::from_score(0.84), TrustBand::Trusted);
599 }
600
601 #[test]
602 fn band_from_score_highly_trusted() {
603 assert_eq!(TrustBand::from_score(0.85), TrustBand::HighlyTrusted);
604 assert_eq!(TrustBand::from_score(1.0), TrustBand::HighlyTrusted);
605 }
606
607 #[test]
608 fn band_ordinal_ordering() {
609 assert!(TrustBand::Blocked < TrustBand::Untrusted);
610 assert!(TrustBand::Untrusted < TrustBand::Neutral);
611 assert!(TrustBand::Neutral < TrustBand::Trusted);
612 assert!(TrustBand::Trusted < TrustBand::HighlyTrusted);
613 }
614
615 #[test]
616 fn band_ordinal_values() {
617 assert_eq!(TrustBand::Blocked.ordinal(), 0);
618 assert_eq!(TrustBand::Untrusted.ordinal(), 1);
619 assert_eq!(TrustBand::Neutral.ordinal(), 2);
620 assert_eq!(TrustBand::Trusted.ordinal(), 3);
621 assert_eq!(TrustBand::HighlyTrusted.ordinal(), 4);
622 }
623
624 #[test]
627 fn default_config_is_valid() {
628 assert!(TrustConfig::default().is_valid());
629 }
630
631 #[test]
632 fn default_config_weights_sum_to_one() {
633 let total: f64 = TrustConfig::default().dimension_weights.values().sum();
634 assert!((total - 1.0).abs() < 1e-9);
635 }
636
637 #[test]
638 fn config_weight_for_returns_correct_values() {
639 let cfg = TrustConfig::default();
640 assert!((cfg.weight_for(&TrustDimension::ContentValidity) - 0.30).abs() < 1e-9);
641 assert!((cfg.weight_for(&TrustDimension::Uptime) - 0.20).abs() < 1e-9);
642 }
643
644 #[test]
645 fn config_weight_for_missing_dimension_returns_zero() {
646 let cfg = TrustConfig {
647 dimension_weights: HashMap::new(),
648 ..TrustConfig::default()
649 };
650 assert_eq!(cfg.weight_for(&TrustDimension::Uptime), 0.0);
651 }
652
653 #[test]
656 fn new_profile_initialised_to_neutral() {
657 let mut scorer = default_scorer();
658 let profile = scorer.get_or_create_profile("peer-init", 0);
659 assert_eq!(profile.band, TrustBand::Neutral);
661 assert!((profile.composite_score - 0.5).abs() < 1e-9);
662 }
663
664 #[test]
665 fn new_profile_all_dimensions_present() {
666 let mut scorer = default_scorer();
667 let profile = scorer.get_or_create_profile("peer-dims", 0);
668 for d in TrustDimension::all() {
669 assert!(
670 profile.scores.contains_key(d.as_key()),
671 "missing dimension {:?}",
672 d
673 );
674 }
675 }
676
677 #[test]
678 fn get_profile_returns_none_for_unknown_peer() {
679 let scorer = default_scorer();
680 assert!(scorer.get_profile("ghost").is_none());
681 }
682
683 #[test]
686 fn record_positive_event_increases_score() {
687 let mut scorer = default_scorer();
688 let before = scorer
689 .get_or_create_profile("peer-pos", 0)
690 .scores
691 .get(TrustDimension::ContentValidity.as_key())
692 .copied()
693 .unwrap_or(0.5);
694 scorer.record_event(make_event(
695 "peer-pos",
696 TrustDimension::ContentValidity,
697 0.1,
698 1,
699 ));
700 let after = scorer
701 .get_profile("peer-pos")
702 .expect("profile")
703 .scores
704 .get(TrustDimension::ContentValidity.as_key())
705 .copied()
706 .unwrap_or(0.0);
707 assert!(after > before);
708 }
709
710 #[test]
711 fn record_negative_event_decreases_score() {
712 let mut scorer = default_scorer();
713 scorer.record_event(make_event(
714 "peer-neg",
715 TrustDimension::ProtocolCompliance,
716 -0.2,
717 1,
718 ));
719 let score = scorer
720 .get_profile("peer-neg")
721 .expect("profile")
722 .scores
723 .get(TrustDimension::ProtocolCompliance.as_key())
724 .copied()
725 .unwrap_or(1.0);
726 assert!(score < 0.5);
727 }
728
729 #[test]
730 fn score_clamped_at_max() {
731 let mut scorer = default_scorer();
732 for _ in 0..20 {
733 scorer.record_event(make_event("peer-max", TrustDimension::Uptime, 0.3, 1));
734 }
735 let score = scorer
736 .get_profile("peer-max")
737 .expect("profile")
738 .scores
739 .get(TrustDimension::Uptime.as_key())
740 .copied()
741 .unwrap_or(0.0);
742 assert!(score <= 1.0);
743 }
744
745 #[test]
746 fn score_clamped_at_min() {
747 let mut scorer = default_scorer();
748 for _ in 0..20 {
749 scorer.record_event(make_event(
750 "peer-min",
751 TrustDimension::ResponseLatency,
752 -0.3,
753 1,
754 ));
755 }
756 let score = scorer
757 .get_profile("peer-min")
758 .expect("profile")
759 .scores
760 .get(TrustDimension::ResponseLatency.as_key())
761 .copied()
762 .unwrap_or(1.0);
763 assert!(score >= 0.0);
764 }
765
766 #[test]
767 fn event_count_increments_per_event() {
768 let mut scorer = default_scorer();
769 for i in 0..5_u64 {
770 scorer.record_event(make_event(
771 "peer-cnt",
772 TrustDimension::DataAvailability,
773 0.01,
774 i,
775 ));
776 }
777 assert_eq!(
778 scorer.get_profile("peer-cnt").expect("profile").event_count,
779 5
780 );
781 }
782
783 #[test]
784 fn total_events_counts_across_peers() {
785 let mut scorer = default_scorer();
786 scorer.record_event(make_event("A", TrustDimension::Uptime, 0.1, 0));
787 scorer.record_event(make_event("B", TrustDimension::Uptime, 0.1, 0));
788 scorer.record_event(make_event("A", TrustDimension::Uptime, 0.1, 0));
789 assert_eq!(scorer.total_events(), 3);
790 }
791
792 #[test]
793 fn event_log_evicts_oldest_when_full() {
794 let mut scorer = PeerTrustScorer::new(TrustConfig::default(), 3);
795 for i in 0..5_u64 {
796 scorer.record_event(make_event("peer-evict", TrustDimension::Uptime, 0.01, i));
797 }
798 assert_eq!(scorer.event_log_len(), 3);
800 }
801
802 #[test]
803 fn composite_score_is_weighted_average() {
804 let mut scorer = default_scorer();
806 let peer = "peer-weighted";
808 scorer.record_event(make_event(peer, TrustDimension::ContentValidity, 0.1, 0));
810 let profile = scorer.get_profile(peer).expect("profile");
811 assert!(profile.composite_score > 0.5);
813 assert!(profile.composite_score <= 0.6);
814 }
815
816 #[test]
817 fn band_updates_after_positive_events() {
818 let mut scorer = default_scorer();
819 let peer = "peer-band-up";
820 for d in TrustDimension::all() {
822 for _ in 0..4 {
823 scorer.record_event(make_event(peer, d.clone(), 0.1, 1));
824 }
825 }
826 let profile = scorer.get_profile(peer).expect("profile");
827 assert!(profile.band >= TrustBand::Trusted);
828 }
829
830 #[test]
831 fn band_updates_after_negative_events() {
832 let mut scorer = default_scorer();
833 let peer = "peer-band-down";
834 for d in TrustDimension::all() {
835 for _ in 0..6 {
836 scorer.record_event(make_event(peer, d.clone(), -0.1, 1));
837 }
838 }
839 let profile = scorer.get_profile(peer).expect("profile");
840 assert!(profile.band <= TrustBand::Untrusted);
841 }
842
843 #[test]
846 fn apply_decay_reduces_scores() {
847 let mut scorer = default_scorer();
848 scorer.record_event(make_event("peer-decay", TrustDimension::Uptime, 0.2, 0));
849 let before = scorer
850 .get_profile("peer-decay")
851 .expect("profile")
852 .scores
853 .get(TrustDimension::Uptime.as_key())
854 .copied()
855 .unwrap_or(0.5);
856 scorer.apply_decay(3_600_000);
858 let after = scorer
859 .get_profile("peer-decay")
860 .expect("profile")
861 .scores
862 .get(TrustDimension::Uptime.as_key())
863 .copied()
864 .unwrap_or(1.0);
865 assert!(after < before);
866 }
867
868 #[test]
869 fn apply_decay_updates_last_updated() {
870 let mut scorer = default_scorer();
871 scorer.record_event(make_event("peer-decay2", TrustDimension::Uptime, 0.0, 100));
872 scorer.apply_decay(9_000_000);
873 assert_eq!(
874 scorer
875 .get_profile("peer-decay2")
876 .expect("profile")
877 .last_updated,
878 9_000_000
879 );
880 }
881
882 #[test]
883 fn apply_decay_with_zero_elapsed_is_identity() {
884 let mut scorer = default_scorer();
885 let ts = 5_000;
886 scorer.record_event(make_event("peer-nodecay", TrustDimension::Uptime, 0.1, ts));
887 let before = scorer
888 .get_profile("peer-nodecay")
889 .expect("profile")
890 .composite_score;
891 scorer.apply_decay(ts); let after = scorer
893 .get_profile("peer-nodecay")
894 .expect("profile")
895 .composite_score;
896 assert!((before - after).abs() < 1e-12);
897 }
898
899 #[test]
900 fn apply_decay_does_not_go_below_min() {
901 let cfg = TrustConfig {
902 decay_rate: 100.0, ..TrustConfig::default()
904 };
905 let mut scorer = PeerTrustScorer::new(cfg, 100);
906 scorer.record_event(make_event("peer-floor", TrustDimension::Uptime, 0.0, 0));
907 scorer.apply_decay(3_600_000);
908 let score = scorer
909 .get_profile("peer-floor")
910 .expect("profile")
911 .scores
912 .get(TrustDimension::Uptime.as_key())
913 .copied()
914 .unwrap_or(1.0);
915 assert!(score >= 0.0);
916 }
917
918 #[test]
921 fn trusted_peers_filters_by_min_band() {
922 let mut scorer = default_scorer();
923 scorer.ban_peer("peer-blocked", 0);
925 scorer.get_or_create_profile("peer-neutral", 0);
927 let trusted = scorer.trusted_peers(TrustBand::Neutral);
929 let ids: Vec<&str> = trusted.iter().map(|p| p.peer_id.as_str()).collect();
930 assert!(!ids.contains(&"peer-blocked"));
931 assert!(ids.contains(&"peer-neutral"));
932 }
933
934 #[test]
935 fn trusted_peers_sorted_descending_by_composite() {
936 let mut scorer = default_scorer();
937 scorer.record_event(make_event("peer-high", TrustDimension::Uptime, 0.3, 0));
938 scorer.record_event(make_event("peer-low", TrustDimension::Uptime, -0.1, 0));
939 let peers = scorer.trusted_peers(TrustBand::Blocked); if peers.len() >= 2 {
941 let first = peers[0].composite_score;
942 let last = peers[peers.len() - 1].composite_score;
943 assert!(first >= last);
944 }
945 }
946
947 #[test]
950 fn blocked_peers_returns_only_blocked() {
951 let mut scorer = default_scorer();
952 scorer.ban_peer("peer-b1", 0);
953 scorer.ban_peer("peer-b2", 0);
954 scorer.get_or_create_profile("peer-ok", 0);
955 let blocked = scorer.blocked_peers();
956 assert!(blocked.contains(&"peer-b1"));
957 assert!(blocked.contains(&"peer-b2"));
958 assert!(!blocked.contains(&"peer-ok"));
959 }
960
961 #[test]
962 fn blocked_peers_empty_when_none_banned() {
963 let scorer = default_scorer();
964 assert!(scorer.blocked_peers().is_empty());
965 }
966
967 #[test]
970 fn peer_events_page_zero_returns_first_n() {
971 let mut scorer = PeerTrustScorer::new(TrustConfig::default(), 1000);
972 for i in 0..10_u64 {
973 scorer.record_event(make_event("peer-pg", TrustDimension::Uptime, 0.01, i));
974 }
975 let page = scorer.peer_events("peer-pg", 0);
976 assert_eq!(page.len(), 10);
978 }
979
980 #[test]
981 fn peer_events_page_one_skips_first_page() {
982 let cfg = TrustConfig {
983 events_per_page: 3,
984 ..TrustConfig::default()
985 };
986 let mut scorer = PeerTrustScorer::new(cfg, 1000);
987 for i in 0..7_u64 {
988 scorer.record_event(make_event("peer-pages", TrustDimension::Uptime, 0.0, i));
989 }
990 let page1 = scorer.peer_events("peer-pages", 1);
991 assert_eq!(page1.len(), 3);
993 }
994
995 #[test]
996 fn peer_events_filters_by_peer_id() {
997 let mut scorer = default_scorer();
998 scorer.record_event(make_event("A", TrustDimension::Uptime, 0.01, 0));
999 scorer.record_event(make_event("B", TrustDimension::Uptime, 0.01, 1));
1000 scorer.record_event(make_event("A", TrustDimension::Uptime, 0.01, 2));
1001 let events = scorer.peer_events("A", 0);
1002 assert!(events.iter().all(|e| e.peer_id == "A"));
1003 assert_eq!(events.len(), 2);
1004 }
1005
1006 #[test]
1007 fn peer_events_empty_page_beyond_end() {
1008 let mut scorer = default_scorer();
1009 scorer.record_event(make_event("peer-eof", TrustDimension::Uptime, 0.0, 0));
1010 let page99 = scorer.peer_events("peer-eof", 99);
1011 assert!(page99.is_empty());
1012 }
1013
1014 #[test]
1017 fn ban_peer_sets_band_to_blocked() {
1018 let mut scorer = default_scorer();
1019 scorer.ban_peer("peer-ban", 0);
1020 assert_eq!(
1021 scorer.get_profile("peer-ban").expect("profile").band,
1022 TrustBand::Blocked
1023 );
1024 }
1025
1026 #[test]
1027 fn ban_peer_sets_all_scores_to_zero() {
1028 let mut scorer = default_scorer();
1029 scorer.ban_peer("peer-zeroed", 0);
1030 let profile = scorer.get_profile("peer-zeroed").expect("profile");
1031 for score in profile.scores.values() {
1032 assert!(*score <= 0.0);
1033 }
1034 }
1035
1036 #[test]
1037 fn ban_creates_profile_if_absent() {
1038 let mut scorer = default_scorer();
1039 scorer.ban_peer("brand-new", 42);
1040 assert!(scorer.get_profile("brand-new").is_some());
1041 }
1042
1043 #[test]
1044 fn rehabilitate_peer_resets_scores_to_0_3() {
1045 let mut scorer = default_scorer();
1046 scorer.ban_peer("peer-rehab", 0);
1047 scorer.rehabilitate_peer("peer-rehab", 100);
1048 let profile = scorer.get_profile("peer-rehab").expect("profile");
1049 for score in profile.scores.values() {
1050 assert!((*score - 0.3).abs() < 1e-12);
1051 }
1052 }
1053
1054 #[test]
1055 fn rehabilitate_updates_band_above_blocked() {
1056 let mut scorer = default_scorer();
1057 scorer.ban_peer("peer-reh2", 0);
1058 scorer.rehabilitate_peer("peer-reh2", 100);
1059 let band = &scorer.get_profile("peer-reh2").expect("profile").band;
1060 assert!(*band > TrustBand::Blocked);
1061 }
1062
1063 #[test]
1064 fn rehabilitate_creates_profile_if_absent() {
1065 let mut scorer = default_scorer();
1066 scorer.rehabilitate_peer("ghost-rehab", 0);
1067 assert!(scorer.get_profile("ghost-rehab").is_some());
1068 }
1069
1070 #[test]
1073 fn scorer_stats_no_peers() {
1074 let scorer = default_scorer();
1075 let stats = scorer.scorer_stats();
1076 assert_eq!(stats.total_peers, 0);
1077 assert_eq!(stats.total_events, 0);
1078 assert!((stats.avg_composite_score - 0.5).abs() < 1e-9);
1079 }
1080
1081 #[test]
1082 fn scorer_stats_counts_blocked_correctly() {
1083 let mut scorer = default_scorer();
1084 scorer.ban_peer("b1", 0);
1085 scorer.ban_peer("b2", 0);
1086 let stats = scorer.scorer_stats();
1087 assert_eq!(stats.blocked_count, 2);
1088 }
1089
1090 #[test]
1091 fn scorer_stats_counts_trusted_correctly() {
1092 let mut scorer = default_scorer();
1093 for peer in &["t1", "t2"] {
1095 for d in TrustDimension::all() {
1096 for _ in 0..3 {
1097 scorer.record_event(make_event(peer, d.clone(), 0.1, 0));
1098 }
1099 }
1100 }
1101 let stats = scorer.scorer_stats();
1102 assert!(stats.trusted_count >= 2);
1103 }
1104
1105 #[test]
1106 fn scorer_stats_total_events_matches_recorded() {
1107 let mut scorer = default_scorer();
1108 let n = 7_u64;
1109 for i in 0..n {
1110 scorer.record_event(make_event("ev-peer", TrustDimension::Uptime, 0.0, i));
1111 }
1112 assert_eq!(scorer.scorer_stats().total_events, n);
1113 }
1114
1115 #[test]
1116 fn scorer_stats_avg_composite_is_mean() {
1117 let mut scorer = default_scorer();
1118 scorer.get_or_create_profile("p-avg1", 0);
1120 scorer.record_event(make_event("p-avg2", TrustDimension::Uptime, 0.2, 0));
1121 let stats = scorer.scorer_stats();
1122 assert!(stats.avg_composite_score > 0.5); }
1124
1125 #[test]
1128 fn dimension_key_returns_stable_strings() {
1129 assert_eq!(
1130 PeerTrustScorer::dimension_key(&TrustDimension::Uptime),
1131 "Uptime"
1132 );
1133 assert_eq!(
1134 PeerTrustScorer::dimension_key(&TrustDimension::ContentValidity),
1135 "ContentValidity"
1136 );
1137 assert_eq!(
1138 PeerTrustScorer::dimension_key(&TrustDimension::ProtocolCompliance),
1139 "ProtocolCompliance"
1140 );
1141 assert_eq!(
1142 PeerTrustScorer::dimension_key(&TrustDimension::ResponseLatency),
1143 "ResponseLatency"
1144 );
1145 assert_eq!(
1146 PeerTrustScorer::dimension_key(&TrustDimension::DataAvailability),
1147 "DataAvailability"
1148 );
1149 }
1150
1151 #[test]
1154 fn events_for_different_peers_do_not_interfere() {
1155 let mut scorer = default_scorer();
1156 scorer.record_event(make_event("peer-iso-A", TrustDimension::Uptime, 0.3, 0));
1157 scorer.record_event(make_event("peer-iso-B", TrustDimension::Uptime, -0.3, 0));
1158 let a = scorer
1159 .get_profile("peer-iso-A")
1160 .expect("profile A")
1161 .composite_score;
1162 let b = scorer
1163 .get_profile("peer-iso-B")
1164 .expect("profile B")
1165 .composite_score;
1166 assert!(a > b);
1167 }
1168
1169 #[test]
1170 fn peer_count_tracks_distinct_peers() {
1171 let mut scorer = default_scorer();
1172 for _ in 0..5 {
1175 scorer.record_event(make_event("P1", TrustDimension::Uptime, 0.0, 0));
1176 }
1177 scorer.record_event(make_event("P2", TrustDimension::Uptime, 0.0, 0));
1178 scorer.record_event(make_event("P3", TrustDimension::Uptime, 0.0, 0));
1179 assert_eq!(scorer.peer_count(), 3);
1180 }
1181
1182 #[test]
1185 fn max_events_of_one_retains_only_latest() {
1186 let mut scorer = PeerTrustScorer::new(TrustConfig::default(), 1);
1187 scorer.record_event(make_event("edge", TrustDimension::Uptime, 0.01, 0));
1188 scorer.record_event(make_event("edge", TrustDimension::Uptime, 0.01, 1));
1189 assert_eq!(scorer.event_log_len(), 1);
1190 }
1191
1192 #[test]
1193 fn rehabilitate_then_ban_leaves_peer_blocked() {
1194 let mut scorer = default_scorer();
1195 scorer.rehabilitate_peer("cycle", 0);
1196 scorer.ban_peer("cycle", 100);
1197 assert_eq!(
1198 scorer.get_profile("cycle").expect("profile").band,
1199 TrustBand::Blocked
1200 );
1201 }
1202
1203 #[test]
1204 fn trusted_peers_with_highly_trusted_min_band() {
1205 let mut scorer = default_scorer();
1206 for d in TrustDimension::all() {
1208 for _ in 0..5 {
1209 scorer.record_event(make_event("elite", d.clone(), 0.1, 0));
1210 }
1211 }
1212 scorer.get_or_create_profile("average", 0); let ht = scorer.trusted_peers(TrustBand::HighlyTrusted);
1214 assert!(ht.iter().all(|p| p.band == TrustBand::HighlyTrusted));
1215 }
1216
1217 #[test]
1218 fn profile_implements_debug() {
1219 let mut scorer = default_scorer();
1220 let profile = scorer.get_or_create_profile("debug-peer", 0);
1221 let s = format!("{:?}", profile);
1222 assert!(s.contains("PeerTrustProfile"));
1223 }
1224
1225 #[test]
1226 fn trust_scorer_stats_implements_debug() {
1227 let stats = TrustScorerStats {
1228 total_peers: 0,
1229 trusted_count: 0,
1230 blocked_count: 0,
1231 total_events: 0,
1232 avg_composite_score: 0.5,
1233 };
1234 let s = format!("{:?}", stats);
1235 assert!(s.contains("TrustScorerStats"));
1236 }
1237
1238 #[test]
1239 fn trust_event_implements_clone() {
1240 let ev = make_event("clone-peer", TrustDimension::Uptime, 0.1, 42);
1241 let ev2 = ev.clone();
1242 assert_eq!(ev2.peer_id, ev.peer_id);
1243 assert_eq!(ev2.delta, ev.delta);
1244 }
1245
1246 #[test]
1247 fn trust_config_implements_clone() {
1248 let cfg = TrustConfig::default();
1249 let cfg2 = cfg.clone();
1250 assert!((cfg2.decay_rate - cfg.decay_rate).abs() < 1e-12);
1251 }
1252
1253 #[test]
1254 fn trust_dimension_all_has_five_variants() {
1255 assert_eq!(TrustDimension::all().len(), 5);
1256 }
1257}