//! Rate limiting for P2P requests. //! //! Implements token bucket rate limiting to prevent DoS attacks and //! ensure fair resource allocation among peers. use hashbrown::HashMap; use libp2p::PeerId; use parking_lot::RwLock; use std::time::{Duration, Instant}; /// Rate limiter configuration. #[derive(Clone, Debug)] pub struct RateLimitConfig { /// Maximum requests per window. pub max_requests: u32, /// Time window duration. pub window: Duration, /// Burst allowance (extra requests allowed in short bursts). pub burst: u32, /// Cooldown period after limit exceeded. pub cooldown: Duration, } impl Default for RateLimitConfig { fn default() -> Self { RateLimitConfig { max_requests: 100, window: Duration::from_secs(60), burst: 20, cooldown: Duration::from_secs(30), } } } impl RateLimitConfig { /// Creates a strict rate limit (fewer requests allowed). pub fn strict() -> Self { RateLimitConfig { max_requests: 30, window: Duration::from_secs(60), burst: 5, cooldown: Duration::from_secs(60), } } /// Creates a relaxed rate limit (more requests allowed). pub fn relaxed() -> Self { RateLimitConfig { max_requests: 500, window: Duration::from_secs(60), burst: 100, cooldown: Duration::from_secs(10), } } /// Configuration for block requests. pub fn blocks() -> Self { RateLimitConfig { max_requests: 50, window: Duration::from_secs(60), burst: 10, cooldown: Duration::from_secs(30), } } /// Configuration for header requests. pub fn headers() -> Self { RateLimitConfig { max_requests: 100, window: Duration::from_secs(60), burst: 20, cooldown: Duration::from_secs(20), } } /// Configuration for transaction requests. pub fn transactions() -> Self { RateLimitConfig { max_requests: 200, window: Duration::from_secs(60), burst: 50, cooldown: Duration::from_secs(15), } } } /// Tracks rate limit state for a single peer. #[derive(Debug)] struct PeerRateState { /// Request timestamps in the current window. requests: Vec, /// When the cooldown ends (if in cooldown). cooldown_until: Option, /// Number of times limit was exceeded. violations: u32, } impl PeerRateState { fn new() -> Self { PeerRateState { requests: Vec::new(), cooldown_until: None, violations: 0, } } /// Cleans up old request timestamps outside the window. fn cleanup(&mut self, window: Duration) { let cutoff = Instant::now() - window; self.requests.retain(|t| *t > cutoff); } } /// Result of a rate limit check. #[derive(Clone, Debug, PartialEq, Eq)] pub enum RateLimitResult { /// Request is allowed. Allowed, /// Request is denied due to rate limit. Denied { /// Time until the peer can make another request. retry_after: Duration, }, /// Peer is in cooldown due to repeated violations. Cooldown { /// Time remaining in cooldown. remaining: Duration, }, } impl RateLimitResult { /// Returns true if the request is allowed. pub fn is_allowed(&self) -> bool { matches!(self, RateLimitResult::Allowed) } } /// Rate limiter for P2P requests. pub struct RateLimiter { /// Configuration. config: RateLimitConfig, /// Per-peer rate state. peers: RwLock>, } impl RateLimiter { /// Creates a new rate limiter with the given configuration. pub fn new(config: RateLimitConfig) -> Self { RateLimiter { config, peers: RwLock::new(HashMap::new()), } } /// Checks if a request from a peer should be allowed. pub fn check(&self, peer_id: &PeerId) -> RateLimitResult { let mut peers = self.peers.write(); let state = peers.entry(*peer_id).or_insert_with(PeerRateState::new); let now = Instant::now(); // Check cooldown if let Some(cooldown_until) = state.cooldown_until { if now < cooldown_until { return RateLimitResult::Cooldown { remaining: cooldown_until - now, }; } state.cooldown_until = None; } // Clean up old requests state.cleanup(self.config.window); // Check rate limit let total_allowed = self.config.max_requests + self.config.burst; if state.requests.len() as u32 >= total_allowed { state.violations += 1; // Enter cooldown if too many violations if state.violations >= 3 { state.cooldown_until = Some(now + self.config.cooldown); return RateLimitResult::Cooldown { remaining: self.config.cooldown, }; } // Calculate retry time let oldest = state.requests.first().copied().unwrap_or(now); let retry_after = self.config.window - (now - oldest); return RateLimitResult::Denied { retry_after }; } RateLimitResult::Allowed } /// Records a request from a peer. pub fn record(&self, peer_id: &PeerId) { let mut peers = self.peers.write(); let state = peers.entry(*peer_id).or_insert_with(PeerRateState::new); state.requests.push(Instant::now()); } /// Checks and records a request in one operation. pub fn check_and_record(&self, peer_id: &PeerId) -> RateLimitResult { let result = self.check(peer_id); if result.is_allowed() { self.record(peer_id); } result } /// Resets the rate limit state for a peer. pub fn reset(&self, peer_id: &PeerId) { self.peers.write().remove(peer_id); } /// Cleans up state for disconnected peers. pub fn cleanup(&self, connected_peers: &[PeerId]) { let connected: std::collections::HashSet<_> = connected_peers.iter().collect(); self.peers.write().retain(|id, _| connected.contains(id)); } /// Returns the number of requests made by a peer in the current window. pub fn request_count(&self, peer_id: &PeerId) -> u32 { let peers = self.peers.read(); peers .get(peer_id) .map(|s| s.requests.len() as u32) .unwrap_or(0) } /// Returns the number of violations for a peer. pub fn violation_count(&self, peer_id: &PeerId) -> u32 { let peers = self.peers.read(); peers.get(peer_id).map(|s| s.violations).unwrap_or(0) } } /// Collection of rate limiters for different request types. pub struct RateLimiters { /// Rate limiter for block requests. pub blocks: RateLimiter, /// Rate limiter for header requests. pub headers: RateLimiter, /// Rate limiter for transaction requests. pub transactions: RateLimiter, /// Global rate limiter (all request types). pub global: RateLimiter, } impl Default for RateLimiters { fn default() -> Self { RateLimiters { blocks: RateLimiter::new(RateLimitConfig::blocks()), headers: RateLimiter::new(RateLimitConfig::headers()), transactions: RateLimiter::new(RateLimitConfig::transactions()), global: RateLimiter::new(RateLimitConfig::default()), } } } impl RateLimiters { /// Creates rate limiters with custom configurations. pub fn new( blocks: RateLimitConfig, headers: RateLimitConfig, transactions: RateLimitConfig, global: RateLimitConfig, ) -> Self { RateLimiters { blocks: RateLimiter::new(blocks), headers: RateLimiter::new(headers), transactions: RateLimiter::new(transactions), global: RateLimiter::new(global), } } /// Checks if a block request from a peer should be allowed. pub fn check_block_request(&self, peer_id: &PeerId) -> RateLimitResult { // Check global first let global_result = self.global.check(peer_id); if !global_result.is_allowed() { return global_result; } self.blocks.check(peer_id) } /// Records a block request from a peer. pub fn record_block_request(&self, peer_id: &PeerId) { self.global.record(peer_id); self.blocks.record(peer_id); } /// Checks if a header request from a peer should be allowed. pub fn check_header_request(&self, peer_id: &PeerId) -> RateLimitResult { let global_result = self.global.check(peer_id); if !global_result.is_allowed() { return global_result; } self.headers.check(peer_id) } /// Records a header request from a peer. pub fn record_header_request(&self, peer_id: &PeerId) { self.global.record(peer_id); self.headers.record(peer_id); } /// Checks if a transaction request from a peer should be allowed. pub fn check_transaction_request(&self, peer_id: &PeerId) -> RateLimitResult { let global_result = self.global.check(peer_id); if !global_result.is_allowed() { return global_result; } self.transactions.check(peer_id) } /// Records a transaction request from a peer. pub fn record_transaction_request(&self, peer_id: &PeerId) { self.global.record(peer_id); self.transactions.record(peer_id); } /// Resets all rate limit state for a peer. pub fn reset(&self, peer_id: &PeerId) { self.blocks.reset(peer_id); self.headers.reset(peer_id); self.transactions.reset(peer_id); self.global.reset(peer_id); } /// Cleans up state for disconnected peers. pub fn cleanup(&self, connected_peers: &[PeerId]) { self.blocks.cleanup(connected_peers); self.headers.cleanup(connected_peers); self.transactions.cleanup(connected_peers); self.global.cleanup(connected_peers); } } #[cfg(test)] mod tests { use super::*; fn random_peer_id() -> PeerId { PeerId::random() } #[test] fn test_rate_limit_allows_initial_requests() { let limiter = RateLimiter::new(RateLimitConfig { max_requests: 10, window: Duration::from_secs(60), burst: 5, cooldown: Duration::from_secs(30), }); let peer = random_peer_id(); // Should allow first 15 requests (10 + 5 burst) for _ in 0..15 { assert!(limiter.check_and_record(&peer).is_allowed()); } // 16th should be denied assert!(!limiter.check(&peer).is_allowed()); } #[test] fn test_rate_limit_cooldown() { let limiter = RateLimiter::new(RateLimitConfig { max_requests: 5, window: Duration::from_secs(60), burst: 0, cooldown: Duration::from_secs(30), }); let peer = random_peer_id(); // Exhaust limit and trigger violations for _ in 0..5 { limiter.record(&peer); } // 3 violations should trigger cooldown for _ in 0..3 { let result = limiter.check(&peer); assert!(!result.is_allowed()); } // Should now be in cooldown match limiter.check(&peer) { RateLimitResult::Cooldown { .. } => {} _ => panic!("Expected cooldown"), } } #[test] fn test_rate_limiters_collection() { let limiters = RateLimiters::default(); let peer = random_peer_id(); // Block requests should work assert!(limiters.check_block_request(&peer).is_allowed()); limiters.record_block_request(&peer); // Header requests should work assert!(limiters.check_header_request(&peer).is_allowed()); limiters.record_header_request(&peer); // Transaction requests should work assert!(limiters.check_transaction_request(&peer).is_allowed()); limiters.record_transaction_request(&peer); } }