use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone)]
pub struct ReputationConfig {
pub initial_score: f64,
pub min_score: f64,
pub max_score: f64,
pub decay_rate: f64,
pub success_weight: f64,
pub failure_weight: f64,
pub latency_weight: f64,
pub max_decay_duration: Duration,
}
impl Default for ReputationConfig {
fn default() -> Self {
Self {
initial_score: 0.5,
min_score: 0.0,
max_score: 1.0,
decay_rate: 0.1,
success_weight: 0.01,
failure_weight: 0.05,
latency_weight: 0.001,
max_decay_duration: Duration::from_secs(7 * 24 * 3600), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerReputation {
pub peer_id: String,
pub score: f64,
pub successes: u64,
pub failures: u64,
pub bytes_transferred: u64,
pub avg_latency_ms: f64,
pub last_seen: SystemTime,
pub first_seen: SystemTime,
pub is_banned: bool,
}
impl PeerReputation {
fn new(peer_id: String, initial_score: f64) -> Self {
let now = SystemTime::now();
Self {
peer_id,
score: initial_score,
successes: 0,
failures: 0,
bytes_transferred: 0,
avg_latency_ms: 0.0,
last_seen: now,
first_seen: now,
is_banned: false,
}
}
#[must_use]
#[inline]
pub fn success_rate(&self) -> f64 {
let total = self.successes + self.failures;
if total == 0 {
0.0
} else {
self.successes as f64 / total as f64
}
}
#[must_use]
#[inline]
pub const fn total_interactions(&self) -> u64 {
self.successes + self.failures
}
#[must_use]
#[inline]
pub const fn is_trusted(&self, threshold: f64) -> bool {
!self.is_banned && self.score >= threshold
}
#[must_use]
#[inline]
pub fn age_seconds(&self) -> u64 {
SystemTime::now()
.duration_since(self.first_seen)
.unwrap_or(Duration::from_secs(0))
.as_secs()
}
#[must_use]
#[inline]
pub fn time_since_last_seen(&self) -> u64 {
SystemTime::now()
.duration_since(self.last_seen)
.unwrap_or(Duration::from_secs(0))
.as_secs()
}
}
pub struct ReputationTracker {
config: ReputationConfig,
peers: HashMap<String, PeerReputation>,
last_decay: SystemTime,
}
impl ReputationTracker {
#[must_use]
pub fn new(config: ReputationConfig) -> Self {
Self {
config,
peers: HashMap::new(),
last_decay: SystemTime::now(),
}
}
fn get_or_create(&mut self, peer_id: String) -> &mut PeerReputation {
self.peers
.entry(peer_id.clone())
.or_insert_with(|| PeerReputation::new(peer_id, self.config.initial_score))
}
pub fn record_success(&mut self, peer_id: String, bytes_transferred: u64) {
let success_weight = self.config.success_weight;
let max_score = self.config.max_score;
let peer = self.get_or_create(peer_id);
peer.successes += 1;
peer.bytes_transferred += bytes_transferred;
peer.last_seen = SystemTime::now();
peer.score = (peer.score + success_weight).min(max_score);
}
pub fn record_failure(&mut self, peer_id: String, penalty: u64) {
let failure_weight = self.config.failure_weight;
let min_score = self.config.min_score;
let peer = self.get_or_create(peer_id);
peer.failures += 1;
peer.last_seen = SystemTime::now();
let penalty_score = failure_weight * (penalty as f64 / 100.0);
peer.score = (peer.score - penalty_score).max(min_score);
if peer.score <= min_score {
peer.is_banned = true;
}
}
pub fn record_latency(&mut self, peer_id: String, latency_ms: u64) {
let latency_weight = self.config.latency_weight;
let max_score = self.config.max_score;
let peer = self.get_or_create(peer_id);
let alpha = 0.3; peer.avg_latency_ms = alpha * latency_ms as f64 + (1.0 - alpha) * peer.avg_latency_ms;
let latency_factor = 1.0 - (latency_ms as f64 / 1000.0).min(1.0);
let latency_bonus = latency_weight * latency_factor;
peer.score = (peer.score + latency_bonus).min(max_score);
}
#[must_use]
#[inline]
pub fn get_reputation(&mut self, peer_id: &str) -> f64 {
self.apply_decay();
self.peers
.get(peer_id)
.map(|p| p.score)
.unwrap_or(self.config.initial_score)
}
#[must_use]
#[inline]
pub fn get_peer_data(&mut self, peer_id: &str) -> Option<&PeerReputation> {
self.apply_decay();
self.peers.get(peer_id)
}
#[must_use]
#[inline]
pub fn get_trusted_peers(&mut self, threshold: f64) -> Vec<String> {
self.apply_decay();
self.peers
.values()
.filter(|p| p.is_trusted(threshold))
.map(|p| p.peer_id.clone())
.collect()
}
#[must_use]
#[inline]
pub fn get_top_peers(&mut self, n: usize) -> Vec<String> {
self.apply_decay();
let mut peers: Vec<_> = self.peers.values().collect();
peers.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
peers.iter().take(n).map(|p| p.peer_id.clone()).collect()
}
#[must_use]
#[inline]
pub fn is_banned(&self, peer_id: &str) -> bool {
self.peers
.get(peer_id)
.map(|p| p.is_banned)
.unwrap_or(false)
}
#[inline]
pub fn ban_peer(&mut self, peer_id: String) {
let min_score = self.config.min_score;
let peer = self.get_or_create(peer_id);
peer.is_banned = true;
peer.score = min_score;
}
#[inline]
pub fn unban_peer(&mut self, peer_id: &str) {
if let Some(peer) = self.peers.get_mut(peer_id) {
peer.is_banned = false;
peer.score = self.config.initial_score;
}
}
fn apply_decay(&mut self) {
let now = SystemTime::now();
let elapsed = now
.duration_since(self.last_decay)
.unwrap_or(Duration::from_secs(0));
if elapsed < Duration::from_secs(3600) {
return;
}
let hours = elapsed.as_secs_f64() / 3600.0;
let decay_factor = self.config.decay_rate * hours;
for peer in self.peers.values_mut() {
let diff = peer.score - self.config.initial_score;
peer.score -= diff * decay_factor;
peer.score = peer
.score
.clamp(self.config.min_score, self.config.max_score);
}
self.last_decay = now;
}
pub fn cleanup_old_peers(&mut self, max_age_secs: u64) {
let now = SystemTime::now();
self.peers.retain(|_, peer| {
let age = now
.duration_since(peer.last_seen)
.unwrap_or(Duration::from_secs(0))
.as_secs();
age < max_age_secs
});
}
#[must_use]
pub fn get_stats(&self) -> ReputationStats {
let total_peers = self.peers.len();
let banned_peers = self.peers.values().filter(|p| p.is_banned).count();
let avg_score = if total_peers > 0 {
self.peers.values().map(|p| p.score).sum::<f64>() / total_peers as f64
} else {
0.0
};
ReputationStats {
total_peers,
banned_peers,
trusted_peers: self.peers.values().filter(|p| p.is_trusted(0.7)).count(),
avg_score,
total_interactions: self.peers.values().map(|p| p.total_interactions()).sum(),
}
}
#[must_use]
#[inline]
pub fn get_all_peer_ids(&self) -> Vec<String> {
self.peers.keys().cloned().collect()
}
#[must_use]
#[inline]
pub fn peer_count(&self) -> usize {
self.peers.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReputationStats {
pub total_peers: usize,
pub banned_peers: usize,
pub trusted_peers: usize,
pub avg_score: f64,
pub total_interactions: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reputation_config_default() {
let config = ReputationConfig::default();
assert_eq!(config.initial_score, 0.5);
assert_eq!(config.min_score, 0.0);
assert_eq!(config.max_score, 1.0);
}
#[test]
fn test_peer_reputation_new() {
let peer = PeerReputation::new("peer1".to_string(), 0.5);
assert_eq!(peer.peer_id, "peer1");
assert_eq!(peer.score, 0.5);
assert_eq!(peer.successes, 0);
assert_eq!(peer.failures, 0);
assert!(!peer.is_banned);
}
#[test]
fn test_record_success() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_success("peer1".to_string(), 1024);
let score = tracker.get_reputation("peer1");
assert!(score > 0.5);
let peer = tracker.get_peer_data("peer1").unwrap();
assert_eq!(peer.successes, 1);
assert_eq!(peer.bytes_transferred, 1024);
}
#[test]
fn test_record_failure() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_failure("peer1".to_string(), 100);
let score = tracker.get_reputation("peer1");
assert!(score < 0.5);
let peer = tracker.get_peer_data("peer1").unwrap();
assert_eq!(peer.failures, 1);
}
#[test]
fn test_record_latency() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_latency("peer1".to_string(), 50);
let peer = tracker.get_peer_data("peer1").unwrap();
assert!(peer.avg_latency_ms > 0.0);
}
#[test]
fn test_ban_peer() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.ban_peer("peer1".to_string());
assert!(tracker.is_banned("peer1"));
tracker.unban_peer("peer1");
assert!(!tracker.is_banned("peer1"));
}
#[test]
fn test_get_trusted_peers() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_success("peer1".to_string(), 1024);
tracker.record_success("peer1".to_string(), 1024);
tracker.record_success("peer1".to_string(), 1024);
tracker.record_failure("peer2".to_string(), 100);
let trusted = tracker.get_trusted_peers(0.5);
assert!(trusted.contains(&"peer1".to_string()));
}
#[test]
fn test_get_top_peers() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_success("peer1".to_string(), 1024);
tracker.record_success("peer2".to_string(), 2048);
tracker.record_success("peer2".to_string(), 2048); tracker.record_failure("peer3".to_string(), 50);
let top = tracker.get_top_peers(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0], "peer2"); }
#[test]
fn test_success_rate() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_success("peer1".to_string(), 1024);
tracker.record_success("peer1".to_string(), 1024);
tracker.record_failure("peer1".to_string(), 50);
let peer = tracker.get_peer_data("peer1").unwrap();
assert!((peer.success_rate() - 0.666).abs() < 0.01);
}
#[test]
fn test_cleanup_old_peers() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_success("peer1".to_string(), 1024);
assert_eq!(tracker.peer_count(), 1);
tracker.cleanup_old_peers(0);
assert_eq!(tracker.peer_count(), 0);
}
#[test]
fn test_reputation_stats() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
tracker.record_success("peer1".to_string(), 1024);
tracker.record_failure("peer2".to_string(), 50);
let stats = tracker.get_stats();
assert_eq!(stats.total_peers, 2);
assert_eq!(stats.total_interactions, 2);
}
#[test]
fn test_auto_ban_low_score() {
let config = ReputationConfig::default();
let mut tracker = ReputationTracker::new(config);
for _ in 0..20 {
tracker.record_failure("peer1".to_string(), 100);
}
assert!(tracker.is_banned("peer1"));
}
}