//! Peer reputation and banning system for Synor network. //! //! This module provides a comprehensive reputation system to track peer behavior //! and automatically ban misbehaving peers. The system tracks: //! - Reputation scores (0-100) //! - Violation history //! - Ban status and expiry //! - Gradual score recovery over time //! //! # Score Mechanics //! //! - New peers start with a score of 50 //! - Successful interactions increase score (up to 100) //! - Violations decrease score based on severity //! - Peers are auto-banned when score drops below 20 //! - Scores gradually recover over time when peer behaves well use hashbrown::HashMap; use libp2p::PeerId; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::time::{Duration, Instant}; /// Types of violations that affect peer reputation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ViolationType { /// Peer sent an invalid or malformed message. InvalidMessage, /// Peer responded too slowly or timed out. SlowResponse, /// Peer is sending too many requests (spam). Spam, /// Peer violated the protocol rules. ProtocolViolation, /// Peer sent duplicate data. DuplicateData, /// Peer provided incorrect data (e.g., bad block). InvalidData, /// Peer disconnected unexpectedly. UnexpectedDisconnect, /// Peer failed to respond to a request. NoResponse, } impl ViolationType { /// Returns the penalty for this violation type. pub fn penalty(&self) -> u8 { match self { ViolationType::InvalidMessage => 15, ViolationType::SlowResponse => 5, ViolationType::Spam => 20, ViolationType::ProtocolViolation => 25, ViolationType::DuplicateData => 3, ViolationType::InvalidData => 30, ViolationType::UnexpectedDisconnect => 5, ViolationType::NoResponse => 10, } } /// Returns the description of this violation type. pub fn description(&self) -> &'static str { match self { ViolationType::InvalidMessage => "sent invalid or malformed message", ViolationType::SlowResponse => "responded too slowly", ViolationType::Spam => "sending excessive requests", ViolationType::ProtocolViolation => "violated protocol rules", ViolationType::DuplicateData => "sent duplicate data", ViolationType::InvalidData => "provided invalid data", ViolationType::UnexpectedDisconnect => "disconnected unexpectedly", ViolationType::NoResponse => "failed to respond", } } } /// A record of a single violation. #[derive(Clone, Debug)] pub struct ViolationRecord { /// Type of violation. pub violation_type: ViolationType, /// When the violation occurred. pub timestamp: Instant, /// Optional context about the violation. pub context: Option, } impl ViolationRecord { /// Creates a new violation record. pub fn new(violation_type: ViolationType) -> Self { ViolationRecord { violation_type, timestamp: Instant::now(), context: None, } } /// Creates a new violation record with context. pub fn with_context(violation_type: ViolationType, context: String) -> Self { ViolationRecord { violation_type, timestamp: Instant::now(), context: Some(context), } } } /// Reputation tracking for a single peer. #[derive(Clone, Debug)] pub struct PeerReputation { /// Peer identifier. pub peer_id: PeerId, /// Current reputation score (0-100). pub score: u8, /// When the peer was last seen active. pub last_seen: Instant, /// History of violations. pub violations: Vec, /// When the ban expires (None if not banned). pub ban_until: Option, /// Total successful interactions. pub successes: u64, /// Total failed interactions. pub failures: u64, /// Last time the score was recovered. last_recovery: Instant, } impl PeerReputation { /// Initial reputation score for new peers. pub const INITIAL_SCORE: u8 = 50; /// Maximum reputation score. pub const MAX_SCORE: u8 = 100; /// Minimum reputation score (below this, peer is banned). pub const BAN_THRESHOLD: u8 = 20; /// Score increment for successful interactions. pub const SUCCESS_BONUS: u8 = 1; /// How often scores can recover (in seconds). pub const RECOVERY_INTERVAL_SECS: u64 = 300; // 5 minutes /// How much score recovers each interval. pub const RECOVERY_AMOUNT: u8 = 2; /// Creates a new peer reputation tracker. pub fn new(peer_id: PeerId) -> Self { let now = Instant::now(); PeerReputation { peer_id, score: Self::INITIAL_SCORE, last_seen: now, violations: Vec::new(), ban_until: None, successes: 0, failures: 0, last_recovery: now, } } /// Returns true if the peer is currently banned. pub fn is_banned(&self) -> bool { if let Some(ban_until) = self.ban_until { Instant::now() < ban_until } else { false } } /// Returns the remaining ban duration, if any. pub fn ban_remaining(&self) -> Option { self.ban_until.and_then(|until| { let now = Instant::now(); if now < until { Some(until - now) } else { None } }) } /// Records a successful interaction. pub fn record_success(&mut self) { self.successes += 1; self.last_seen = Instant::now(); self.score = self .score .saturating_add(Self::SUCCESS_BONUS) .min(Self::MAX_SCORE); } /// Records a violation and returns true if the peer should be banned. pub fn record_violation(&mut self, violation: ViolationType) -> bool { self.record_violation_with_context(violation, None) } /// Records a violation with context and returns true if the peer should be banned. pub fn record_violation_with_context( &mut self, violation: ViolationType, context: Option, ) -> bool { self.failures += 1; self.last_seen = Instant::now(); let record = if let Some(ctx) = context { ViolationRecord::with_context(violation, ctx) } else { ViolationRecord::new(violation) }; self.violations.push(record); // Apply penalty let penalty = violation.penalty(); self.score = self.score.saturating_sub(penalty); // Check if should be banned self.score < Self::BAN_THRESHOLD } /// Applies gradual score recovery over time. /// Call this periodically to allow good peers to recover reputation. pub fn apply_recovery(&mut self) { if self.is_banned() { return; } let now = Instant::now(); let elapsed = now.duration_since(self.last_recovery); let intervals = elapsed.as_secs() / Self::RECOVERY_INTERVAL_SECS; if intervals > 0 { let recovery = (intervals as u8).saturating_mul(Self::RECOVERY_AMOUNT); self.score = self.score.saturating_add(recovery).min(Self::MAX_SCORE); self.last_recovery = now; } } /// Returns the success rate as a percentage (0-100). pub fn success_rate(&self) -> f64 { let total = self.successes + self.failures; if total == 0 { 100.0 } else { (self.successes as f64 / total as f64) * 100.0 } } /// Returns the number of recent violations (within the last hour). pub fn recent_violation_count(&self) -> usize { let one_hour_ago = Instant::now() - Duration::from_secs(3600); self.violations .iter() .filter(|v| v.timestamp > one_hour_ago) .count() } /// Clears violation history older than the specified duration. pub fn clear_old_violations(&mut self, max_age: Duration) { let cutoff = Instant::now() - max_age; self.violations.retain(|v| v.timestamp > cutoff); } /// Manually bans the peer for the specified duration. pub fn ban(&mut self, duration: Duration) { self.ban_until = Some(Instant::now() + duration); self.score = 0; } /// Unbans the peer and resets their score to a low starting point. pub fn unban(&mut self) { self.ban_until = None; // Don't fully restore reputation - they need to earn it back self.score = Self::BAN_THRESHOLD; } } /// Configuration for the reputation manager. #[derive(Clone, Debug)] pub struct ReputationConfig { /// Default ban duration. pub default_ban_duration: Duration, /// Minimum ban duration. pub min_ban_duration: Duration, /// Maximum ban duration. pub max_ban_duration: Duration, /// How often to run cleanup of old data. pub cleanup_interval: Duration, /// Maximum age of violation records to keep. pub max_violation_age: Duration, /// Whether to enable gradual score recovery. pub enable_recovery: bool, } impl Default for ReputationConfig { fn default() -> Self { ReputationConfig { default_ban_duration: Duration::from_secs(3600), // 1 hour min_ban_duration: Duration::from_secs(300), // 5 minutes max_ban_duration: Duration::from_secs(86400), // 24 hours cleanup_interval: Duration::from_secs(600), // 10 minutes max_violation_age: Duration::from_secs(86400), // 24 hours enable_recovery: true, } } } /// Manages reputation for all peers. pub struct ReputationManager { /// Per-peer reputation data. reputations: RwLock>, /// Configuration. config: ReputationConfig, /// Last cleanup time. last_cleanup: RwLock, } impl ReputationManager { /// Creates a new reputation manager with default configuration. pub fn new() -> Self { Self::with_config(ReputationConfig::default()) } /// Creates a new reputation manager with custom configuration. pub fn with_config(config: ReputationConfig) -> Self { ReputationManager { reputations: RwLock::new(HashMap::new()), config, last_cleanup: RwLock::new(Instant::now()), } } /// Gets or creates reputation data for a peer. fn get_or_create(&self, peer_id: &PeerId) -> PeerReputation { let reputations = self.reputations.read(); if let Some(rep) = reputations.get(peer_id) { rep.clone() } else { drop(reputations); let rep = PeerReputation::new(*peer_id); self.reputations.write().insert(*peer_id, rep.clone()); rep } } /// Records a successful interaction with a peer. pub fn record_success(&self, peer_id: &PeerId) { let mut reputations = self.reputations.write(); let rep = reputations .entry(*peer_id) .or_insert_with(|| PeerReputation::new(*peer_id)); if self.config.enable_recovery { rep.apply_recovery(); } rep.record_success(); } /// Records a violation by a peer. /// Returns true if the peer was auto-banned as a result. pub fn record_violation(&self, peer_id: &PeerId, violation: ViolationType) -> bool { self.record_violation_with_context(peer_id, violation, None) } /// Records a violation by a peer with additional context. /// Returns true if the peer was auto-banned as a result. pub fn record_violation_with_context( &self, peer_id: &PeerId, violation: ViolationType, context: Option, ) -> bool { let mut reputations = self.reputations.write(); let rep = reputations .entry(*peer_id) .or_insert_with(|| PeerReputation::new(*peer_id)); let should_ban = rep.record_violation_with_context(violation, context); if should_ban && rep.ban_until.is_none() { // Calculate ban duration based on violation history let ban_duration = self.calculate_ban_duration(rep); rep.ban(ban_duration); true } else { false } } /// Calculates ban duration based on peer's violation history. fn calculate_ban_duration(&self, rep: &PeerReputation) -> Duration { let recent_violations = rep.recent_violation_count(); // Exponential backoff: longer bans for repeat offenders let multiplier = match recent_violations { 0..=2 => 1, 3..=5 => 2, 6..=10 => 4, _ => 8, }; let duration = self.config.default_ban_duration * multiplier; duration.clamp(self.config.min_ban_duration, self.config.max_ban_duration) } /// Checks if a peer is currently banned. pub fn is_banned(&self, peer_id: &PeerId) -> bool { let reputations = self.reputations.read(); if let Some(rep) = reputations.get(peer_id) { rep.is_banned() } else { false } } /// Gets the reputation score for a peer. /// Returns None if the peer is unknown. pub fn get_score(&self, peer_id: &PeerId) -> Option { let mut reputations = self.reputations.write(); if let Some(rep) = reputations.get_mut(peer_id) { if self.config.enable_recovery { rep.apply_recovery(); } Some(rep.score) } else { None } } /// Gets the full reputation data for a peer. pub fn get_reputation(&self, peer_id: &PeerId) -> Option { let mut reputations = self.reputations.write(); if let Some(rep) = reputations.get_mut(peer_id) { if self.config.enable_recovery { rep.apply_recovery(); } Some(rep.clone()) } else { None } } /// Manually bans a peer for the default duration. pub fn ban_peer(&self, peer_id: &PeerId) { self.ban_peer_for(peer_id, self.config.default_ban_duration); } /// Manually bans a peer for a specific duration. pub fn ban_peer_for(&self, peer_id: &PeerId, duration: Duration) { let mut reputations = self.reputations.write(); let rep = reputations .entry(*peer_id) .or_insert_with(|| PeerReputation::new(*peer_id)); rep.ban(duration); } /// Unbans a peer. pub fn unban_peer(&self, peer_id: &PeerId) { let mut reputations = self.reputations.write(); if let Some(rep) = reputations.get_mut(peer_id) { rep.unban(); } } /// Gets all currently banned peers. pub fn banned_peers(&self) -> Vec { self.reputations .read() .iter() .filter(|(_, rep)| rep.is_banned()) .map(|(id, _)| *id) .collect() } /// Gets peers with low reputation (at risk of being banned). pub fn low_reputation_peers(&self, threshold: u8) -> Vec<(PeerId, u8)> { self.reputations .read() .iter() .filter(|(_, rep)| rep.score < threshold && !rep.is_banned()) .map(|(id, rep)| (*id, rep.score)) .collect() } /// Gets peers sorted by reputation score (highest first). pub fn peers_by_reputation(&self) -> Vec<(PeerId, u8)> { let mut peers: Vec<_> = self .reputations .read() .iter() .filter(|(_, rep)| !rep.is_banned()) .map(|(id, rep)| (*id, rep.score)) .collect(); peers.sort_by(|a, b| b.1.cmp(&a.1)); peers } /// Removes reputation data for a peer. pub fn remove_peer(&self, peer_id: &PeerId) { self.reputations.write().remove(peer_id); } /// Runs periodic cleanup of old data. pub fn cleanup(&self) { let now = Instant::now(); // Check if cleanup is needed { let last_cleanup = self.last_cleanup.read(); if now.duration_since(*last_cleanup) < self.config.cleanup_interval { return; } } // Update last cleanup time *self.last_cleanup.write() = now; // Clean up old violations and expired bans let mut reputations = self.reputations.write(); for rep in reputations.values_mut() { rep.clear_old_violations(self.config.max_violation_age); // Clear expired bans if let Some(ban_until) = rep.ban_until { if now >= ban_until { rep.ban_until = None; // Restore to ban threshold score after ban expires rep.score = PeerReputation::BAN_THRESHOLD; } } // Apply recovery if self.config.enable_recovery { rep.apply_recovery(); } } } /// Returns statistics about peer reputations. pub fn stats(&self) -> ReputationStats { let reputations = self.reputations.read(); let total = reputations.len(); let banned = reputations.values().filter(|r| r.is_banned()).count(); let low_rep = reputations .values() .filter(|r| r.score < 30 && !r.is_banned()) .count(); let high_rep = reputations.values().filter(|r| r.score >= 80).count(); let avg_score = if total > 0 { reputations.values().map(|r| r.score as u64).sum::() / total as u64 } else { 50 } as u8; ReputationStats { total_peers: total, banned_peers: banned, low_reputation_peers: low_rep, high_reputation_peers: high_rep, average_score: avg_score, } } } impl Default for ReputationManager { fn default() -> Self { Self::new() } } /// Statistics about peer reputations. #[derive(Clone, Debug)] pub struct ReputationStats { /// Total number of tracked peers. pub total_peers: usize, /// Number of currently banned peers. pub banned_peers: usize, /// Number of peers with low reputation. pub low_reputation_peers: usize, /// Number of peers with high reputation. pub high_reputation_peers: usize, /// Average reputation score. pub average_score: u8, } #[cfg(test)] mod tests { use super::*; fn random_peer_id() -> PeerId { PeerId::random() } #[test] fn test_peer_reputation_new() { let peer_id = random_peer_id(); let rep = PeerReputation::new(peer_id); assert_eq!(rep.score, PeerReputation::INITIAL_SCORE); assert!(!rep.is_banned()); assert_eq!(rep.successes, 0); assert_eq!(rep.failures, 0); } #[test] fn test_record_success() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); rep.record_success(); assert_eq!(rep.successes, 1); assert_eq!( rep.score, PeerReputation::INITIAL_SCORE + PeerReputation::SUCCESS_BONUS ); } #[test] fn test_record_violation() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); let penalty = ViolationType::InvalidMessage.penalty(); let should_ban = rep.record_violation(ViolationType::InvalidMessage); assert!(!should_ban); // Score is still above threshold assert_eq!(rep.failures, 1); assert_eq!(rep.violations.len(), 1); assert_eq!(rep.score, PeerReputation::INITIAL_SCORE - penalty); } #[test] fn test_auto_ban_on_low_score() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); // Apply multiple severe violations to trigger ban let should_ban1 = rep.record_violation(ViolationType::ProtocolViolation); // -25 let should_ban2 = rep.record_violation(ViolationType::InvalidData); // -30 // First violation shouldn't ban (50 - 25 = 25) assert!(!should_ban1); // Second violation should trigger ban (25 - 30 = 0) assert!(should_ban2); assert!(rep.score < PeerReputation::BAN_THRESHOLD); } #[test] fn test_score_max_limit() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); rep.score = 99; rep.record_success(); rep.record_success(); assert_eq!(rep.score, PeerReputation::MAX_SCORE); } #[test] fn test_score_min_limit() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); rep.score = 5; rep.record_violation(ViolationType::InvalidData); // -30 assert_eq!(rep.score, 0); } #[test] fn test_manual_ban() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); rep.ban(Duration::from_secs(3600)); assert!(rep.is_banned()); assert!(rep.ban_remaining().is_some()); } #[test] fn test_unban() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); rep.ban(Duration::from_secs(3600)); assert!(rep.is_banned()); rep.unban(); assert!(!rep.is_banned()); assert_eq!(rep.score, PeerReputation::BAN_THRESHOLD); } #[test] fn test_reputation_manager_record_success() { let manager = ReputationManager::new(); let peer_id = random_peer_id(); manager.record_success(&peer_id); let score = manager.get_score(&peer_id).unwrap(); assert_eq!( score, PeerReputation::INITIAL_SCORE + PeerReputation::SUCCESS_BONUS ); } #[test] fn test_reputation_manager_record_violation() { let manager = ReputationManager::new(); let peer_id = random_peer_id(); let banned = manager.record_violation(&peer_id, ViolationType::SlowResponse); assert!(!banned); let score = manager.get_score(&peer_id).unwrap(); assert_eq!( score, PeerReputation::INITIAL_SCORE - ViolationType::SlowResponse.penalty() ); } #[test] fn test_reputation_manager_auto_ban() { let manager = ReputationManager::new(); let peer_id = random_peer_id(); // Apply severe violations to trigger auto-ban manager.record_violation(&peer_id, ViolationType::InvalidData); // -30 let banned = manager.record_violation(&peer_id, ViolationType::InvalidData); // -30 assert!(banned); assert!(manager.is_banned(&peer_id)); } #[test] fn test_reputation_manager_manual_ban() { let manager = ReputationManager::new(); let peer_id = random_peer_id(); manager.ban_peer(&peer_id); assert!(manager.is_banned(&peer_id)); } #[test] fn test_reputation_manager_unban() { let manager = ReputationManager::new(); let peer_id = random_peer_id(); manager.ban_peer(&peer_id); assert!(manager.is_banned(&peer_id)); manager.unban_peer(&peer_id); assert!(!manager.is_banned(&peer_id)); } #[test] fn test_banned_peers_list() { let manager = ReputationManager::new(); let peer1 = random_peer_id(); let peer2 = random_peer_id(); let peer3 = random_peer_id(); manager.ban_peer(&peer1); manager.ban_peer(&peer2); manager.record_success(&peer3); let banned = manager.banned_peers(); assert_eq!(banned.len(), 2); assert!(banned.contains(&peer1)); assert!(banned.contains(&peer2)); assert!(!banned.contains(&peer3)); } #[test] fn test_peers_by_reputation() { let manager = ReputationManager::new(); let peer1 = random_peer_id(); let peer2 = random_peer_id(); // Give peer1 higher reputation for _ in 0..10 { manager.record_success(&peer1); } manager.record_violation(&peer2, ViolationType::SlowResponse); let peers = manager.peers_by_reputation(); assert!(!peers.is_empty()); // peer1 should be first (higher score) if peers.len() >= 2 { assert!(peers[0].1 > peers[1].1); } } #[test] fn test_reputation_stats() { let manager = ReputationManager::new(); let peer1 = random_peer_id(); let peer2 = random_peer_id(); manager.record_success(&peer1); manager.ban_peer(&peer2); let stats = manager.stats(); assert_eq!(stats.total_peers, 2); assert_eq!(stats.banned_peers, 1); } #[test] fn test_violation_types() { // Test all violation types have penalties let violations = [ ViolationType::InvalidMessage, ViolationType::SlowResponse, ViolationType::Spam, ViolationType::ProtocolViolation, ViolationType::DuplicateData, ViolationType::InvalidData, ViolationType::UnexpectedDisconnect, ViolationType::NoResponse, ]; for v in violations { assert!(v.penalty() > 0); assert!(!v.description().is_empty()); } } #[test] fn test_success_rate() { let peer_id = random_peer_id(); let mut rep = PeerReputation::new(peer_id); // 100% when no interactions assert_eq!(rep.success_rate(), 100.0); // 75% with 3 successes and 1 failure rep.record_success(); rep.record_success(); rep.record_success(); rep.record_violation(ViolationType::SlowResponse); assert!((rep.success_rate() - 75.0).abs() < 0.01); } }