//! Network latency tracking for DAGKnight adaptive consensus. //! //! This module tracks observed network propagation delays to enable //! DAGKnight's adaptive k parameter calculation. Unlike GHOSTDAG's //! fixed k assumption, DAGKnight adjusts based on real-world conditions. //! //! # Key Metrics //! //! - **Block propagation delay**: Time from block creation to network-wide visibility //! - **Anticone growth rate**: How quickly anticones grow (indicates network latency) //! - **Confirmation velocity**: Rate at which blocks achieve probabilistic finality use parking_lot::RwLock; use std::collections::VecDeque; use std::time::{Duration, Instant}; use crate::BlockId; /// Maximum number of latency samples to keep for moving average. const MAX_LATENCY_SAMPLES: usize = 1000; /// Default network delay assumption in milliseconds. const DEFAULT_DELAY_MS: u64 = 100; /// Minimum delay to prevent unrealistic values. const MIN_DELAY_MS: u64 = 10; /// Maximum delay to cap at reasonable network conditions. const MAX_DELAY_MS: u64 = 5000; /// Latency sample from observed block propagation. #[derive(Clone, Debug)] pub struct LatencySample { /// Block that was observed. pub block_id: BlockId, /// Timestamp when block was first seen locally. pub local_time: Instant, /// Timestamp from block header (creation time). pub block_time_ms: u64, /// Observed propagation delay in milliseconds. pub delay_ms: u64, /// Anticone size at time of observation. pub anticone_size: usize, } /// Rolling statistics for latency measurements. #[derive(Clone, Debug, Default)] pub struct LatencyStats { /// Mean propagation delay (ms). pub mean_delay_ms: f64, /// Standard deviation of delay (ms). pub std_dev_ms: f64, /// 95th percentile delay (ms). pub p95_delay_ms: f64, /// 99th percentile delay (ms). pub p99_delay_ms: f64, /// Average anticone growth rate (blocks per second). pub anticone_growth_rate: f64, /// Number of samples in current window. pub sample_count: usize, } /// Network latency tracker for DAGKnight. /// /// Collects block propagation samples and computes statistics /// used for adaptive k calculation and confirmation time estimation. pub struct LatencyTracker { /// Recent latency samples. samples: RwLock>, /// Cached statistics (recomputed on demand). stats_cache: RwLock>, /// Cache validity duration. cache_ttl: Duration, } impl LatencyTracker { /// Creates a new latency tracker. pub fn new() -> Self { Self { samples: RwLock::new(VecDeque::with_capacity(MAX_LATENCY_SAMPLES)), stats_cache: RwLock::new(None), cache_ttl: Duration::from_secs(5), } } /// Creates a latency tracker with custom cache TTL. pub fn with_cache_ttl(cache_ttl: Duration) -> Self { Self { samples: RwLock::new(VecDeque::with_capacity(MAX_LATENCY_SAMPLES)), stats_cache: RwLock::new(None), cache_ttl, } } /// Records a new block observation. /// /// # Arguments /// * `block_id` - Hash of the observed block /// * `block_time_ms` - Timestamp from block header (Unix ms) /// * `anticone_size` - Number of blocks in the anticone at observation time pub fn record_block( &self, block_id: BlockId, block_time_ms: u64, anticone_size: usize, ) { let local_time = Instant::now(); let now_ms = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); // Calculate observed delay (clamp to valid range) let delay_ms = if now_ms > block_time_ms { (now_ms - block_time_ms).clamp(MIN_DELAY_MS, MAX_DELAY_MS) } else { // Clock skew - use default DEFAULT_DELAY_MS }; let sample = LatencySample { block_id, local_time, block_time_ms, delay_ms, anticone_size, }; let mut samples = self.samples.write(); if samples.len() >= MAX_LATENCY_SAMPLES { samples.pop_front(); } samples.push_back(sample); // Invalidate stats cache *self.stats_cache.write() = None; } /// Records a latency sample directly (for testing or external measurements). pub fn record_sample(&self, sample: LatencySample) { let mut samples = self.samples.write(); if samples.len() >= MAX_LATENCY_SAMPLES { samples.pop_front(); } samples.push_back(sample); // Invalidate stats cache *self.stats_cache.write() = None; } /// Gets current latency statistics. /// /// Uses cached value if available and fresh, otherwise recomputes. pub fn get_stats(&self) -> LatencyStats { // Check cache { let cache = self.stats_cache.read(); if let Some((cached_at, stats)) = cache.as_ref() { if cached_at.elapsed() < self.cache_ttl { return stats.clone(); } } } // Recompute statistics let stats = self.compute_stats(); // Update cache *self.stats_cache.write() = Some((Instant::now(), stats.clone())); stats } /// Computes latency statistics from current samples. fn compute_stats(&self) -> LatencyStats { let samples = self.samples.read(); if samples.is_empty() { return LatencyStats { mean_delay_ms: DEFAULT_DELAY_MS as f64, std_dev_ms: 0.0, p95_delay_ms: DEFAULT_DELAY_MS as f64, p99_delay_ms: DEFAULT_DELAY_MS as f64, anticone_growth_rate: 0.0, sample_count: 0, }; } let n = samples.len(); // Collect delay values let mut delays: Vec = samples.iter().map(|s| s.delay_ms as f64).collect(); delays.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); // Mean let sum: f64 = delays.iter().sum(); let mean = sum / n as f64; // Standard deviation let variance: f64 = delays.iter().map(|d| (d - mean).powi(2)).sum::() / n as f64; let std_dev = variance.sqrt(); // Percentiles let p95_idx = ((n as f64 * 0.95) as usize).min(n - 1); let p99_idx = ((n as f64 * 0.99) as usize).min(n - 1); // Anticone growth rate (blocks per second) let anticone_growth_rate = if n > 1 { let first = samples.front().unwrap(); let last = samples.back().unwrap(); let time_span_secs = last.local_time.duration_since(first.local_time).as_secs_f64(); if time_span_secs > 0.0 { let total_anticone_growth: usize = samples.iter().map(|s| s.anticone_size).sum(); total_anticone_growth as f64 / time_span_secs / n as f64 } else { 0.0 } } else { 0.0 }; LatencyStats { mean_delay_ms: mean, std_dev_ms: std_dev, p95_delay_ms: delays[p95_idx], p99_delay_ms: delays[p99_idx], anticone_growth_rate, sample_count: n, } } /// Estimates the network delay for adaptive k calculation. /// /// Uses P95 delay as a conservative estimate to ensure security. pub fn estimated_network_delay(&self) -> Duration { let stats = self.get_stats(); Duration::from_millis(stats.p95_delay_ms as u64) } /// Estimates the expected anticone size for a given delay. /// /// Used by DAGKnight to predict confirmation times. pub fn expected_anticone_size(&self, delay: Duration) -> usize { let stats = self.get_stats(); let delay_secs = delay.as_secs_f64(); // Anticone grows at approximately anticone_growth_rate blocks/second (stats.anticone_growth_rate * delay_secs).ceil() as usize } /// Gets the number of samples currently tracked. pub fn sample_count(&self) -> usize { self.samples.read().len() } /// Clears all samples and resets the tracker. pub fn reset(&self) { self.samples.write().clear(); *self.stats_cache.write() = None; } } impl Default for LatencyTracker { fn default() -> Self { Self::new() } } impl std::fmt::Debug for LatencyTracker { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let stats = self.get_stats(); f.debug_struct("LatencyTracker") .field("sample_count", &stats.sample_count) .field("mean_delay_ms", &stats.mean_delay_ms) .field("p95_delay_ms", &stats.p95_delay_ms) .finish() } } #[cfg(test)] mod tests { use super::*; use synor_types::Hash256; fn make_block_id(n: u8) -> BlockId { let mut bytes = [0u8; 32]; bytes[0] = n; Hash256::from_bytes(bytes) } #[test] fn test_empty_tracker() { let tracker = LatencyTracker::new(); let stats = tracker.get_stats(); assert_eq!(stats.sample_count, 0); assert_eq!(stats.mean_delay_ms, DEFAULT_DELAY_MS as f64); } #[test] fn test_record_samples() { let tracker = LatencyTracker::new(); let now_ms = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64; // Record some samples with varying delays for i in 0..10 { tracker.record_block( make_block_id(i), now_ms - (50 + i as u64 * 10), // 50-140ms delays i as usize, ); } let stats = tracker.get_stats(); assert_eq!(stats.sample_count, 10); assert!(stats.mean_delay_ms >= 50.0); assert!(stats.mean_delay_ms <= 150.0); } #[test] fn test_sample_limit() { let tracker = LatencyTracker::new(); let now_ms = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64; // Record more than MAX_LATENCY_SAMPLES for i in 0..MAX_LATENCY_SAMPLES + 100 { tracker.record_block(make_block_id(i as u8), now_ms - 100, 0); } assert_eq!(tracker.sample_count(), MAX_LATENCY_SAMPLES); } #[test] fn test_estimated_delay() { let tracker = LatencyTracker::new(); let now_ms = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64; // Record samples with ~100ms delay for i in 0..50 { tracker.record_block(make_block_id(i), now_ms - 100, 0); } let delay = tracker.estimated_network_delay(); assert!(delay.as_millis() >= 90); assert!(delay.as_millis() <= 200); } #[test] fn test_reset() { let tracker = LatencyTracker::new(); let now_ms = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64; tracker.record_block(make_block_id(0), now_ms - 100, 0); assert_eq!(tracker.sample_count(), 1); tracker.reset(); assert_eq!(tracker.sample_count(), 0); } }