use std::collections::VecDeque;
pub struct HealthRing {
window: usize,
entries: VecDeque<(u64, u64, u64)>,
}
impl HealthRing {
pub fn new(window: usize) -> Self {
Self {
window,
entries: VecDeque::new(),
}
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn observe(&mut self, height: u64, total: u64, found: u64) {
if let Some(&(last_height, _, _)) = self.entries.back() {
if height <= last_height {
return;
}
if height > last_height.saturating_add(1) {
self.entries.clear();
}
}
self.entries.push_back((height, total, found));
while self.entries.len() > self.window {
self.entries.pop_front();
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub(crate) fn entries(&self) -> Vec<(u64, u64, u64)> {
self.entries.iter().copied().collect()
}
pub(crate) fn restore(&mut self, saved: &[(u64, u64, u64)]) {
self.entries.clear();
let skip = saved.len().saturating_sub(self.window);
self.entries.extend(saved.iter().skip(skip).copied());
}
pub fn aggregate_ratio(&self) -> f64 {
let mut found = 0u128;
let mut total = 0u128;
for &(_, block_total, block_found) in &self.entries {
found = found.saturating_add(u128::from(block_found));
total = total.saturating_add(u128::from(block_total));
}
if total == 0 {
0.0
} else {
(found as f64) / (total as f64)
}
}
pub fn is_healthy(&self, min_ratio: f64) -> bool {
if self.window == 0 || self.entries.len() != self.window {
return false;
}
let mut found = 0u128;
let mut total = 0u128;
for &(_, block_total, block_found) in &self.entries {
found = found.saturating_add(u128::from(block_found));
total = total.saturating_add(u128::from(block_total));
}
ratio_met(found, total, min_ratio)
}
}
pub fn health_required(configured: Option<bool>, network_is_regtest: bool) -> bool {
match configured {
Some(required) => required,
None => !network_is_regtest,
}
}
pub fn mempool_layer_trusted(ring: &HealthRing, min_ratio: f64, required: bool) -> bool {
!required || ring.is_healthy(min_ratio)
}
fn ratio_met(found: u128, total: u128, min_ratio: f64) -> bool {
if !min_ratio.is_finite() || !(0.0..=1.0).contains(&min_ratio) {
return false;
}
let pct = (min_ratio * 100.0).round();
if !(0.0..=100.0).contains(&pct) {
return false;
}
found.saturating_mul(100) >= (pct as u128).saturating_mul(total)
}
#[cfg(test)]
mod tests {
use super::*;
fn fill(ring: &mut HealthRing, start: u64, count: u64, total: u64, found: u64) {
for step in 0..count {
ring.observe(start + step, total, found);
}
}
#[test]
fn not_healthy_until_the_window_is_full() {
let mut ring = HealthRing::new(3);
fill(&mut ring, 1, 2, 100, 100);
assert!(!ring.is_healthy(0.75));
ring.observe(3, 100, 100);
assert!(ring.is_healthy(0.75));
assert!(mempool_layer_trusted(&ring, 0.75, true));
}
#[test]
fn flips_at_exactly_the_configured_ratio() {
let mut exact = HealthRing::new(1);
exact.observe(1, 100, 75);
assert!(exact.is_healthy(0.75));
let mut below = HealthRing::new(1);
below.observe(1, 100, 74);
assert!(!below.is_healthy(0.75));
let mut aggregate = HealthRing::new(2);
aggregate.observe(1, 100, 100);
aggregate.observe(2, 100, 50);
assert!(aggregate.is_healthy(0.75));
aggregate.observe(3, 100, 49);
assert_eq!(aggregate.len(), 2);
assert!(!aggregate.is_healthy(0.75));
}
#[test]
fn a_height_gap_clears_the_ring_and_a_repeat_does_not() {
let mut ring = HealthRing::new(2);
fill(&mut ring, 10, 2, 100, 100);
assert!(ring.is_healthy(0.75));
ring.observe(11, 1, 1);
assert!(ring.is_healthy(0.75));
ring.observe(13, 100, 100);
assert_eq!(ring.len(), 1);
assert!(!ring.is_healthy(0.75));
ring.observe(14, 100, 100);
assert!(ring.is_healthy(0.75));
}
#[test]
fn an_unrequired_gate_passes_on_an_empty_ring() {
let ring = HealthRing::new(6);
assert!(!health_required(None, true));
assert!(health_required(None, false));
assert!(health_required(Some(true), true));
assert!(!health_required(Some(false), false));
assert!(mempool_layer_trusted(
&ring,
0.75,
health_required(None, true)
));
assert!(!mempool_layer_trusted(
&ring,
0.75,
health_required(None, false)
));
assert!(mempool_layer_trusted(
&ring,
0.75,
health_required(Some(false), false)
));
}
}