pub type Millis = u64;
#[derive(Debug, Clone)]
pub struct Awareness {
score: i32,
max: i32,
}
impl Awareness {
#[must_use]
pub fn new(max: i32) -> Self {
Self {
score: 0,
max: max.max(1),
}
}
pub fn apply_delta(&mut self, delta: i32) {
self.score = (self.score + delta).clamp(0, self.max - 1);
}
#[must_use]
pub fn score(&self) -> i32 {
self.score
}
#[must_use]
pub fn multiplier(&self) -> u64 {
(self.score as u64) + 1
}
#[must_use]
pub fn scale(&self, base_ms: Millis) -> Millis {
base_ms.saturating_mul(self.multiplier())
}
}
#[must_use]
pub fn node_scale(n: usize) -> f64 {
let n = n.max(1) as f64;
n.log10().max(1.0)
}
#[must_use]
pub fn min_suspicion_ms(suspicion_mult: u32, n: usize, probe_interval_ms: Millis) -> Millis {
let raw = (suspicion_mult as f64) * node_scale(n) * (probe_interval_ms as f64);
raw.round() as Millis
}
#[must_use]
pub fn max_suspicion_ms(min_ms: Millis, suspicion_max_timeout_mult: u32) -> Millis {
min_ms.saturating_mul(suspicion_max_timeout_mult as u64)
}
#[must_use]
pub fn suspicion_timeout_ms(min_ms: Millis, max_ms: Millis, confirmations: u32, k: u32) -> Millis {
let max_ms = max_ms.max(min_ms);
if confirmations == 0 {
return max_ms;
}
let k = k.max(1);
let frac = (f64::from(confirmations + 1)).ln() / (f64::from(k + 1)).ln();
let frac = frac.clamp(0.0, 1.0);
let span = (max_ms - min_ms) as f64;
let reduction = (span * frac).round() as Millis;
max_ms.saturating_sub(reduction).max(min_ms)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn awareness_starts_healthy_and_clamps() {
let mut a = Awareness::new(8);
assert_eq!(a.score(), 0);
assert_eq!(a.multiplier(), 1);
assert_eq!(a.scale(500), 500);
a.apply_delta(-5);
assert_eq!(a.score(), 0);
a.apply_delta(2);
assert_eq!(a.score(), 2);
assert_eq!(a.multiplier(), 3);
assert_eq!(a.scale(500), 1500);
a.apply_delta(100);
assert_eq!(a.score(), 7);
assert_eq!(a.multiplier(), 8);
}
#[test]
fn awareness_min_max_is_clamped_up() {
let a = Awareness::new(0);
assert_eq!(a.multiplier(), 1);
let mut a = Awareness::new(-3);
a.apply_delta(10);
assert_eq!(a.score(), 0);
}
#[test]
fn node_scale_floor_is_one() {
assert!((node_scale(0) - 1.0).abs() < 1e-9);
assert!((node_scale(1) - 1.0).abs() < 1e-9);
assert!((node_scale(10) - 1.0).abs() < 1e-9);
assert!((node_scale(100) - 2.0).abs() < 1e-9);
}
#[test]
fn suspicion_window_shrinks_with_confirmations() {
let n = 7;
let min = min_suspicion_ms(4, n, 1000);
let max = max_suspicion_ms(min, 6);
assert_eq!(min, 4000);
assert_eq!(max, 24000);
assert_eq!(suspicion_timeout_ms(min, max, 0, 3), max);
let t1 = suspicion_timeout_ms(min, max, 1, 3);
let t2 = suspicion_timeout_ms(min, max, 2, 3);
let t3 = suspicion_timeout_ms(min, max, 3, 3);
assert!(t1 < max);
assert!(t2 < t1);
assert!(t3 <= t2);
assert_eq!(suspicion_timeout_ms(min, max, 3, 3), min);
assert_eq!(suspicion_timeout_ms(min, max, 10, 3), min);
}
#[test]
fn suspicion_timeout_handles_degenerate_bounds() {
assert_eq!(suspicion_timeout_ms(5000, 1000, 0, 0), 5000);
assert_eq!(suspicion_timeout_ms(5000, 1000, 5, 0), 5000);
}
}