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.
372 lines
11 KiB
Rust
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);
|
|
}
|
|
}
|