Skip to main content

ipfrs_network/
reputation.rs

1//! Peer reputation system for tracking and scoring peer behavior
2//!
3//! This module provides a comprehensive reputation system that tracks peer behavior
4//! over time and calculates reputation scores based on various factors including:
5//! - Transfer success/failure rates
6//! - Response times and latency
7//! - Protocol violations
8//! - Uptime and reliability
9//! - Historical behavior patterns
10//!
11//! # Examples
12//!
13//! ```
14//! use ipfrs_network::reputation::{ReputationManager, ReputationConfig};
15//! use libp2p::PeerId;
16//!
17//! # #[tokio::main]
18//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! let config = ReputationConfig::default();
20//! let mut manager = ReputationManager::new(config.clone());
21//!
22//! // Track successful interaction
23//! let peer_id = PeerId::random();
24//! manager.record_successful_transfer(&peer_id, 1024);
25//! manager.record_low_latency(&peer_id, 50);
26//!
27//! // Get reputation score
28//! if let Some(score) = manager.get_reputation(&peer_id) {
29//!     println!("Peer reputation: {:.2}", score.overall_score(&config));
30//! }
31//!
32//! // Check if peer is trusted
33//! assert!(manager.is_trusted(&peer_id));
34//! # Ok(())
35//! # }
36//! ```
37
38use dashmap::DashMap;
39use libp2p::PeerId;
40use serde::{Deserialize, Serialize};
41use std::time::{Duration, Instant};
42
43/// Configuration for the reputation system
44#[derive(Debug, Clone)]
45pub struct ReputationConfig {
46    /// Minimum score to be considered trusted (0.0-1.0)
47    pub trust_threshold: f64,
48
49    /// Score below which a peer is considered bad (0.0-1.0)
50    pub bad_peer_threshold: f64,
51
52    /// Weight for transfer success rate (0.0-1.0)
53    pub transfer_success_weight: f64,
54
55    /// Weight for latency score (0.0-1.0)
56    pub latency_weight: f64,
57
58    /// Weight for protocol compliance (0.0-1.0)
59    pub protocol_compliance_weight: f64,
60
61    /// Weight for uptime score (0.0-1.0)
62    pub uptime_weight: f64,
63
64    /// Maximum number of peers to track
65    pub max_tracked_peers: usize,
66
67    /// How long to remember peer reputation after last interaction
68    pub retention_period: Duration,
69
70    /// Decay factor for old scores (0.0-1.0, higher = faster decay)
71    pub score_decay_rate: f64,
72
73    /// Exponential moving average alpha for score updates (0.0-1.0)
74    pub ema_alpha: f64,
75}
76
77impl Default for ReputationConfig {
78    fn default() -> Self {
79        Self {
80            trust_threshold: 0.7,
81            bad_peer_threshold: 0.3,
82            transfer_success_weight: 0.4,
83            latency_weight: 0.2,
84            protocol_compliance_weight: 0.2,
85            uptime_weight: 0.2,
86            max_tracked_peers: 10000,
87            retention_period: Duration::from_secs(24 * 3600), // 24 hours
88            score_decay_rate: 0.1,
89            ema_alpha: 0.3,
90        }
91    }
92}
93
94impl ReputationConfig {
95    /// Configuration for strict reputation requirements
96    pub fn strict() -> Self {
97        Self {
98            trust_threshold: 0.85,
99            bad_peer_threshold: 0.4,
100            transfer_success_weight: 0.5,
101            latency_weight: 0.2,
102            protocol_compliance_weight: 0.2,
103            uptime_weight: 0.1,
104            ema_alpha: 0.4,
105            ..Default::default()
106        }
107    }
108
109    /// Configuration for lenient reputation requirements
110    pub fn lenient() -> Self {
111        Self {
112            trust_threshold: 0.5,
113            bad_peer_threshold: 0.2,
114            transfer_success_weight: 0.3,
115            latency_weight: 0.2,
116            protocol_compliance_weight: 0.1,
117            uptime_weight: 0.4,
118            ema_alpha: 0.2,
119            ..Default::default()
120        }
121    }
122
123    /// Configuration optimized for performance-critical applications
124    pub fn performance_focused() -> Self {
125        Self {
126            trust_threshold: 0.75,
127            bad_peer_threshold: 0.35,
128            transfer_success_weight: 0.3,
129            latency_weight: 0.5,
130            protocol_compliance_weight: 0.1,
131            uptime_weight: 0.1,
132            ema_alpha: 0.35,
133            ..Default::default()
134        }
135    }
136}
137
138/// Reputation score for a peer
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ReputationScore {
141    /// Transfer success rate (0.0-1.0)
142    pub transfer_success_rate: f64,
143
144    /// Latency score (0.0-1.0, higher is better)
145    pub latency_score: f64,
146
147    /// Protocol compliance score (0.0-1.0)
148    pub protocol_compliance_score: f64,
149
150    /// Uptime score (0.0-1.0)
151    pub uptime_score: f64,
152
153    /// Number of successful transfers
154    pub successful_transfers: u64,
155
156    /// Number of failed transfers
157    pub failed_transfers: u64,
158
159    /// Number of protocol violations
160    pub protocol_violations: u64,
161
162    /// Average latency in milliseconds
163    pub average_latency_ms: u64,
164
165    /// Total uptime duration (in seconds, serializable)
166    #[serde(
167        serialize_with = "serialize_duration",
168        deserialize_with = "deserialize_duration"
169    )]
170    pub total_uptime: Duration,
171
172    /// Last seen timestamp (skipped in serialization)
173    #[serde(skip, default = "Instant::now")]
174    pub last_seen: Instant,
175
176    /// First seen timestamp (skipped in serialization)
177    #[serde(skip, default = "Instant::now")]
178    pub first_seen: Instant,
179}
180
181fn serialize_duration<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
182where
183    S: serde::Serializer,
184{
185    serializer.serialize_u64(duration.as_secs())
186}
187
188fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
189where
190    D: serde::Deserializer<'de>,
191{
192    let secs = u64::deserialize(deserializer)?;
193    Ok(Duration::from_secs(secs))
194}
195
196impl Default for ReputationScore {
197    fn default() -> Self {
198        let now = Instant::now();
199        Self {
200            transfer_success_rate: 1.0, // Start with perfect score
201            latency_score: 1.0,
202            protocol_compliance_score: 1.0,
203            uptime_score: 1.0,
204            successful_transfers: 0,
205            failed_transfers: 0,
206            protocol_violations: 0,
207            average_latency_ms: 0,
208            total_uptime: Duration::from_secs(0),
209            last_seen: now,
210            first_seen: now,
211        }
212    }
213}
214
215impl ReputationScore {
216    /// Calculate overall reputation score using weighted average
217    pub fn overall_score(&self, config: &ReputationConfig) -> f64 {
218        let transfer_score = self.transfer_success_rate * config.transfer_success_weight;
219        let latency_score_weighted = self.latency_score * config.latency_weight;
220        let protocol_score = self.protocol_compliance_score * config.protocol_compliance_weight;
221        let uptime_score_weighted = self.uptime_score * config.uptime_weight;
222
223        transfer_score + latency_score_weighted + protocol_score + uptime_score_weighted
224    }
225
226    /// Update transfer success rate with exponential moving average
227    fn update_transfer_success_rate(&mut self, success: bool, alpha: f64) {
228        let new_value = if success { 1.0 } else { 0.0 };
229        self.transfer_success_rate = alpha * new_value + (1.0 - alpha) * self.transfer_success_rate;
230
231        if success {
232            self.successful_transfers = self.successful_transfers.saturating_add(1);
233        } else {
234            self.failed_transfers = self.failed_transfers.saturating_add(1);
235        }
236    }
237
238    /// Update latency score based on new latency measurement
239    fn update_latency_score(&mut self, latency_ms: u64, alpha: f64) {
240        // Calculate EMA of latency
241        let current_avg = self.average_latency_ms as f64;
242        let new_avg = alpha * (latency_ms as f64) + (1.0 - alpha) * current_avg;
243        self.average_latency_ms = new_avg as u64;
244
245        // Convert latency to score (lower is better)
246        // Score approaches 0 as latency approaches 1000ms, score = 1 at 0ms
247        let normalized_latency = (latency_ms as f64).min(1000.0) / 1000.0;
248        self.latency_score = 1.0 - normalized_latency;
249    }
250
251    /// Record a protocol violation
252    fn record_protocol_violation(&mut self, alpha: f64) {
253        self.protocol_violations = self.protocol_violations.saturating_add(1);
254
255        // Decrease protocol compliance score
256        self.protocol_compliance_score *= 1.0 - alpha;
257    }
258
259    /// Update uptime tracking
260    fn update_uptime(&mut self, connected_duration: Duration) {
261        self.total_uptime += connected_duration;
262
263        // Calculate uptime score based on total time known vs time connected
264        let known_duration = self.last_seen.duration_since(self.first_seen);
265        if known_duration.as_secs() > 0 {
266            self.uptime_score =
267                (self.total_uptime.as_secs_f64() / known_duration.as_secs_f64()).min(1.0);
268        }
269    }
270
271    /// Apply time-based decay to scores
272    fn apply_decay(&mut self, decay_rate: f64) {
273        let decay_factor = 1.0 - decay_rate;
274        self.transfer_success_rate *= decay_factor;
275        self.latency_score *= decay_factor;
276        self.protocol_compliance_score *= decay_factor;
277        self.uptime_score *= decay_factor;
278    }
279}
280
281/// Reputation event types
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum ReputationEvent {
284    /// Successful data transfer
285    SuccessfulTransfer,
286    /// Failed data transfer
287    FailedTransfer,
288    /// Low latency response
289    LowLatency,
290    /// High latency response
291    HighLatency,
292    /// Protocol violation detected
293    ProtocolViolation,
294    /// Peer disconnected gracefully
295    GracefulDisconnect,
296    /// Peer disconnected unexpectedly
297    UnexpectedDisconnect,
298}
299
300/// Reputation manager for tracking peer reputations
301pub struct ReputationManager {
302    config: ReputationConfig,
303    reputations: DashMap<PeerId, ReputationScore>,
304    stats: parking_lot::RwLock<ReputationStats>,
305}
306
307impl ReputationManager {
308    /// Create a new reputation manager
309    pub fn new(config: ReputationConfig) -> Self {
310        Self {
311            config,
312            reputations: DashMap::new(),
313            stats: parking_lot::RwLock::new(ReputationStats::default()),
314        }
315    }
316
317    /// Get reputation score for a peer
318    pub fn get_reputation(&self, peer_id: &PeerId) -> Option<ReputationScore> {
319        self.reputations.get(peer_id).map(|entry| entry.clone())
320    }
321
322    /// Check if a peer is trusted based on their reputation
323    pub fn is_trusted(&self, peer_id: &PeerId) -> bool {
324        self.reputations
325            .get(peer_id)
326            .map(|score| score.overall_score(&self.config) >= self.config.trust_threshold)
327            .unwrap_or(false)
328    }
329
330    /// Check if a peer has a bad reputation
331    pub fn is_bad_peer(&self, peer_id: &PeerId) -> bool {
332        self.reputations
333            .get(peer_id)
334            .map(|score| score.overall_score(&self.config) < self.config.bad_peer_threshold)
335            .unwrap_or(false)
336    }
337
338    /// Get list of trusted peers
339    pub fn get_trusted_peers(&self) -> Vec<PeerId> {
340        self.reputations
341            .iter()
342            .filter(|entry| {
343                entry.value().overall_score(&self.config) >= self.config.trust_threshold
344            })
345            .map(|entry| *entry.key())
346            .collect()
347    }
348
349    /// Get list of bad peers
350    pub fn get_bad_peers(&self) -> Vec<PeerId> {
351        self.reputations
352            .iter()
353            .filter(|entry| {
354                entry.value().overall_score(&self.config) < self.config.bad_peer_threshold
355            })
356            .map(|entry| *entry.key())
357            .collect()
358    }
359
360    /// Record a successful transfer
361    pub fn record_successful_transfer(&mut self, peer_id: &PeerId, _bytes: u64) {
362        let mut score = self.reputations.entry(*peer_id).or_default();
363        score.update_transfer_success_rate(true, self.config.ema_alpha);
364        score.last_seen = Instant::now();
365
366        let mut stats = self.stats.write();
367        stats.successful_events += 1;
368    }
369
370    /// Record a failed transfer
371    pub fn record_failed_transfer(&mut self, peer_id: &PeerId) {
372        let mut score = self.reputations.entry(*peer_id).or_default();
373        score.update_transfer_success_rate(false, self.config.ema_alpha);
374        score.last_seen = Instant::now();
375
376        let mut stats = self.stats.write();
377        stats.failed_events += 1;
378    }
379
380    /// Record low latency response
381    pub fn record_low_latency(&mut self, peer_id: &PeerId, latency_ms: u64) {
382        let mut score = self.reputations.entry(*peer_id).or_default();
383        score.update_latency_score(latency_ms, self.config.ema_alpha);
384        score.last_seen = Instant::now();
385
386        let mut stats = self.stats.write();
387        stats.latency_updates += 1;
388    }
389
390    /// Record a protocol violation
391    pub fn record_protocol_violation(&mut self, peer_id: &PeerId) {
392        let mut score = self.reputations.entry(*peer_id).or_default();
393        score.record_protocol_violation(self.config.ema_alpha);
394        score.last_seen = Instant::now();
395
396        let mut stats = self.stats.write();
397        stats.protocol_violations += 1;
398    }
399
400    /// Update uptime for a peer
401    pub fn update_uptime(&mut self, peer_id: &PeerId, duration: Duration) {
402        let mut score = self.reputations.entry(*peer_id).or_default();
403        score.update_uptime(duration);
404        score.last_seen = Instant::now();
405
406        let mut stats = self.stats.write();
407        stats.uptime_updates += 1;
408    }
409
410    /// Record a reputation event
411    pub fn record_event(&mut self, peer_id: &PeerId, event: ReputationEvent) {
412        match event {
413            ReputationEvent::SuccessfulTransfer => self.record_successful_transfer(peer_id, 0),
414            ReputationEvent::FailedTransfer => self.record_failed_transfer(peer_id),
415            ReputationEvent::LowLatency => self.record_low_latency(peer_id, 50),
416            ReputationEvent::HighLatency => self.record_low_latency(peer_id, 500),
417            ReputationEvent::ProtocolViolation => self.record_protocol_violation(peer_id),
418            ReputationEvent::GracefulDisconnect => {
419                // Maintain current score, just update last_seen
420                if let Some(mut score) = self.reputations.get_mut(peer_id) {
421                    score.last_seen = Instant::now();
422                }
423            }
424            ReputationEvent::UnexpectedDisconnect => {
425                // Penalize slightly for unexpected disconnect
426                self.record_failed_transfer(peer_id);
427            }
428        }
429    }
430
431    /// Apply time-based decay to all reputation scores
432    pub fn apply_decay(&mut self) {
433        for mut entry in self.reputations.iter_mut() {
434            entry.value_mut().apply_decay(self.config.score_decay_rate);
435        }
436    }
437
438    /// Remove stale peer reputations based on retention period
439    pub fn cleanup_stale(&mut self) -> usize {
440        let retention = self.config.retention_period;
441        let now = Instant::now();
442        let mut removed = 0;
443
444        self.reputations.retain(|_, score| {
445            let should_keep = now.duration_since(score.last_seen) < retention;
446            if !should_keep {
447                removed += 1;
448            }
449            should_keep
450        });
451
452        if removed > 0 {
453            let mut stats = self.stats.write();
454            stats.peers_removed += removed as u64;
455        }
456
457        removed
458    }
459
460    /// Get the number of tracked peers
461    pub fn tracked_peer_count(&self) -> usize {
462        self.reputations.len()
463    }
464
465    /// Get reputation statistics
466    pub fn stats(&self) -> ReputationStats {
467        self.stats.read().clone()
468    }
469
470    /// Clear all reputation data
471    pub fn clear(&mut self) {
472        self.reputations.clear();
473        *self.stats.write() = ReputationStats::default();
474    }
475}
476
477/// Statistics about reputation tracking
478#[derive(Debug, Clone, Default, Serialize, Deserialize)]
479pub struct ReputationStats {
480    /// Number of successful events recorded
481    pub successful_events: u64,
482
483    /// Number of failed events recorded
484    pub failed_events: u64,
485
486    /// Number of protocol violations recorded
487    pub protocol_violations: u64,
488
489    /// Number of latency updates
490    pub latency_updates: u64,
491
492    /// Number of uptime updates
493    pub uptime_updates: u64,
494
495    /// Number of peers removed due to staleness
496    pub peers_removed: u64,
497}
498
499// ============================================================================
500// PeerReputationTracker — event-driven peer scoring system
501// ============================================================================
502
503use std::collections::HashMap;
504use std::sync::atomic::{AtomicU64, Ordering};
505use std::sync::RwLock;
506
507/// Classification tier for a peer based on their reputation score.
508#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
509pub enum PeerTier {
510    /// Score <= -50: peer is banned and should not be used.
511    Banned,
512    /// -50 < score <= -20: peer is untrusted and should be de-prioritized.
513    Untrusted,
514    /// -20 < score < 20: peer has neutral standing.
515    Neutral,
516    /// 20 <= score < 50: peer is good and should be preferred.
517    Good,
518    /// score >= 50: peer is fully trusted.
519    Trusted,
520}
521
522/// An event that affects a peer's reputation score.
523#[derive(Debug, Clone, Copy, PartialEq)]
524pub enum PeerReputationEvent {
525    /// Peer successfully delivered a block.
526    BlockDelivered {
527        /// Round-trip latency in milliseconds.
528        latency_ms: u64,
529    },
530    /// Peer failed to deliver a requested block.
531    BlockFailed,
532    /// Peer sent a block whose CID or content did not match what was requested.
533    InvalidBlock,
534    /// Peer returned a valid DHT response.
535    DhtResponseValid {
536        /// Latency of the DHT response in milliseconds.
537        latency_ms: u64,
538    },
539    /// Peer's DHT query returned no results.
540    DhtResponseEmpty,
541    /// Connection to the peer failed.
542    ConnectionFailed,
543    /// Peer violated the protocol (severe).
544    ProtocolViolation,
545}
546
547impl PeerReputationEvent {
548    /// Returns the signed score delta this event contributes.
549    ///
550    /// Deltas:
551    /// - `BlockDelivered`: +1.0, +0.5 bonus when `latency_ms < 100`
552    /// - `BlockFailed`: -1.0
553    /// - `InvalidBlock`: -10.0
554    /// - `DhtResponseValid`: +0.5, +0.25 bonus when `latency_ms < 200`
555    /// - `DhtResponseEmpty`: -0.25
556    /// - `ConnectionFailed`: -2.0
557    /// - `ProtocolViolation`: -15.0
558    pub fn score_delta(&self) -> f64 {
559        match self {
560            PeerReputationEvent::BlockDelivered { latency_ms } => {
561                let bonus = if *latency_ms < 100 { 0.5 } else { 0.0 };
562                1.0 + bonus
563            }
564            PeerReputationEvent::BlockFailed => -1.0,
565            PeerReputationEvent::InvalidBlock => -10.0,
566            PeerReputationEvent::DhtResponseValid { latency_ms } => {
567                let bonus = if *latency_ms < 200 { 0.25 } else { 0.0 };
568                0.5 + bonus
569            }
570            PeerReputationEvent::DhtResponseEmpty => -0.25,
571            PeerReputationEvent::ConnectionFailed => -2.0,
572            PeerReputationEvent::ProtocolViolation => -15.0,
573        }
574    }
575}
576
577/// Per-peer reputation state.
578#[derive(Debug, Clone)]
579pub struct PeerReputation {
580    /// String identifier for the peer (e.g. libp2p PeerId string representation).
581    pub peer_id: String,
582    /// Current reputation score, clamped to `[-100.0, 100.0]`. Starts at 0.0.
583    pub score: f64,
584    /// Total number of events recorded for this peer.
585    pub event_count: u64,
586    /// Instant of the last recorded event, or `None` if no events yet.
587    pub last_event_at: Option<Instant>,
588    /// Instant at which this record was created.
589    pub created_at: Instant,
590}
591
592impl PeerReputation {
593    /// Creates a fresh reputation record for the given peer.
594    pub fn new(peer_id: impl Into<String>) -> Self {
595        Self {
596            peer_id: peer_id.into(),
597            score: 0.0,
598            event_count: 0,
599            last_event_at: None,
600            created_at: Instant::now(),
601        }
602    }
603
604    /// Applies the score delta for `event`, clamps the result, and increments the event counter.
605    pub fn record_event(&mut self, event: PeerReputationEvent) {
606        self.score = (self.score + event.score_delta()).clamp(-100.0, 100.0);
607        self.event_count = self.event_count.saturating_add(1);
608        self.last_event_at = Some(Instant::now());
609    }
610
611    /// Returns `true` when the peer's score has fallen to or below -50.0.
612    pub fn is_banned(&self) -> bool {
613        self.score <= -50.0
614    }
615
616    /// Returns `true` when the peer's score has reached or exceeded +50.0.
617    pub fn is_trusted(&self) -> bool {
618        self.score >= 50.0
619    }
620
621    /// Applies time-based exponential decay toward 0.
622    ///
623    /// The formula used is:
624    /// ```text
625    /// score *= 1.0 - 0.001 * elapsed.as_secs_f64().min(100.0)
626    /// ```
627    /// This approximates a decay of 0.1 % per second, capped at 10 % for any
628    /// single call regardless of how much real time has elapsed.
629    pub fn decay(&mut self, elapsed: Duration) {
630        let factor = 1.0 - 0.001 * elapsed.as_secs_f64().min(100.0);
631        self.score *= factor;
632    }
633
634    /// Returns the `PeerTier` that corresponds to the current score.
635    pub fn tier(&self) -> PeerTier {
636        if self.score <= -50.0 {
637            PeerTier::Banned
638        } else if self.score <= -20.0 {
639            PeerTier::Untrusted
640        } else if self.score < 20.0 {
641            PeerTier::Neutral
642        } else if self.score < 50.0 {
643            PeerTier::Good
644        } else {
645            PeerTier::Trusted
646        }
647    }
648}
649
650/// Atomic snapshot of aggregate statistics collected by [`PeerReputationTracker`].
651#[derive(Debug, Default)]
652pub struct PeerReputationStats {
653    /// Total number of events processed across all peers.
654    pub total_events: AtomicU64,
655    /// Number of times a peer's score dropped to or below the ban threshold.
656    pub total_bans: AtomicU64,
657    /// Number of times a previously-banned peer's score recovered above the ban threshold.
658    pub total_unbans: AtomicU64,
659}
660
661impl PeerReputationStats {
662    /// Creates a new zeroed stats instance.
663    pub fn new() -> Self {
664        Self::default()
665    }
666
667    /// Returns a plain-data snapshot of the current atomic values.
668    pub fn snapshot(&self) -> PeerReputationStatsSnapshot {
669        PeerReputationStatsSnapshot {
670            total_events: self.total_events.load(Ordering::Relaxed),
671            total_bans: self.total_bans.load(Ordering::Relaxed),
672            total_unbans: self.total_unbans.load(Ordering::Relaxed),
673        }
674    }
675}
676
677/// Non-atomic snapshot of [`PeerReputationStats`] values.
678#[derive(Debug, Clone, Copy, PartialEq, Eq)]
679pub struct PeerReputationStatsSnapshot {
680    /// Total number of events processed across all peers.
681    pub total_events: u64,
682    /// Number of times a peer's score dropped to or below the ban threshold.
683    pub total_bans: u64,
684    /// Number of times a previously-banned peer's score recovered above the ban threshold.
685    pub total_unbans: u64,
686}
687
688/// Thread-safe tracker that maintains per-peer reputation scores and exposes
689/// aggregate statistics via `AtomicU64` counters.
690///
691/// # Thread Safety
692///
693/// All mutating operations acquire the internal `RwLock<HashMap>` write lock,
694/// while read-only queries acquire only the read lock.  The aggregate stats are
695/// updated via relaxed atomic stores so they never block readers.
696pub struct PeerReputationTracker {
697    peers: RwLock<HashMap<String, PeerReputation>>,
698    stats: PeerReputationStats,
699}
700
701impl Default for PeerReputationTracker {
702    fn default() -> Self {
703        Self::new()
704    }
705}
706
707impl PeerReputationTracker {
708    /// Creates a new, empty tracker.
709    pub fn new() -> Self {
710        Self {
711            peers: RwLock::new(HashMap::new()),
712            stats: PeerReputationStats::new(),
713        }
714    }
715
716    /// Records an event for the given peer.
717    ///
718    /// If the peer is unknown a new record initialised to score 0 is created
719    /// automatically.  The function also updates `total_bans` / `total_unbans`
720    /// counters when the event crosses the ban threshold.
721    pub fn record_event(&self, peer_id: &str, event: PeerReputationEvent) {
722        let mut guard = self.peers.write().unwrap_or_else(|e| e.into_inner());
723
724        let peer = guard
725            .entry(peer_id.to_owned())
726            .or_insert_with(|| PeerReputation::new(peer_id));
727
728        let was_banned = peer.is_banned();
729        peer.record_event(event);
730        let now_banned = peer.is_banned();
731
732        self.stats.total_events.fetch_add(1, Ordering::Relaxed);
733
734        if !was_banned && now_banned {
735            self.stats.total_bans.fetch_add(1, Ordering::Relaxed);
736        } else if was_banned && !now_banned {
737            self.stats.total_unbans.fetch_add(1, Ordering::Relaxed);
738        }
739    }
740
741    /// Returns the current score for `peer_id`, or 0.0 if the peer is unknown.
742    pub fn get_score(&self, peer_id: &str) -> f64 {
743        let guard = self.peers.read().unwrap_or_else(|e| e.into_inner());
744        guard.get(peer_id).map(|p| p.score).unwrap_or(0.0)
745    }
746
747    /// Returns the [`PeerTier`] for `peer_id`.
748    ///
749    /// Unknown peers are treated as having a score of 0 (`PeerTier::Neutral`).
750    pub fn get_tier(&self, peer_id: &str) -> PeerTier {
751        let guard = self.peers.read().unwrap_or_else(|e| e.into_inner());
752        guard
753            .get(peer_id)
754            .map(|p| p.tier())
755            .unwrap_or(PeerTier::Neutral)
756    }
757
758    /// Returns `true` when the peer is banned (score <= -50).
759    pub fn is_banned(&self, peer_id: &str) -> bool {
760        let guard = self.peers.read().unwrap_or_else(|e| e.into_inner());
761        guard.get(peer_id).map(|p| p.is_banned()).unwrap_or(false)
762    }
763
764    /// Returns the peer IDs of all currently-banned peers.
765    pub fn banned_peers(&self) -> Vec<String> {
766        let guard = self.peers.read().unwrap_or_else(|e| e.into_inner());
767        guard
768            .values()
769            .filter(|p| p.is_banned())
770            .map(|p| p.peer_id.clone())
771            .collect()
772    }
773
774    /// Returns the peer IDs of all currently-trusted peers (score >= 50).
775    pub fn trusted_peers(&self) -> Vec<String> {
776        let guard = self.peers.read().unwrap_or_else(|e| e.into_inner());
777        guard
778            .values()
779            .filter(|p| p.is_trusted())
780            .map(|p| p.peer_id.clone())
781            .collect()
782    }
783
784    /// Returns the top `n` peers by score (descending), as `(peer_id, score)` pairs.
785    pub fn top_peers(&self, n: usize) -> Vec<(String, f64)> {
786        let guard = self.peers.read().unwrap_or_else(|e| e.into_inner());
787
788        let mut entries: Vec<(String, f64)> = guard
789            .values()
790            .map(|p| (p.peer_id.clone(), p.score))
791            .collect();
792
793        entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
794        entries.truncate(n);
795        entries
796    }
797
798    /// Applies time-based score decay to every tracked peer.
799    pub fn apply_decay(&self, elapsed: Duration) {
800        let mut guard = self.peers.write().unwrap_or_else(|e| e.into_inner());
801        for peer in guard.values_mut() {
802            peer.decay(elapsed);
803        }
804    }
805
806    /// Removes the reputation record for `peer_id`.  Does nothing if the peer
807    /// is unknown.
808    pub fn remove_peer(&self, peer_id: &str) {
809        let mut guard = self.peers.write().unwrap_or_else(|e| e.into_inner());
810        guard.remove(peer_id);
811    }
812
813    /// Returns the total number of peers currently being tracked.
814    pub fn peer_count(&self) -> usize {
815        let guard = self.peers.read().unwrap_or_else(|e| e.into_inner());
816        guard.len()
817    }
818
819    /// Returns a reference to the aggregate statistics.
820    pub fn stats(&self) -> &PeerReputationStats {
821        &self.stats
822    }
823}
824
825// ============================================================================
826// Tests
827// ============================================================================
828
829#[cfg(test)]
830mod tracker_tests {
831    use super::{PeerReputation, PeerReputationEvent, PeerReputationTracker, PeerTier};
832    use std::time::Duration;
833
834    fn make_tracker() -> PeerReputationTracker {
835        PeerReputationTracker::new()
836    }
837
838    // -------------------------------------------------------------------------
839    // PeerReputationEvent::score_delta
840    // -------------------------------------------------------------------------
841
842    #[test]
843    fn test_block_delivered_high_latency_delta() {
844        let delta = PeerReputationEvent::BlockDelivered { latency_ms: 200 }.score_delta();
845        assert!((delta - 1.0).abs() < f64::EPSILON);
846    }
847
848    #[test]
849    fn test_block_delivered_low_latency_bonus() {
850        let delta = PeerReputationEvent::BlockDelivered { latency_ms: 50 }.score_delta();
851        assert!((delta - 1.5).abs() < f64::EPSILON);
852    }
853
854    #[test]
855    fn test_block_failed_delta() {
856        assert!((PeerReputationEvent::BlockFailed.score_delta() - (-1.0)).abs() < f64::EPSILON);
857    }
858
859    #[test]
860    fn test_invalid_block_delta() {
861        assert!((PeerReputationEvent::InvalidBlock.score_delta() - (-10.0)).abs() < f64::EPSILON);
862    }
863
864    #[test]
865    fn test_dht_valid_slow_delta() {
866        let delta = PeerReputationEvent::DhtResponseValid { latency_ms: 500 }.score_delta();
867        assert!((delta - 0.5).abs() < f64::EPSILON);
868    }
869
870    #[test]
871    fn test_dht_valid_fast_bonus() {
872        let delta = PeerReputationEvent::DhtResponseValid { latency_ms: 100 }.score_delta();
873        assert!((delta - 0.75).abs() < f64::EPSILON);
874    }
875
876    #[test]
877    fn test_dht_empty_delta() {
878        assert!(
879            (PeerReputationEvent::DhtResponseEmpty.score_delta() - (-0.25)).abs() < f64::EPSILON
880        );
881    }
882
883    #[test]
884    fn test_connection_failed_delta() {
885        assert!(
886            (PeerReputationEvent::ConnectionFailed.score_delta() - (-2.0)).abs() < f64::EPSILON
887        );
888    }
889
890    #[test]
891    fn test_protocol_violation_delta() {
892        assert!(
893            (PeerReputationEvent::ProtocolViolation.score_delta() - (-15.0)).abs() < f64::EPSILON
894        );
895    }
896
897    // -------------------------------------------------------------------------
898    // PeerReputation struct
899    // -------------------------------------------------------------------------
900
901    #[test]
902    fn test_score_starts_at_zero() {
903        let rep = PeerReputation::new("peer-A");
904        assert_eq!(rep.score, 0.0);
905        assert_eq!(rep.event_count, 0);
906        assert!(rep.last_event_at.is_none());
907    }
908
909    #[test]
910    fn test_score_clamped_at_positive_100() {
911        let mut rep = PeerReputation::new("peer-B");
912        // Apply many positive events: each BlockDelivered(low latency) = +1.5
913        for _ in 0..200 {
914            rep.record_event(PeerReputationEvent::BlockDelivered { latency_ms: 10 });
915        }
916        assert!(
917            rep.score <= 100.0,
918            "score should not exceed 100.0, got {}",
919            rep.score
920        );
921        assert!((rep.score - 100.0).abs() < f64::EPSILON);
922    }
923
924    #[test]
925    fn test_score_clamped_at_negative_100() {
926        let mut rep = PeerReputation::new("peer-C");
927        // ProtocolViolation = -15 each; 7 times = -105 unclamped
928        for _ in 0..8 {
929            rep.record_event(PeerReputationEvent::ProtocolViolation);
930        }
931        assert!(rep.score >= -100.0, "score must not go below -100.0");
932        assert!((rep.score - (-100.0)).abs() < f64::EPSILON);
933    }
934
935    #[test]
936    fn test_multiple_events_accumulate() {
937        let mut rep = PeerReputation::new("peer-D");
938        rep.record_event(PeerReputationEvent::BlockDelivered { latency_ms: 50 }); // +1.5
939        rep.record_event(PeerReputationEvent::DhtResponseValid { latency_ms: 100 }); // +0.75
940        rep.record_event(PeerReputationEvent::BlockFailed); // -1.0
941                                                            // Expected: 1.5 + 0.75 - 1.0 = 1.25
942        assert!((rep.score - 1.25).abs() < 1e-9);
943        assert_eq!(rep.event_count, 3);
944        assert!(rep.last_event_at.is_some());
945    }
946
947    #[test]
948    fn test_is_banned_threshold() {
949        let mut rep = PeerReputation::new("peer-E");
950        // Drive score to exactly -50
951        rep.score = -50.0;
952        assert!(rep.is_banned());
953        rep.score = -49.9;
954        assert!(!rep.is_banned());
955        rep.score = -100.0;
956        assert!(rep.is_banned());
957    }
958
959    #[test]
960    fn test_is_trusted_threshold() {
961        let mut rep = PeerReputation::new("peer-F");
962        rep.score = 50.0;
963        assert!(rep.is_trusted());
964        rep.score = 49.9;
965        assert!(!rep.is_trusted());
966        rep.score = 100.0;
967        assert!(rep.is_trusted());
968    }
969
970    #[test]
971    fn test_peer_tier_classification() {
972        let mut rep = PeerReputation::new("peer-G");
973
974        rep.score = -60.0;
975        assert_eq!(rep.tier(), PeerTier::Banned);
976
977        rep.score = -50.0;
978        assert_eq!(rep.tier(), PeerTier::Banned);
979
980        rep.score = -35.0;
981        assert_eq!(rep.tier(), PeerTier::Untrusted);
982
983        rep.score = -20.0;
984        assert_eq!(rep.tier(), PeerTier::Untrusted);
985
986        rep.score = -0.1;
987        assert_eq!(rep.tier(), PeerTier::Neutral);
988
989        rep.score = 0.0;
990        assert_eq!(rep.tier(), PeerTier::Neutral);
991
992        rep.score = 10.0;
993        assert_eq!(rep.tier(), PeerTier::Neutral);
994
995        rep.score = 20.0;
996        assert_eq!(rep.tier(), PeerTier::Good);
997
998        rep.score = 49.9;
999        assert_eq!(rep.tier(), PeerTier::Good);
1000
1001        rep.score = 50.0;
1002        assert_eq!(rep.tier(), PeerTier::Trusted);
1003
1004        rep.score = 100.0;
1005        assert_eq!(rep.tier(), PeerTier::Trusted);
1006    }
1007
1008    #[test]
1009    fn test_decay_reduces_positive_score() {
1010        let mut rep = PeerReputation::new("peer-H");
1011        rep.score = 50.0;
1012        let before = rep.score;
1013        rep.decay(Duration::from_secs(10));
1014        assert!(rep.score < before, "decay should reduce a positive score");
1015        assert!(
1016            rep.score > 0.0,
1017            "score should remain positive after small decay"
1018        );
1019    }
1020
1021    #[test]
1022    fn test_decay_moves_negative_score_toward_zero() {
1023        let mut rep = PeerReputation::new("peer-I");
1024        rep.score = -50.0;
1025        let before = rep.score;
1026        rep.decay(Duration::from_secs(10));
1027        // score *= (1.0 - 0.001 * 10) = 0.99  →  -49.5
1028        assert!(
1029            rep.score > before,
1030            "decay should move a negative score toward 0"
1031        );
1032        assert!(rep.score < 0.0, "score should still be negative");
1033    }
1034
1035    // -------------------------------------------------------------------------
1036    // PeerReputationTracker
1037    // -------------------------------------------------------------------------
1038
1039    #[test]
1040    fn test_unknown_peer_returns_zero() {
1041        let tracker = make_tracker();
1042        assert_eq!(tracker.get_score("nobody"), 0.0);
1043        assert_eq!(tracker.get_tier("nobody"), PeerTier::Neutral);
1044        assert!(!tracker.is_banned("nobody"));
1045    }
1046
1047    #[test]
1048    fn test_record_event_creates_peer() {
1049        let tracker = make_tracker();
1050        tracker.record_event(
1051            "alice",
1052            PeerReputationEvent::BlockDelivered { latency_ms: 50 },
1053        );
1054        assert_eq!(tracker.peer_count(), 1);
1055        assert!(tracker.get_score("alice") > 0.0);
1056    }
1057
1058    #[test]
1059    fn test_banned_peers_list() {
1060        let tracker = make_tracker();
1061        // ban "bad-peer" with protocol violations
1062        for _ in 0..4 {
1063            tracker.record_event("bad-peer", PeerReputationEvent::ProtocolViolation);
1064        }
1065        // keep "good-peer" neutral
1066        tracker.record_event(
1067            "good-peer",
1068            PeerReputationEvent::BlockDelivered { latency_ms: 50 },
1069        );
1070
1071        let banned = tracker.banned_peers();
1072        assert!(banned.contains(&"bad-peer".to_owned()));
1073        assert!(!banned.contains(&"good-peer".to_owned()));
1074    }
1075
1076    #[test]
1077    fn test_trusted_peers_list() {
1078        let tracker = make_tracker();
1079        // give "star-peer" a score >= 50: need >= 34 BlockDelivered(high latency) at +1 each
1080        for _ in 0..55 {
1081            tracker.record_event(
1082                "star-peer",
1083                PeerReputationEvent::BlockDelivered { latency_ms: 200 },
1084            );
1085        }
1086        tracker.record_event("meh-peer", PeerReputationEvent::DhtResponseEmpty);
1087
1088        let trusted = tracker.trusted_peers();
1089        assert!(trusted.contains(&"star-peer".to_owned()));
1090        assert!(!trusted.contains(&"meh-peer".to_owned()));
1091    }
1092
1093    #[test]
1094    fn test_top_peers_ordering() {
1095        let tracker = make_tracker();
1096
1097        for _ in 0..10 {
1098            tracker.record_event(
1099                "alpha",
1100                PeerReputationEvent::BlockDelivered { latency_ms: 50 },
1101            );
1102        }
1103        for _ in 0..5 {
1104            tracker.record_event(
1105                "beta",
1106                PeerReputationEvent::BlockDelivered { latency_ms: 50 },
1107            );
1108        }
1109        tracker.record_event("gamma", PeerReputationEvent::BlockFailed);
1110
1111        let top = tracker.top_peers(3);
1112        assert_eq!(top.len(), 3);
1113        assert_eq!(top[0].0, "alpha");
1114        assert!(top[0].1 >= top[1].1);
1115        assert!(top[1].1 >= top[2].1);
1116    }
1117
1118    #[test]
1119    fn test_top_peers_fewer_than_n() {
1120        let tracker = make_tracker();
1121        tracker.record_event(
1122            "only-one",
1123            PeerReputationEvent::BlockDelivered { latency_ms: 200 },
1124        );
1125        let top = tracker.top_peers(10);
1126        assert_eq!(top.len(), 1);
1127    }
1128
1129    #[test]
1130    fn test_apply_decay_all_peers() {
1131        let tracker = make_tracker();
1132        for _ in 0..30 {
1133            tracker.record_event(
1134                "peer-x",
1135                PeerReputationEvent::BlockDelivered { latency_ms: 200 },
1136            );
1137        }
1138        let score_before = tracker.get_score("peer-x");
1139        tracker.apply_decay(Duration::from_secs(60));
1140        let score_after = tracker.get_score("peer-x");
1141        assert!(
1142            score_after < score_before,
1143            "decay must reduce a positive score"
1144        );
1145    }
1146
1147    #[test]
1148    fn test_remove_peer() {
1149        let tracker = make_tracker();
1150        tracker.record_event(
1151            "temp",
1152            PeerReputationEvent::BlockDelivered { latency_ms: 200 },
1153        );
1154        assert_eq!(tracker.peer_count(), 1);
1155        tracker.remove_peer("temp");
1156        assert_eq!(tracker.peer_count(), 0);
1157        // Removing unknown peer is a no-op
1158        tracker.remove_peer("nonexistent");
1159        assert_eq!(tracker.peer_count(), 0);
1160    }
1161
1162    #[test]
1163    fn test_stats_total_events() {
1164        let tracker = make_tracker();
1165        tracker.record_event(
1166            "p1",
1167            PeerReputationEvent::BlockDelivered { latency_ms: 200 },
1168        );
1169        tracker.record_event("p1", PeerReputationEvent::BlockFailed);
1170        tracker.record_event("p2", PeerReputationEvent::DhtResponseEmpty);
1171        let snap = tracker.stats().snapshot();
1172        assert_eq!(snap.total_events, 3);
1173    }
1174
1175    #[test]
1176    fn test_stats_ban_counter() {
1177        let tracker = make_tracker();
1178        // 4 × ProtocolViolation = -60 → ban triggered
1179        for _ in 0..4 {
1180            tracker.record_event("villain", PeerReputationEvent::ProtocolViolation);
1181        }
1182        let snap = tracker.stats().snapshot();
1183        assert_eq!(snap.total_bans, 1);
1184    }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189    use super::*;
1190
1191    #[test]
1192    fn test_reputation_config_presets() {
1193        let strict = ReputationConfig::strict();
1194        assert!(strict.trust_threshold > ReputationConfig::default().trust_threshold);
1195
1196        let lenient = ReputationConfig::lenient();
1197        assert!(lenient.trust_threshold < ReputationConfig::default().trust_threshold);
1198
1199        let perf = ReputationConfig::performance_focused();
1200        assert!(perf.latency_weight > ReputationConfig::default().latency_weight);
1201    }
1202
1203    #[test]
1204    fn test_reputation_score_default() {
1205        let score = ReputationScore::default();
1206        assert_eq!(score.transfer_success_rate, 1.0);
1207        assert_eq!(score.successful_transfers, 0);
1208        assert_eq!(score.failed_transfers, 0);
1209    }
1210
1211    #[test]
1212    fn test_reputation_score_overall() {
1213        let config = ReputationConfig::default();
1214        let score = ReputationScore::default();
1215
1216        let overall = score.overall_score(&config);
1217        assert!(overall > 0.9); // Should be close to 1.0 with default values
1218    }
1219
1220    #[test]
1221    fn test_successful_transfer_updates() {
1222        let config = ReputationConfig::default();
1223        let mut manager = ReputationManager::new(config);
1224        let peer = PeerId::random();
1225
1226        manager.record_successful_transfer(&peer, 1024);
1227
1228        let score = manager
1229            .get_reputation(&peer)
1230            .expect("test: get_reputation should return Some after recording successful transfer");
1231        assert_eq!(score.successful_transfers, 1);
1232        assert_eq!(score.failed_transfers, 0);
1233    }
1234
1235    #[test]
1236    fn test_failed_transfer_updates() {
1237        let config = ReputationConfig::default();
1238        let mut manager = ReputationManager::new(config);
1239        let peer = PeerId::random();
1240
1241        manager.record_failed_transfer(&peer);
1242
1243        let score = manager
1244            .get_reputation(&peer)
1245            .expect("test: get_reputation should return Some after recording failed transfer");
1246        assert_eq!(score.failed_transfers, 1);
1247        assert!(score.transfer_success_rate < 1.0);
1248    }
1249
1250    #[test]
1251    fn test_latency_scoring() {
1252        let config = ReputationConfig::default();
1253        let mut manager = ReputationManager::new(config);
1254        let peer = PeerId::random();
1255
1256        // Record low latency
1257        manager.record_low_latency(&peer, 50);
1258        let score = manager
1259            .get_reputation(&peer)
1260            .expect("test: get_reputation should return Some after recording low latency");
1261        assert!(score.latency_score > 0.9);
1262
1263        // Record high latency
1264        manager.record_low_latency(&peer, 900);
1265        let score = manager
1266            .get_reputation(&peer)
1267            .expect("test: get_reputation should return Some after recording high latency");
1268        assert!(score.latency_score < 0.5);
1269    }
1270
1271    #[test]
1272    fn test_protocol_violation() {
1273        let config = ReputationConfig::default();
1274        let mut manager = ReputationManager::new(config);
1275        let peer = PeerId::random();
1276
1277        manager.record_protocol_violation(&peer);
1278
1279        let score = manager
1280            .get_reputation(&peer)
1281            .expect("test: get_reputation should return Some after recording protocol violation");
1282        assert_eq!(score.protocol_violations, 1);
1283        assert!(score.protocol_compliance_score < 1.0);
1284    }
1285
1286    #[test]
1287    fn test_is_trusted() {
1288        let config = ReputationConfig::default();
1289        let mut manager = ReputationManager::new(config);
1290        let peer = PeerId::random();
1291
1292        // New peer starts trusted
1293        manager.record_successful_transfer(&peer, 1024);
1294        assert!(manager.is_trusted(&peer));
1295
1296        // Many failures reduce trust
1297        for _ in 0..10 {
1298            manager.record_failed_transfer(&peer);
1299        }
1300        assert!(!manager.is_trusted(&peer));
1301    }
1302
1303    #[test]
1304    fn test_is_bad_peer() {
1305        // Use a config with higher transfer success weight and faster EMA
1306        let config = ReputationConfig {
1307            transfer_success_weight: 0.9, // Focus on transfer success
1308            ema_alpha: 0.5,               // Faster updates
1309            latency_weight: 0.05,
1310            protocol_compliance_weight: 0.025,
1311            uptime_weight: 0.025,
1312            ..Default::default()
1313        };
1314
1315        let mut manager = ReputationManager::new(config);
1316        let peer = PeerId::random();
1317
1318        // Record many failures
1319        for _ in 0..20 {
1320            manager.record_failed_transfer(&peer);
1321        }
1322
1323        assert!(manager.is_bad_peer(&peer));
1324    }
1325
1326    #[test]
1327    fn test_get_trusted_peers() {
1328        let config = ReputationConfig::default();
1329        let mut manager = ReputationManager::new(config);
1330
1331        let peer1 = PeerId::random();
1332        let peer2 = PeerId::random();
1333
1334        manager.record_successful_transfer(&peer1, 1024);
1335        manager.record_successful_transfer(&peer2, 1024);
1336
1337        let trusted = manager.get_trusted_peers();
1338        assert_eq!(trusted.len(), 2);
1339    }
1340
1341    #[test]
1342    fn test_get_bad_peers() {
1343        // Use a config with higher transfer success weight and faster EMA
1344        let config = ReputationConfig {
1345            transfer_success_weight: 0.9,
1346            ema_alpha: 0.5,
1347            latency_weight: 0.05,
1348            protocol_compliance_weight: 0.025,
1349            uptime_weight: 0.025,
1350            ..Default::default()
1351        };
1352
1353        let mut manager = ReputationManager::new(config);
1354
1355        let peer = PeerId::random();
1356
1357        for _ in 0..20 {
1358            manager.record_failed_transfer(&peer);
1359        }
1360
1361        let bad_peers = manager.get_bad_peers();
1362        assert_eq!(bad_peers.len(), 1);
1363    }
1364
1365    #[test]
1366    fn test_uptime_tracking() {
1367        let config = ReputationConfig::default();
1368        let mut manager = ReputationManager::new(config);
1369        let peer = PeerId::random();
1370
1371        manager.update_uptime(&peer, Duration::from_secs(3600));
1372
1373        let score = manager
1374            .get_reputation(&peer)
1375            .expect("test: get_reputation should return Some after updating uptime");
1376        assert_eq!(score.total_uptime.as_secs(), 3600);
1377    }
1378
1379    #[test]
1380    fn test_reputation_events() {
1381        let config = ReputationConfig::default();
1382        let mut manager = ReputationManager::new(config);
1383        let peer = PeerId::random();
1384
1385        manager.record_event(&peer, ReputationEvent::SuccessfulTransfer);
1386        manager.record_event(&peer, ReputationEvent::LowLatency);
1387        manager.record_event(&peer, ReputationEvent::ProtocolViolation);
1388
1389        let score = manager
1390            .get_reputation(&peer)
1391            .expect("test: get_reputation should return Some after recording events");
1392        assert!(score.successful_transfers > 0);
1393        assert!(score.protocol_violations > 0);
1394    }
1395
1396    #[test]
1397    fn test_stats_tracking() {
1398        let config = ReputationConfig::default();
1399        let mut manager = ReputationManager::new(config);
1400        let peer = PeerId::random();
1401
1402        manager.record_successful_transfer(&peer, 1024);
1403        manager.record_failed_transfer(&peer);
1404        manager.record_protocol_violation(&peer);
1405
1406        let stats = manager.stats();
1407        assert_eq!(stats.successful_events, 1);
1408        assert_eq!(stats.failed_events, 1);
1409        assert_eq!(stats.protocol_violations, 1);
1410    }
1411
1412    #[test]
1413    fn test_tracked_peer_count() {
1414        let config = ReputationConfig::default();
1415        let mut manager = ReputationManager::new(config);
1416
1417        for _ in 0..5 {
1418            let peer = PeerId::random();
1419            manager.record_successful_transfer(&peer, 1024);
1420        }
1421
1422        assert_eq!(manager.tracked_peer_count(), 5);
1423    }
1424
1425    #[test]
1426    fn test_clear() {
1427        let config = ReputationConfig::default();
1428        let mut manager = ReputationManager::new(config);
1429        let peer = PeerId::random();
1430
1431        manager.record_successful_transfer(&peer, 1024);
1432        assert_eq!(manager.tracked_peer_count(), 1);
1433
1434        manager.clear();
1435        assert_eq!(manager.tracked_peer_count(), 0);
1436    }
1437}