synor/crates/synor-dag/src/latency.rs
Gulshan Yadav e2a6a10bee feat(dag): implement DAGKnight adaptive consensus protocol
Phase 13 Milestone 1 - DAGKnight Protocol Implementation:

- Add LatencyTracker for network propagation delay measurement
  - Rolling statistics (mean, stddev, P95, P99)
  - Anticone growth rate tracking
  - Configurable sample window (1000 samples)

- Implement DagKnightManager extending GHOSTDAG
  - Adaptive k parameter based on observed network latency
  - Probabilistic confirmation time estimation
  - Confidence levels (Low/Medium/High/VeryHigh)
  - ConfirmationStatus with depth and finality tracking

- Add BlockRateConfig for throughput scaling
  - Standard: 10 BPS (100ms block time) - current
  - Enhanced: 32 BPS (31ms block time) - target
  - Maximum: 100 BPS (10ms block time) - stretch goal
  - Auto-adjusted merge/finality/pruning depths per config

- Utility functions for network analysis
  - calculate_optimal_k() for k parameter optimization
  - estimate_throughput() for TPS projection

Based on DAGKnight paper (2022) and Kaspa 2025 roadmap.
2026-01-19 09:46:50 +05:30

372 lines
11 KiB
Rust

//! 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<VecDeque<LatencySample>>,
/// Cached statistics (recomputed on demand).
stats_cache: RwLock<Option<(Instant, LatencyStats)>>,
/// 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<f64> = 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::<f64>() / 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);
}
}