844 lines
26 KiB
Rust
844 lines
26 KiB
Rust
//! 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<String>,
|
|
}
|
|
|
|
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<ViolationRecord>,
|
|
/// When the ban expires (None if not banned).
|
|
pub ban_until: Option<Instant>,
|
|
/// 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<Duration> {
|
|
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<String>,
|
|
) -> 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<HashMap<PeerId, PeerReputation>>,
|
|
/// Configuration.
|
|
config: ReputationConfig,
|
|
/// Last cleanup time.
|
|
last_cleanup: RwLock<Instant>,
|
|
}
|
|
|
|
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<String>,
|
|
) -> 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<u8> {
|
|
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<PeerReputation> {
|
|
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<PeerId> {
|
|
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::<u64>() / 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);
|
|
}
|
|
}
|