use std::{
collections::{BTreeSet, HashMap},
net::IpAddr,
sync::{Arc, Mutex, MutexGuard},
time::{Duration, Instant},
};
#[cfg(test)]
mod tests;
pub const BASE_COOLDOWN: Duration = Duration::from_secs(10 * 60);
pub const MAX_COOLDOWN: Duration = Duration::from_secs(2 * 60 * 60);
pub const MAX_COOLDOWN_PEERS: usize = 20_000;
#[derive(Clone, Debug, Default)]
pub struct PeerCooldowns {
peers: Arc<Mutex<CooldownState>>,
}
#[derive(Debug, Default)]
struct CooldownState {
peers: HashMap<IpAddr, Cooldown>,
expirations: BTreeSet<(Instant, IpAddr)>,
}
#[derive(Copy, Clone, Debug)]
struct Cooldown {
until: Instant,
strikes: u32,
}
impl Cooldown {
fn is_forgotten(&self, now: Instant) -> bool {
now.saturating_duration_since(self.until) >= MAX_COOLDOWN
}
}
impl PeerCooldowns {
pub fn is_cooling_down(&self, ip: IpAddr, now: Instant) -> bool {
self.peers()
.peers
.get(&ip.to_canonical())
.is_some_and(|cooldown| now < cooldown.until)
}
pub fn record_invalid_transaction(&self, ip: IpAddr, now: Instant) -> Option<Duration> {
let ip = ip.to_canonical();
let mut state = self.peers();
if !state.peers.contains_key(&ip) && state.peers.len() >= MAX_COOLDOWN_PEERS {
let (_until, earliest) = state.expirations.pop_first()?;
state.peers.remove(&earliest);
}
let mut cooldown = state.peers.get(&ip).copied().unwrap_or(Cooldown {
until: now,
strikes: 0,
});
if now < cooldown.until {
return None;
}
if cooldown.is_forgotten(now) {
cooldown.strikes = 0;
}
state.expirations.remove(&(cooldown.until, ip));
cooldown.strikes = cooldown.strikes.saturating_add(1);
let length = cooldown_length(cooldown.strikes);
cooldown.until = now + length;
state.expirations.insert((cooldown.until, ip));
state.peers.insert(ip, cooldown);
Some(length)
}
#[cfg(test)]
fn len(&self) -> usize {
self.peers().peers.len()
}
fn peers(&self) -> MutexGuard<'_, CooldownState> {
self.peers
.lock()
.expect("no code panics while holding the peer cooldowns lock")
}
}
fn cooldown_length(strikes: u32) -> Duration {
let doublings = strikes.saturating_sub(1);
1u32.checked_shl(doublings)
.and_then(|multiplier| BASE_COOLDOWN.checked_mul(multiplier))
.map_or(MAX_COOLDOWN, |length| length.min(MAX_COOLDOWN))
}