chie_core/
reputation.rs

1//! Peer reputation tracking system for network reliability.
2//!
3//! This module provides a reputation scoring system to track peer reliability,
4//! performance, and behavior in the P2P network.
5//!
6//! # Features
7//!
8//! - Dynamic reputation scoring based on peer behavior
9//! - Decay mechanism for outdated scores
10//! - Automatic banning of malicious peers
11//! - Performance metrics tracking
12//! - Configurable scoring weights
13//!
14//! # Example
15//!
16//! ```
17//! use chie_core::reputation::{ReputationTracker, ReputationConfig};
18//!
19//! let config = ReputationConfig::default();
20//! let mut tracker = ReputationTracker::new(config);
21//!
22//! // Record successful interaction
23//! tracker.record_success("peer1".to_string(), 100);
24//!
25//! // Record failure
26//! tracker.record_failure("peer2".to_string(), 50);
27//!
28//! // Check reputation
29//! let score = tracker.get_reputation("peer1");
30//! println!("Peer1 reputation: {:.2}", score);
31//!
32//! // Get trusted peers
33//! let trusted = tracker.get_trusted_peers(0.7);
34//! println!("Trusted peers: {:?}", trusted);
35//! ```
36
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39use std::time::{Duration, SystemTime};
40
41/// Reputation configuration.
42#[derive(Debug, Clone)]
43pub struct ReputationConfig {
44    /// Initial reputation score for new peers.
45    pub initial_score: f64,
46
47    /// Minimum reputation score (below this = banned).
48    pub min_score: f64,
49
50    /// Maximum reputation score.
51    pub max_score: f64,
52
53    /// Reputation decay rate per hour.
54    pub decay_rate: f64,
55
56    /// Weight for successful transactions.
57    pub success_weight: f64,
58
59    /// Weight for failed transactions.
60    pub failure_weight: f64,
61
62    /// Weight for latency (lower latency = higher score).
63    pub latency_weight: f64,
64
65    /// Maximum time before reputation decays to initial.
66    pub max_decay_duration: Duration,
67}
68
69impl Default for ReputationConfig {
70    fn default() -> Self {
71        Self {
72            initial_score: 0.5,
73            min_score: 0.0,
74            max_score: 1.0,
75            decay_rate: 0.1,
76            success_weight: 0.01,
77            failure_weight: 0.05,
78            latency_weight: 0.001,
79            max_decay_duration: Duration::from_secs(7 * 24 * 3600), // 7 days
80        }
81    }
82}
83
84/// Peer reputation data.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct PeerReputation {
87    /// Peer ID.
88    pub peer_id: String,
89
90    /// Current reputation score (0.0 to 1.0).
91    pub score: f64,
92
93    /// Total successful interactions.
94    pub successes: u64,
95
96    /// Total failed interactions.
97    pub failures: u64,
98
99    /// Total bytes transferred.
100    pub bytes_transferred: u64,
101
102    /// Average latency in milliseconds.
103    pub avg_latency_ms: f64,
104
105    /// Last interaction timestamp.
106    pub last_seen: SystemTime,
107
108    /// First interaction timestamp.
109    pub first_seen: SystemTime,
110
111    /// Is peer banned.
112    pub is_banned: bool,
113}
114
115impl PeerReputation {
116    /// Create a new peer reputation record.
117    fn new(peer_id: String, initial_score: f64) -> Self {
118        let now = SystemTime::now();
119        Self {
120            peer_id,
121            score: initial_score,
122            successes: 0,
123            failures: 0,
124            bytes_transferred: 0,
125            avg_latency_ms: 0.0,
126            last_seen: now,
127            first_seen: now,
128            is_banned: false,
129        }
130    }
131
132    /// Get success rate.
133    #[must_use]
134    #[inline]
135    pub fn success_rate(&self) -> f64 {
136        let total = self.successes + self.failures;
137        if total == 0 {
138            0.0
139        } else {
140            self.successes as f64 / total as f64
141        }
142    }
143
144    /// Get total interactions.
145    #[must_use]
146    #[inline]
147    pub const fn total_interactions(&self) -> u64 {
148        self.successes + self.failures
149    }
150
151    /// Check if peer is trusted (score above threshold).
152    #[must_use]
153    #[inline]
154    pub const fn is_trusted(&self, threshold: f64) -> bool {
155        !self.is_banned && self.score >= threshold
156    }
157
158    /// Get age in seconds.
159    #[must_use]
160    #[inline]
161    pub fn age_seconds(&self) -> u64 {
162        SystemTime::now()
163            .duration_since(self.first_seen)
164            .unwrap_or(Duration::from_secs(0))
165            .as_secs()
166    }
167
168    /// Get time since last interaction in seconds.
169    #[must_use]
170    #[inline]
171    pub fn time_since_last_seen(&self) -> u64 {
172        SystemTime::now()
173            .duration_since(self.last_seen)
174            .unwrap_or(Duration::from_secs(0))
175            .as_secs()
176    }
177}
178
179/// Reputation tracker for managing peer reputations.
180pub struct ReputationTracker {
181    /// Configuration.
182    config: ReputationConfig,
183
184    /// Peer reputation records.
185    peers: HashMap<String, PeerReputation>,
186
187    /// Last decay timestamp.
188    last_decay: SystemTime,
189}
190
191impl ReputationTracker {
192    /// Create a new reputation tracker.
193    #[must_use]
194    pub fn new(config: ReputationConfig) -> Self {
195        Self {
196            config,
197            peers: HashMap::new(),
198            last_decay: SystemTime::now(),
199        }
200    }
201
202    /// Get or create peer reputation.
203    fn get_or_create(&mut self, peer_id: String) -> &mut PeerReputation {
204        self.peers
205            .entry(peer_id.clone())
206            .or_insert_with(|| PeerReputation::new(peer_id, self.config.initial_score))
207    }
208
209    /// Record a successful interaction.
210    pub fn record_success(&mut self, peer_id: String, bytes_transferred: u64) {
211        let success_weight = self.config.success_weight;
212        let max_score = self.config.max_score;
213
214        let peer = self.get_or_create(peer_id);
215        peer.successes += 1;
216        peer.bytes_transferred += bytes_transferred;
217        peer.last_seen = SystemTime::now();
218
219        // Increase reputation
220        peer.score = (peer.score + success_weight).min(max_score);
221    }
222
223    /// Record a failed interaction.
224    pub fn record_failure(&mut self, peer_id: String, penalty: u64) {
225        let failure_weight = self.config.failure_weight;
226        let min_score = self.config.min_score;
227
228        let peer = self.get_or_create(peer_id);
229        peer.failures += 1;
230        peer.last_seen = SystemTime::now();
231
232        // Decrease reputation
233        let penalty_score = failure_weight * (penalty as f64 / 100.0);
234        peer.score = (peer.score - penalty_score).max(min_score);
235
236        // Ban if score too low
237        if peer.score <= min_score {
238            peer.is_banned = true;
239        }
240    }
241
242    /// Record latency measurement.
243    pub fn record_latency(&mut self, peer_id: String, latency_ms: u64) {
244        let latency_weight = self.config.latency_weight;
245        let max_score = self.config.max_score;
246
247        let peer = self.get_or_create(peer_id);
248
249        // Update moving average
250        let alpha = 0.3; // Smoothing factor
251        peer.avg_latency_ms = alpha * latency_ms as f64 + (1.0 - alpha) * peer.avg_latency_ms;
252
253        // Adjust reputation based on latency
254        let latency_factor = 1.0 - (latency_ms as f64 / 1000.0).min(1.0);
255        let latency_bonus = latency_weight * latency_factor;
256        peer.score = (peer.score + latency_bonus).min(max_score);
257    }
258
259    /// Get peer reputation score.
260    #[must_use]
261    #[inline]
262    pub fn get_reputation(&mut self, peer_id: &str) -> f64 {
263        self.apply_decay();
264        self.peers
265            .get(peer_id)
266            .map(|p| p.score)
267            .unwrap_or(self.config.initial_score)
268    }
269
270    /// Get peer reputation data.
271    #[must_use]
272    #[inline]
273    pub fn get_peer_data(&mut self, peer_id: &str) -> Option<&PeerReputation> {
274        self.apply_decay();
275        self.peers.get(peer_id)
276    }
277
278    /// Get all trusted peers above threshold.
279    #[must_use]
280    #[inline]
281    pub fn get_trusted_peers(&mut self, threshold: f64) -> Vec<String> {
282        self.apply_decay();
283        self.peers
284            .values()
285            .filter(|p| p.is_trusted(threshold))
286            .map(|p| p.peer_id.clone())
287            .collect()
288    }
289
290    /// Get top N peers by reputation.
291    #[must_use]
292    #[inline]
293    pub fn get_top_peers(&mut self, n: usize) -> Vec<String> {
294        self.apply_decay();
295        let mut peers: Vec<_> = self.peers.values().collect();
296        peers.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
297        peers.iter().take(n).map(|p| p.peer_id.clone()).collect()
298    }
299
300    /// Check if peer is banned.
301    #[must_use]
302    #[inline]
303    pub fn is_banned(&self, peer_id: &str) -> bool {
304        self.peers
305            .get(peer_id)
306            .map(|p| p.is_banned)
307            .unwrap_or(false)
308    }
309
310    /// Manually ban a peer.
311    #[inline]
312    pub fn ban_peer(&mut self, peer_id: String) {
313        let min_score = self.config.min_score;
314        let peer = self.get_or_create(peer_id);
315        peer.is_banned = true;
316        peer.score = min_score;
317    }
318
319    /// Unban a peer.
320    #[inline]
321    pub fn unban_peer(&mut self, peer_id: &str) {
322        if let Some(peer) = self.peers.get_mut(peer_id) {
323            peer.is_banned = false;
324            peer.score = self.config.initial_score;
325        }
326    }
327
328    /// Apply reputation decay to all peers.
329    fn apply_decay(&mut self) {
330        let now = SystemTime::now();
331        let elapsed = now
332            .duration_since(self.last_decay)
333            .unwrap_or(Duration::from_secs(0));
334
335        if elapsed < Duration::from_secs(3600) {
336            // Only decay once per hour
337            return;
338        }
339
340        let hours = elapsed.as_secs_f64() / 3600.0;
341        let decay_factor = self.config.decay_rate * hours;
342
343        for peer in self.peers.values_mut() {
344            // Decay towards initial score
345            let diff = peer.score - self.config.initial_score;
346            peer.score -= diff * decay_factor;
347            peer.score = peer
348                .score
349                .clamp(self.config.min_score, self.config.max_score);
350        }
351
352        self.last_decay = now;
353    }
354
355    /// Clean up old peer records.
356    pub fn cleanup_old_peers(&mut self, max_age_secs: u64) {
357        let now = SystemTime::now();
358        self.peers.retain(|_, peer| {
359            let age = now
360                .duration_since(peer.last_seen)
361                .unwrap_or(Duration::from_secs(0))
362                .as_secs();
363            age < max_age_secs
364        });
365    }
366
367    #[must_use]
368    /// Get reputation statistics.
369    pub fn get_stats(&self) -> ReputationStats {
370        let total_peers = self.peers.len();
371        let banned_peers = self.peers.values().filter(|p| p.is_banned).count();
372        let avg_score = if total_peers > 0 {
373            self.peers.values().map(|p| p.score).sum::<f64>() / total_peers as f64
374        } else {
375            0.0
376        };
377
378        ReputationStats {
379            total_peers,
380            banned_peers,
381            trusted_peers: self.peers.values().filter(|p| p.is_trusted(0.7)).count(),
382            avg_score,
383            total_interactions: self.peers.values().map(|p| p.total_interactions()).sum(),
384        }
385    }
386
387    /// Get all peer IDs.
388    #[must_use]
389    #[inline]
390    pub fn get_all_peer_ids(&self) -> Vec<String> {
391        self.peers.keys().cloned().collect()
392    }
393
394    /// Get peer count.
395    #[must_use]
396    #[inline]
397    pub fn peer_count(&self) -> usize {
398        self.peers.len()
399    }
400}
401
402/// Reputation system statistics.
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct ReputationStats {
405    /// Total number of peers tracked.
406    pub total_peers: usize,
407
408    /// Number of banned peers.
409    pub banned_peers: usize,
410
411    /// Number of trusted peers (score >= 0.7).
412    pub trusted_peers: usize,
413
414    /// Average reputation score.
415    pub avg_score: f64,
416
417    /// Total interactions across all peers.
418    pub total_interactions: u64,
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_reputation_config_default() {
427        let config = ReputationConfig::default();
428        assert_eq!(config.initial_score, 0.5);
429        assert_eq!(config.min_score, 0.0);
430        assert_eq!(config.max_score, 1.0);
431    }
432
433    #[test]
434    fn test_peer_reputation_new() {
435        let peer = PeerReputation::new("peer1".to_string(), 0.5);
436        assert_eq!(peer.peer_id, "peer1");
437        assert_eq!(peer.score, 0.5);
438        assert_eq!(peer.successes, 0);
439        assert_eq!(peer.failures, 0);
440        assert!(!peer.is_banned);
441    }
442
443    #[test]
444    fn test_record_success() {
445        let config = ReputationConfig::default();
446        let mut tracker = ReputationTracker::new(config);
447
448        tracker.record_success("peer1".to_string(), 1024);
449        let score = tracker.get_reputation("peer1");
450
451        assert!(score > 0.5);
452        let peer = tracker.get_peer_data("peer1").unwrap();
453        assert_eq!(peer.successes, 1);
454        assert_eq!(peer.bytes_transferred, 1024);
455    }
456
457    #[test]
458    fn test_record_failure() {
459        let config = ReputationConfig::default();
460        let mut tracker = ReputationTracker::new(config);
461
462        tracker.record_failure("peer1".to_string(), 100);
463        let score = tracker.get_reputation("peer1");
464
465        assert!(score < 0.5);
466        let peer = tracker.get_peer_data("peer1").unwrap();
467        assert_eq!(peer.failures, 1);
468    }
469
470    #[test]
471    fn test_record_latency() {
472        let config = ReputationConfig::default();
473        let mut tracker = ReputationTracker::new(config);
474
475        tracker.record_latency("peer1".to_string(), 50);
476        let peer = tracker.get_peer_data("peer1").unwrap();
477
478        assert!(peer.avg_latency_ms > 0.0);
479    }
480
481    #[test]
482    fn test_ban_peer() {
483        let config = ReputationConfig::default();
484        let mut tracker = ReputationTracker::new(config);
485
486        tracker.ban_peer("peer1".to_string());
487        assert!(tracker.is_banned("peer1"));
488
489        tracker.unban_peer("peer1");
490        assert!(!tracker.is_banned("peer1"));
491    }
492
493    #[test]
494    fn test_get_trusted_peers() {
495        let config = ReputationConfig::default();
496        let mut tracker = ReputationTracker::new(config);
497
498        tracker.record_success("peer1".to_string(), 1024);
499        tracker.record_success("peer1".to_string(), 1024);
500        tracker.record_success("peer1".to_string(), 1024);
501
502        tracker.record_failure("peer2".to_string(), 100);
503
504        let trusted = tracker.get_trusted_peers(0.5);
505        assert!(trusted.contains(&"peer1".to_string()));
506    }
507
508    #[test]
509    fn test_get_top_peers() {
510        let config = ReputationConfig::default();
511        let mut tracker = ReputationTracker::new(config);
512
513        tracker.record_success("peer1".to_string(), 1024);
514        tracker.record_success("peer2".to_string(), 2048);
515        tracker.record_success("peer2".to_string(), 2048); // More successes for peer2
516        tracker.record_failure("peer3".to_string(), 50);
517
518        let top = tracker.get_top_peers(2);
519        assert_eq!(top.len(), 2);
520        assert_eq!(top[0], "peer2"); // peer2 should have highest score
521    }
522
523    #[test]
524    fn test_success_rate() {
525        let config = ReputationConfig::default();
526        let mut tracker = ReputationTracker::new(config);
527
528        tracker.record_success("peer1".to_string(), 1024);
529        tracker.record_success("peer1".to_string(), 1024);
530        tracker.record_failure("peer1".to_string(), 50);
531
532        let peer = tracker.get_peer_data("peer1").unwrap();
533        assert!((peer.success_rate() - 0.666).abs() < 0.01);
534    }
535
536    #[test]
537    fn test_cleanup_old_peers() {
538        let config = ReputationConfig::default();
539        let mut tracker = ReputationTracker::new(config);
540
541        tracker.record_success("peer1".to_string(), 1024);
542        assert_eq!(tracker.peer_count(), 1);
543
544        tracker.cleanup_old_peers(0);
545        assert_eq!(tracker.peer_count(), 0);
546    }
547
548    #[test]
549    fn test_reputation_stats() {
550        let config = ReputationConfig::default();
551        let mut tracker = ReputationTracker::new(config);
552
553        tracker.record_success("peer1".to_string(), 1024);
554        tracker.record_failure("peer2".to_string(), 50);
555
556        let stats = tracker.get_stats();
557        assert_eq!(stats.total_peers, 2);
558        assert_eq!(stats.total_interactions, 2);
559    }
560
561    #[test]
562    fn test_auto_ban_low_score() {
563        let config = ReputationConfig::default();
564        let mut tracker = ReputationTracker::new(config);
565
566        // Record many failures to drive score to minimum
567        for _ in 0..20 {
568            tracker.record_failure("peer1".to_string(), 100);
569        }
570
571        assert!(tracker.is_banned("peer1"));
572    }
573}