Skip to main content

ipfrs_network/
routing_table_manager.rs

1//! Kademlia-style XOR-metric routing table manager for P2P overlay networks.
2//!
3//! Implements a full 256-bucket Kademlia routing table where each bucket stores
4//! at most `k` entries (default 20) sorted by last-seen time.  A replacement
5//! cache holds candidates that were not admitted because the bucket was full.
6//!
7//! ## Design highlights
8//!
9//! * `NodeId([u8; 32])` — 256-bit node identifier (SHA-256 hash of public key).
10//! * `BucketEntry` — information about a known remote node.
11//! * `KBucket` — a single k-bucket with a replacement cache (`VecDeque`).
12//! * `RoutingTableManager` — 256 k-buckets indexed by the leading-zero-bit
13//!   count of `XOR(local_id, target_id)`.
14//!
15//! ## Thread safety
16//!
17//! `RoutingTableManager` is *not* `Sync` by design; callers that need shared
18//! access should wrap it in `parking_lot::RwLock` or `tokio::sync::RwLock`.
19//!
20//! ## Example
21//!
22//! ```rust
23//! use ipfrs_network::routing_table_manager::{NodeId, RoutingTableManager};
24//!
25//! let local = NodeId([0u8; 32]);
26//! let mut rtm = RoutingTableManager::new(local, 20, 3);
27//!
28//! let mut target_bytes = [0u8; 32];
29//! target_bytes[0] = 0x80; // distance bucket 0
30//! let target = NodeId(target_bytes);
31//!
32//! rtm.add_node(target, "127.0.0.1:4001".to_string(), 5, 1000);
33//! assert_eq!(rtm.total_nodes(), 1);
34//! ```
35
36use std::collections::VecDeque;
37
38// ────────────────────────────────────────────────────────────────────────────
39// Constants
40// ────────────────────────────────────────────────────────────────────────────
41
42/// Default k-bucket size (standard Kademlia value).
43pub const DEFAULT_K: usize = 20;
44
45/// Default alpha concurrency parameter.
46pub const DEFAULT_ALPHA: usize = 3;
47
48/// Maximum number of entries kept in a bucket's replacement cache.
49pub const DEFAULT_REPLACEMENT_CACHE_SIZE: usize = 20;
50
51/// Maximum number of failed queries before a node is evicted.
52const MAX_FAILED_QUERIES: u32 = 3;
53
54/// Number of k-buckets (one per bit of the 256-bit node ID).
55const NUM_BUCKETS: usize = 256;
56
57// ────────────────────────────────────────────────────────────────────────────
58// NodeId
59// ────────────────────────────────────────────────────────────────────────────
60
61/// 256-bit node identifier, typically the SHA-256 hash of a public key.
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
63pub struct NodeId(pub [u8; 32]);
64
65impl NodeId {
66    /// Create a new `NodeId` from raw bytes.
67    pub fn new(bytes: [u8; 32]) -> Self {
68        Self(bytes)
69    }
70
71    /// Borrow the underlying byte slice.
72    pub fn as_bytes(&self) -> &[u8; 32] {
73        &self.0
74    }
75
76    /// Return the XOR distance between `self` and `other`.
77    pub fn xor_distance(&self, other: &NodeId) -> [u8; 32] {
78        xor_distance(self, other)
79    }
80}
81
82impl std::fmt::Display for NodeId {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        for byte in &self.0 {
85            write!(f, "{byte:02x}")?;
86        }
87        Ok(())
88    }
89}
90
91// ────────────────────────────────────────────────────────────────────────────
92// BucketEntry
93// ────────────────────────────────────────────────────────────────────────────
94
95/// Metadata stored for every known remote node.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct BucketEntry {
98    /// The remote node's 256-bit identifier.
99    pub node_id: NodeId,
100    /// Network address (e.g. `/ip4/1.2.3.4/tcp/4001`).
101    pub address: String,
102    /// Unix timestamp (seconds) when this entry was last contacted.
103    pub last_seen: u64,
104    /// Observed round-trip time in milliseconds.
105    pub rtt_ms: u64,
106    /// Number of consecutive failed queries to this node.
107    pub failed_queries: u32,
108}
109
110impl BucketEntry {
111    /// Construct a fresh bucket entry.
112    pub fn new(node_id: NodeId, address: String, rtt_ms: u64, last_seen: u64) -> Self {
113        Self {
114            node_id,
115            address,
116            last_seen,
117            rtt_ms,
118            failed_queries: 0,
119        }
120    }
121}
122
123// ────────────────────────────────────────────────────────────────────────────
124// KBucket
125// ────────────────────────────────────────────────────────────────────────────
126
127/// A single Kademlia k-bucket.
128///
129/// Entries are stored in LRU order: index 0 is the *least*-recently-seen node,
130/// index `len-1` is the *most*-recently-seen node.  When the bucket is full a
131/// newly observed node goes into `replacement_cache` rather than the bucket.
132#[derive(Clone, Debug)]
133pub struct KBucket {
134    /// Active entries, ordered from least-recently-seen (front) to
135    /// most-recently-seen (back).
136    pub entries: Vec<BucketEntry>,
137    /// Candidates waiting to replace evicted entries (FIFO).
138    pub replacement_cache: VecDeque<BucketEntry>,
139    /// Maximum number of active entries.
140    pub max_size: usize,
141}
142
143impl KBucket {
144    /// Create an empty k-bucket with the given maximum size.
145    pub fn new(max_size: usize) -> Self {
146        Self {
147            entries: Vec::with_capacity(max_size),
148            replacement_cache: VecDeque::new(),
149            max_size,
150        }
151    }
152
153    /// Returns `true` if the bucket holds no active entries.
154    pub fn is_empty(&self) -> bool {
155        self.entries.is_empty()
156    }
157
158    /// Returns the number of active entries.
159    pub fn len(&self) -> usize {
160        self.entries.len()
161    }
162
163    /// Returns `true` if the active-entry list is at capacity.
164    pub fn is_full(&self) -> bool {
165        self.entries.len() >= self.max_size
166    }
167
168    /// Find the position of `node_id` in the active entries, if present.
169    fn find_entry_index(&self, node_id: &NodeId) -> Option<usize> {
170        self.entries.iter().position(|e| &e.node_id == node_id)
171    }
172
173    /// Find the position of `node_id` in the replacement cache, if present.
174    fn find_cache_index(&self, node_id: &NodeId) -> Option<usize> {
175        self.replacement_cache
176            .iter()
177            .position(|e| &e.node_id == node_id)
178    }
179
180    /// Insert or refresh a node in the bucket.
181    ///
182    /// * If the node already exists in active entries → move to tail (LRU update).
183    /// * Else if the bucket is not full → append to tail.
184    /// * Else if the node is in the replacement cache → update it there.
185    /// * Else → push to the back of the replacement cache (bounded by
186    ///   `DEFAULT_REPLACEMENT_CACHE_SIZE`).
187    pub fn add(&mut self, entry: BucketEntry) {
188        if let Some(idx) = self.find_entry_index(&entry.node_id) {
189            // Refresh existing active entry and move it to the tail.
190            let mut existing = self.entries.remove(idx);
191            existing.last_seen = entry.last_seen;
192            existing.rtt_ms = entry.rtt_ms;
193            existing.failed_queries = 0; // successful contact resets failures
194            self.entries.push(existing);
195            return;
196        }
197
198        if !self.is_full() {
199            self.entries.push(entry);
200            return;
201        }
202
203        // Bucket is full — manage replacement cache.
204        if let Some(idx) = self.find_cache_index(&entry.node_id) {
205            // Update existing cache entry in place.
206            if let Some(cached) = self.replacement_cache.get_mut(idx) {
207                cached.last_seen = entry.last_seen;
208                cached.rtt_ms = entry.rtt_ms;
209                cached.failed_queries = 0;
210            }
211        } else {
212            // Evict the oldest cache entry if at capacity.
213            if self.replacement_cache.len() >= DEFAULT_REPLACEMENT_CACHE_SIZE {
214                self.replacement_cache.pop_front();
215            }
216            self.replacement_cache.push_back(entry);
217        }
218    }
219
220    /// Remove the node with the given ID from active entries.
221    ///
222    /// If a replacement candidate exists in the cache it is promoted.
223    ///
224    /// Returns `true` if the node was found and removed.
225    pub fn remove(&mut self, node_id: &NodeId) -> bool {
226        if let Some(idx) = self.find_entry_index(node_id) {
227            self.entries.remove(idx);
228            // Promote the freshest replacement (back of the cache).
229            if let Some(replacement) = self.replacement_cache.pop_back() {
230                self.entries.push(replacement);
231            }
232            true
233        } else {
234            false
235        }
236    }
237
238    /// Increment the failure counter for a node.
239    ///
240    /// Returns `true` if the node was evicted due to too many failures.
241    pub fn mark_failed(&mut self, node_id: &NodeId, now: u64) -> bool {
242        if let Some(idx) = self.find_entry_index(node_id) {
243            self.entries[idx].failed_queries += 1;
244            if self.entries[idx].failed_queries >= MAX_FAILED_QUERIES {
245                self.entries.remove(idx);
246                // Promote replacement if available.
247                if let Some(replacement) = self.replacement_cache.pop_back() {
248                    self.entries.push(replacement);
249                }
250                return true;
251            }
252            // Update last_seen even on failure to avoid stale timestamps.
253            self.entries[idx].last_seen = now;
254        }
255        false
256    }
257
258    /// Update the RTT and last-seen timestamp for a node.
259    pub fn update_rtt(&mut self, node_id: &NodeId, rtt_ms: u64, now: u64) {
260        if let Some(idx) = self.find_entry_index(node_id) {
261            self.entries[idx].rtt_ms = rtt_ms;
262            self.entries[idx].last_seen = now;
263            self.entries[idx].failed_queries = 0;
264        }
265    }
266}
267
268// ────────────────────────────────────────────────────────────────────────────
269// Routing table statistics
270// ────────────────────────────────────────────────────────────────────────────
271
272/// Aggregate statistics for the routing table.
273#[derive(Clone, Debug)]
274pub struct RoutingTableStats {
275    /// Total number of active entries across all buckets.
276    pub total_nodes: usize,
277    /// Number of buckets that hold at least one entry.
278    pub non_empty_buckets: usize,
279    /// Average RTT (ms) across all active entries, or 0 if the table is empty.
280    pub avg_rtt_ms: f64,
281    /// Size of the largest bucket.
282    pub max_bucket_size: usize,
283}
284
285// ────────────────────────────────────────────────────────────────────────────
286// Free-standing distance helpers
287// ────────────────────────────────────────────────────────────────────────────
288
289/// Compute the 256-bit XOR distance between two node IDs.
290pub fn xor_distance(a: &NodeId, b: &NodeId) -> [u8; 32] {
291    let mut dist = [0u8; 32];
292    for (i, byte) in dist.iter_mut().enumerate() {
293        *byte = a.0[i] ^ b.0[i];
294    }
295    dist
296}
297
298/// Return the index of the k-bucket for `target` relative to `local`.
299///
300/// The index equals the number of *leading zero bits* in `XOR(local, target)`,
301/// capped at 255.  A result of 255 means the two IDs are identical (distance 0)
302/// and the target is the local node itself.
303pub fn bucket_index(local: &NodeId, target: &NodeId) -> usize {
304    let dist = xor_distance(local, target);
305    let mut leading = 0usize;
306    for byte in &dist {
307        if *byte == 0 {
308            leading += 8;
309        } else {
310            leading += byte.leading_zeros() as usize;
311            break;
312        }
313    }
314    leading.min(255)
315}
316
317// ────────────────────────────────────────────────────────────────────────────
318// RoutingTableManager
319// ────────────────────────────────────────────────────────────────────────────
320
321/// Kademlia-style XOR-metric routing table manager.
322///
323/// Manages 256 k-buckets (one per bit of the 256-bit node-ID space).  Bucket
324/// `i` holds nodes whose XOR distance to the local node starts with exactly
325/// `i` leading zero bits.
326///
327/// # Parameters
328///
329/// * `k` — maximum number of active entries per bucket (default: 20).
330/// * `alpha` — concurrency factor for parallel lookups (default: 3; stored
331///   but not used internally — callers read it when launching lookups).
332pub struct RoutingTableManager {
333    /// The 256 k-buckets.
334    pub buckets: Vec<KBucket>,
335    /// This node's own identifier.
336    pub local_id: NodeId,
337    /// Maximum entries per bucket.
338    pub k: usize,
339    /// Lookup concurrency parameter.
340    pub alpha: usize,
341}
342
343impl RoutingTableManager {
344    /// Create a new routing table manager.
345    ///
346    /// # Arguments
347    ///
348    /// * `local_id` — this node's 256-bit identifier.
349    /// * `k` — k-bucket size (use `DEFAULT_K` for the standard Kademlia value).
350    /// * `alpha` — concurrency factor (use `DEFAULT_ALPHA` for the standard value).
351    pub fn new(local_id: NodeId, k: usize, alpha: usize) -> Self {
352        let k = k.max(1); // guard against a degenerate k=0
353        let buckets = (0..NUM_BUCKETS).map(|_| KBucket::new(k)).collect();
354        Self {
355            buckets,
356            local_id,
357            k,
358            alpha,
359        }
360    }
361
362    /// Create a routing table manager with default parameters (`k=20`, `alpha=3`).
363    pub fn with_defaults(local_id: NodeId) -> Self {
364        Self::new(local_id, DEFAULT_K, DEFAULT_ALPHA)
365    }
366
367    // ── Routing table mutations ───────────────────────────────────────────────
368
369    /// Insert or refresh a node in the routing table.
370    ///
371    /// * If the node already appears in its bucket → LRU update (move to tail).
372    /// * If the bucket is not full → append.
373    /// * If the bucket is full → put into the replacement cache.
374    ///
375    /// Silently ignores attempts to add the local node itself.
376    pub fn add_node(&mut self, node_id: NodeId, address: String, rtt_ms: u64, now: u64) {
377        if node_id == self.local_id {
378            return;
379        }
380        let idx = bucket_index(&self.local_id, &node_id);
381        let entry = BucketEntry::new(node_id, address, rtt_ms, now);
382        self.buckets[idx].add(entry);
383    }
384
385    /// Remove a node from the routing table.
386    ///
387    /// If the node was in a bucket's active list a replacement from the cache
388    /// is promoted automatically.
389    ///
390    /// Returns `true` if the node was found in an active bucket.
391    pub fn remove_node(&mut self, node_id: &NodeId) -> bool {
392        let idx = bucket_index(&self.local_id, node_id);
393        self.buckets[idx].remove(node_id)
394    }
395
396    /// Record a failed query for a node.
397    ///
398    /// After `MAX_FAILED_QUERIES` (3) consecutive failures the node is evicted
399    /// and a replacement is promoted from the cache.
400    pub fn mark_failed(&mut self, node_id: &NodeId, now: u64) {
401        let idx = bucket_index(&self.local_id, node_id);
402        self.buckets[idx].mark_failed(node_id, now);
403    }
404
405    /// Update the RTT observation and last-seen timestamp for a node.
406    pub fn update_rtt(&mut self, node_id: &NodeId, rtt_ms: u64, now: u64) {
407        let idx = bucket_index(&self.local_id, node_id);
408        self.buckets[idx].update_rtt(node_id, rtt_ms, now);
409    }
410
411    // ── Lookup ────────────────────────────────────────────────────────────────
412
413    /// Return up to `count` entries closest to `target` measured by XOR distance.
414    ///
415    /// The result is sorted ascending by XOR distance (closest first).
416    pub fn find_closest(&self, target: &NodeId, count: usize) -> Vec<BucketEntry> {
417        // Collect every active entry together with its XOR distance.
418        let mut candidates: Vec<([u8; 32], BucketEntry)> = self
419            .buckets
420            .iter()
421            .flat_map(|b| b.entries.iter())
422            .map(|e| (xor_distance(target, &e.node_id), e.clone()))
423            .collect();
424
425        // Sort by distance (lexicographic on the 32-byte XOR result is correct).
426        candidates.sort_by_key(|a| a.0);
427        candidates.truncate(count);
428        candidates.into_iter().map(|(_, e)| e).collect()
429    }
430
431    // ── Observation helpers ───────────────────────────────────────────────────
432
433    /// Return the number of active entries in each of the 256 buckets.
434    pub fn bucket_sizes(&self) -> Vec<usize> {
435        self.buckets.iter().map(|b| b.len()).collect()
436    }
437
438    /// Return the total number of active entries across all buckets.
439    pub fn total_nodes(&self) -> usize {
440        self.buckets.iter().map(|b| b.len()).sum()
441    }
442
443    /// Compute aggregate statistics for the routing table.
444    pub fn stats(&self) -> RoutingTableStats {
445        let total_nodes = self.total_nodes();
446        let non_empty_buckets = self.buckets.iter().filter(|b| !b.is_empty()).count();
447        let max_bucket_size = self.buckets.iter().map(|b| b.len()).max().unwrap_or(0);
448
449        let avg_rtt_ms = if total_nodes == 0 {
450            0.0
451        } else {
452            let rtt_sum: u64 = self
453                .buckets
454                .iter()
455                .flat_map(|b| b.entries.iter())
456                .map(|e| e.rtt_ms)
457                .sum();
458            rtt_sum as f64 / total_nodes as f64
459        };
460
461        RoutingTableStats {
462            total_nodes,
463            non_empty_buckets,
464            avg_rtt_ms,
465            max_bucket_size,
466        }
467    }
468
469    // ── Convenience accessors ─────────────────────────────────────────────────
470
471    /// Return the k-bucket index for a given target node ID.
472    pub fn bucket_index_for(&self, target: &NodeId) -> usize {
473        bucket_index(&self.local_id, target)
474    }
475
476    /// Iterate over all active `BucketEntry` records.
477    pub fn iter_entries(&self) -> impl Iterator<Item = &BucketEntry> {
478        self.buckets.iter().flat_map(|b| b.entries.iter())
479    }
480
481    /// Return `true` if the routing table contains no active entries.
482    pub fn is_empty(&self) -> bool {
483        self.buckets.iter().all(|b| b.is_empty())
484    }
485}
486
487// ────────────────────────────────────────────────────────────────────────────
488// Tests
489// ────────────────────────────────────────────────────────────────────────────
490
491#[cfg(test)]
492mod tests {
493    use super::{
494        bucket_index, xor_distance, BucketEntry, KBucket, NodeId, RoutingTableManager, DEFAULT_K,
495        DEFAULT_REPLACEMENT_CACHE_SIZE, MAX_FAILED_QUERIES, NUM_BUCKETS,
496    };
497
498    // ── xorshift64 PRNG (no rand crate) ──────────────────────────────────────
499
500    struct Xorshift64 {
501        state: u64,
502    }
503
504    impl Xorshift64 {
505        fn new(seed: u64) -> Self {
506            // Avoid a zero state which would make the generator degenerate.
507            Self {
508                state: if seed == 0 { 0xdeadbeef_cafebabe } else { seed },
509            }
510        }
511
512        fn next_u64(&mut self) -> u64 {
513            let mut x = self.state;
514            x ^= x << 13;
515            x ^= x >> 7;
516            x ^= x << 17;
517            self.state = x;
518            x
519        }
520
521        fn next_node_id(&mut self) -> NodeId {
522            let mut bytes = [0u8; 32];
523            for chunk in bytes.chunks_exact_mut(8) {
524                let val = self.next_u64().to_le_bytes();
525                chunk.copy_from_slice(&val);
526            }
527            NodeId(bytes)
528        }
529
530        fn next_rtt(&mut self) -> u64 {
531            (self.next_u64() % 500) + 1
532        }
533
534        fn next_ts(&mut self) -> u64 {
535            (self.next_u64() % 1_000_000) + 1_000_000
536        }
537    }
538
539    // ── Helper constructors ───────────────────────────────────────────────────
540
541    fn zero_id() -> NodeId {
542        NodeId([0u8; 32])
543    }
544
545    /// Build a NodeId where byte `byte_idx` has `value`, all others 0.
546    fn id_with_byte(byte_idx: usize, value: u8) -> NodeId {
547        let mut bytes = [0u8; 32];
548        bytes[byte_idx] = value;
549        NodeId(bytes)
550    }
551
552    fn make_rtm(k: usize) -> RoutingTableManager {
553        RoutingTableManager::new(zero_id(), k, 3)
554    }
555
556    fn dummy_entry(id: NodeId, rtt: u64, ts: u64) -> BucketEntry {
557        BucketEntry::new(id, format!("127.0.0.1:{}", rtt), rtt, ts)
558    }
559
560    // ── 1. xor_distance basics ────────────────────────────────────────────────
561
562    #[test]
563    fn test_xor_distance_identical() {
564        let id = NodeId([0xABu8; 32]);
565        let dist = xor_distance(&id, &id);
566        assert_eq!(dist, [0u8; 32]);
567    }
568
569    #[test]
570    fn test_xor_distance_complementary() {
571        let a = NodeId([0xFF; 32]);
572        let b = NodeId([0x00; 32]);
573        let dist = xor_distance(&a, &b);
574        assert_eq!(dist, [0xFF; 32]);
575    }
576
577    #[test]
578    fn test_xor_distance_symmetry() {
579        let mut rng = Xorshift64::new(42);
580        let a = rng.next_node_id();
581        let b = rng.next_node_id();
582        assert_eq!(xor_distance(&a, &b), xor_distance(&b, &a));
583    }
584
585    #[test]
586    fn test_xor_distance_triangle_inequality() {
587        // XOR metric satisfies d(a,c) <= d(a,b) XOR d(b,c) byte-wise.
588        // Simpler check: d(a,a) == 0.
589        let mut rng = Xorshift64::new(7);
590        let a = rng.next_node_id();
591        assert_eq!(xor_distance(&a, &a), [0u8; 32]);
592    }
593
594    // ── 2. bucket_index ───────────────────────────────────────────────────────
595
596    #[test]
597    fn test_bucket_index_equal_ids() {
598        let id = NodeId([0u8; 32]);
599        // XOR is all zeros → 256 leading zeros, capped at 255
600        assert_eq!(bucket_index(&id, &id), 255);
601    }
602
603    #[test]
604    fn test_bucket_index_first_bit_differs() {
605        let local = zero_id();
606        let mut target_bytes = [0u8; 32];
607        target_bytes[0] = 0x80; // MSB set → XOR has MSB set → 0 leading zeros
608        let target = NodeId(target_bytes);
609        assert_eq!(bucket_index(&local, &target), 0);
610    }
611
612    #[test]
613    fn test_bucket_index_second_bit_differs() {
614        let local = zero_id();
615        let mut target_bytes = [0u8; 32];
616        target_bytes[0] = 0x40; // second bit set → 1 leading zero
617        let target = NodeId(target_bytes);
618        assert_eq!(bucket_index(&local, &target), 1);
619    }
620
621    #[test]
622    fn test_bucket_index_second_byte() {
623        let local = zero_id();
624        let mut target_bytes = [0u8; 32];
625        target_bytes[1] = 0x80; // first byte zero, second byte MSB → 8 leading zeros
626        let target = NodeId(target_bytes);
627        assert_eq!(bucket_index(&local, &target), 8);
628    }
629
630    #[test]
631    fn test_bucket_index_last_byte() {
632        let local = zero_id();
633        let mut target_bytes = [0u8; 32];
634        target_bytes[31] = 0x01; // only last bit differs → 255 leading zeros, but capped
635        let target = NodeId(target_bytes);
636        assert_eq!(bucket_index(&local, &target), 255);
637    }
638
639    // ── 3. KBucket – add ─────────────────────────────────────────────────────
640
641    #[test]
642    fn test_kbucket_add_up_to_capacity() {
643        let mut bucket = KBucket::new(3);
644        for i in 0..3u8 {
645            let id = id_with_byte(0, i + 1);
646            bucket.add(dummy_entry(id, 10, 1000));
647        }
648        assert_eq!(bucket.len(), 3);
649        assert!(bucket.replacement_cache.is_empty());
650    }
651
652    #[test]
653    fn test_kbucket_overflow_goes_to_cache() {
654        let mut bucket = KBucket::new(2);
655        bucket.add(dummy_entry(id_with_byte(0, 1), 10, 1000));
656        bucket.add(dummy_entry(id_with_byte(0, 2), 10, 1001));
657        bucket.add(dummy_entry(id_with_byte(0, 3), 10, 1002)); // goes to cache
658        assert_eq!(bucket.len(), 2);
659        assert_eq!(bucket.replacement_cache.len(), 1);
660    }
661
662    #[test]
663    fn test_kbucket_lru_move_to_tail() {
664        let mut bucket = KBucket::new(4);
665        let id_a = id_with_byte(0, 1);
666        let id_b = id_with_byte(0, 2);
667        bucket.add(dummy_entry(id_a, 10, 1000));
668        bucket.add(dummy_entry(id_b, 10, 1001));
669        // Re-add id_a with a newer timestamp
670        bucket.add(dummy_entry(id_a, 20, 2000));
671        // id_a should now be at the tail (index 1)
672        assert_eq!(bucket.entries.last().map(|e| e.node_id), Some(id_a));
673        assert_eq!(bucket.entries.len(), 2);
674    }
675
676    #[test]
677    fn test_kbucket_lru_resets_failures() {
678        let mut bucket = KBucket::new(4);
679        let id = id_with_byte(0, 1);
680        bucket.add(dummy_entry(id, 10, 1000));
681        // Simulate a failure
682        bucket.entries[0].failed_queries = 2;
683        // Re-add the same node (successful contact)
684        bucket.add(dummy_entry(id, 15, 2000));
685        assert_eq!(bucket.entries.last().map(|e| e.failed_queries), Some(0));
686    }
687
688    // ── 4. KBucket – remove ──────────────────────────────────────────────────
689
690    #[test]
691    fn test_kbucket_remove_existing() {
692        let mut bucket = KBucket::new(4);
693        let id = id_with_byte(0, 1);
694        bucket.add(dummy_entry(id, 10, 1000));
695        assert!(bucket.remove(&id));
696        assert!(bucket.is_empty());
697    }
698
699    #[test]
700    fn test_kbucket_remove_absent() {
701        let mut bucket = KBucket::new(4);
702        assert!(!bucket.remove(&id_with_byte(0, 99)));
703    }
704
705    #[test]
706    fn test_kbucket_remove_promotes_replacement() {
707        let mut bucket = KBucket::new(2);
708        let id_a = id_with_byte(0, 1);
709        let id_b = id_with_byte(0, 2);
710        let id_c = id_with_byte(0, 3); // will go to cache
711        bucket.add(dummy_entry(id_a, 10, 1000));
712        bucket.add(dummy_entry(id_b, 10, 1001));
713        bucket.add(dummy_entry(id_c, 10, 1002));
714        assert_eq!(bucket.replacement_cache.len(), 1);
715        bucket.remove(&id_a);
716        // id_c should have been promoted
717        assert_eq!(bucket.len(), 2);
718        assert!(bucket.entries.iter().any(|e| e.node_id == id_c));
719        assert!(bucket.replacement_cache.is_empty());
720    }
721
722    // ── 5. KBucket – mark_failed ─────────────────────────────────────────────
723
724    #[test]
725    fn test_kbucket_mark_failed_eviction() {
726        let mut bucket = KBucket::new(4);
727        let id = id_with_byte(0, 1);
728        bucket.add(dummy_entry(id, 10, 1000));
729        // Two failures – still present.
730        bucket.mark_failed(&id, 1001);
731        bucket.mark_failed(&id, 1002);
732        assert!(!bucket.is_empty());
733        // Third failure – evicted.
734        let evicted = bucket.mark_failed(&id, 1003);
735        assert!(evicted);
736        assert!(bucket.is_empty());
737    }
738
739    #[test]
740    fn test_kbucket_mark_failed_promotes_replacement() {
741        let mut bucket = KBucket::new(1);
742        let id_a = id_with_byte(0, 1);
743        let id_b = id_with_byte(0, 2);
744        bucket.add(dummy_entry(id_a, 10, 1000));
745        bucket.add(dummy_entry(id_b, 10, 1001)); // goes to cache
746                                                 // Fail id_a three times
747        for ts in 1..=3u64 {
748            bucket.mark_failed(&id_a, 1000 + ts);
749        }
750        // id_b should have been promoted
751        assert_eq!(bucket.len(), 1);
752        assert_eq!(bucket.entries[0].node_id, id_b);
753    }
754
755    // ── 6. KBucket – update_rtt ──────────────────────────────────────────────
756
757    #[test]
758    fn test_kbucket_update_rtt() {
759        let mut bucket = KBucket::new(4);
760        let id = id_with_byte(0, 1);
761        bucket.add(dummy_entry(id, 100, 1000));
762        bucket.update_rtt(&id, 50, 2000);
763        let entry = bucket
764            .entries
765            .iter()
766            .find(|e| e.node_id == id)
767            .expect("test: entry should be found in bucket after add");
768        assert_eq!(entry.rtt_ms, 50);
769        assert_eq!(entry.last_seen, 2000);
770        assert_eq!(entry.failed_queries, 0);
771    }
772
773    // ── 7. RoutingTableManager – add_node ────────────────────────────────────
774
775    #[test]
776    fn test_rtm_add_node_basic() {
777        let mut rtm = make_rtm(DEFAULT_K);
778        let id = id_with_byte(0, 0x80); // bucket 0
779        rtm.add_node(id, "addr".to_string(), 10, 1000);
780        assert_eq!(rtm.total_nodes(), 1);
781    }
782
783    #[test]
784    fn test_rtm_add_node_ignores_local() {
785        let mut rtm = make_rtm(DEFAULT_K);
786        rtm.add_node(zero_id(), "self".to_string(), 0, 0);
787        assert_eq!(rtm.total_nodes(), 0);
788    }
789
790    #[test]
791    fn test_rtm_add_many_nodes_distributed() {
792        let mut rng = Xorshift64::new(1234);
793        let local = rng.next_node_id();
794        let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
795        for _ in 0..100 {
796            let id = rng.next_node_id();
797            rtm.add_node(id, "a".to_string(), rng.next_rtt(), rng.next_ts());
798        }
799        assert!(rtm.total_nodes() > 0);
800        // Nodes should be spread across multiple buckets.
801        let non_empty = rtm.bucket_sizes().iter().filter(|&&s| s > 0).count();
802        assert!(non_empty > 1, "expected multiple non-empty buckets");
803    }
804
805    // ── 8. RoutingTableManager – remove_node ─────────────────────────────────
806
807    #[test]
808    fn test_rtm_remove_node_present() {
809        let mut rtm = make_rtm(DEFAULT_K);
810        let id = id_with_byte(0, 0x80);
811        rtm.add_node(id, "addr".to_string(), 10, 1000);
812        assert!(rtm.remove_node(&id));
813        assert_eq!(rtm.total_nodes(), 0);
814    }
815
816    #[test]
817    fn test_rtm_remove_node_absent() {
818        let mut rtm = make_rtm(DEFAULT_K);
819        assert!(!rtm.remove_node(&id_with_byte(0, 1)));
820    }
821
822    // ── 9. RoutingTableManager – find_closest ────────────────────────────────
823
824    #[test]
825    fn test_rtm_find_closest_empty() {
826        let rtm = make_rtm(DEFAULT_K);
827        let target = id_with_byte(0, 0x80);
828        let result = rtm.find_closest(&target, 5);
829        assert!(result.is_empty());
830    }
831
832    #[test]
833    fn test_rtm_find_closest_count_limit() {
834        let mut rng = Xorshift64::new(99);
835        let local = rng.next_node_id();
836        let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
837        for _ in 0..50 {
838            let id = rng.next_node_id();
839            rtm.add_node(id, "a".to_string(), rng.next_rtt(), rng.next_ts());
840        }
841        let target = rng.next_node_id();
842        let result = rtm.find_closest(&target, 10);
843        assert!(result.len() <= 10);
844    }
845
846    #[test]
847    fn test_rtm_find_closest_sorted_order() {
848        let mut rng = Xorshift64::new(777);
849        let local = rng.next_node_id();
850        let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
851        let target = rng.next_node_id();
852        for _ in 0..30 {
853            let id = rng.next_node_id();
854            rtm.add_node(id, "a".to_string(), rng.next_rtt(), rng.next_ts());
855        }
856        let result = rtm.find_closest(&target, 10);
857        // Verify ascending XOR distance order.
858        let distances: Vec<[u8; 32]> = result
859            .iter()
860            .map(|e| xor_distance(&target, &e.node_id))
861            .collect();
862        for pair in distances.windows(2) {
863            assert!(pair[0] <= pair[1], "result must be sorted by XOR distance");
864        }
865    }
866
867    #[test]
868    fn test_rtm_find_closest_returns_nearest() {
869        let local = zero_id();
870        let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
871        // id_near is in bucket 0 (MSB differs), id_far is in bucket 1.
872        let id_near = id_with_byte(0, 0x80); // distance starts with 10000000…
873        let id_far = id_with_byte(0, 0x40); // distance starts with 01000000…
874        rtm.add_node(id_near, "near".to_string(), 5, 1000);
875        rtm.add_node(id_far, "far".to_string(), 5, 1000);
876        // When the target is id_near itself, id_near should be returned first.
877        let result = rtm.find_closest(&id_near, 2);
878        assert_eq!(result[0].node_id, id_near);
879    }
880
881    // ── 10. RoutingTableManager – mark_failed ────────────────────────────────
882
883    #[test]
884    fn test_rtm_mark_failed_eviction() {
885        let mut rtm = make_rtm(DEFAULT_K);
886        let id = id_with_byte(0, 0x80);
887        rtm.add_node(id, "a".to_string(), 10, 1000);
888        for ts in 1..=3u64 {
889            rtm.mark_failed(&id, 1000 + ts);
890        }
891        assert_eq!(rtm.total_nodes(), 0);
892    }
893
894    #[test]
895    fn test_rtm_mark_failed_noop_on_absent() {
896        let mut rtm = make_rtm(DEFAULT_K);
897        // Should not panic or change anything.
898        rtm.mark_failed(&id_with_byte(0, 99), 9999);
899        assert_eq!(rtm.total_nodes(), 0);
900    }
901
902    // ── 11. RoutingTableManager – update_rtt ─────────────────────────────────
903
904    #[test]
905    fn test_rtm_update_rtt() {
906        let mut rtm = make_rtm(DEFAULT_K);
907        let id = id_with_byte(0, 0x80);
908        rtm.add_node(id, "a".to_string(), 100, 1000);
909        rtm.update_rtt(&id, 25, 2000);
910        let entry = rtm
911            .iter_entries()
912            .find(|e| e.node_id == id)
913            .expect("test: entry should be found in routing table");
914        assert_eq!(entry.rtt_ms, 25);
915        assert_eq!(entry.last_seen, 2000);
916    }
917
918    #[test]
919    fn test_rtm_update_rtt_noop_on_absent() {
920        let mut rtm = make_rtm(DEFAULT_K);
921        // Should not panic.
922        rtm.update_rtt(&id_with_byte(0, 7), 50, 9999);
923    }
924
925    // ── 12. RoutingTableManager – bucket_sizes / total_nodes ─────────────────
926
927    #[test]
928    fn test_rtm_bucket_sizes_length() {
929        let rtm = make_rtm(DEFAULT_K);
930        assert_eq!(rtm.bucket_sizes().len(), NUM_BUCKETS);
931    }
932
933    #[test]
934    fn test_rtm_total_nodes_zero_on_empty() {
935        let rtm = make_rtm(DEFAULT_K);
936        assert_eq!(rtm.total_nodes(), 0);
937    }
938
939    #[test]
940    fn test_rtm_total_nodes_after_add() {
941        let mut rtm = make_rtm(DEFAULT_K);
942        for i in 1u8..=5 {
943            rtm.add_node(id_with_byte(0, i * 0x10), "a".to_string(), 10, 1000);
944        }
945        assert_eq!(rtm.total_nodes(), 5);
946    }
947
948    // ── 13. RoutingTableManager – stats ──────────────────────────────────────
949
950    #[test]
951    fn test_rtm_stats_empty() {
952        let rtm = make_rtm(DEFAULT_K);
953        let s = rtm.stats();
954        assert_eq!(s.total_nodes, 0);
955        assert_eq!(s.non_empty_buckets, 0);
956        assert_eq!(s.avg_rtt_ms, 0.0);
957        assert_eq!(s.max_bucket_size, 0);
958    }
959
960    #[test]
961    fn test_rtm_stats_single_node() {
962        let mut rtm = make_rtm(DEFAULT_K);
963        rtm.add_node(id_with_byte(0, 0x80), "a".to_string(), 42, 1000);
964        let s = rtm.stats();
965        assert_eq!(s.total_nodes, 1);
966        assert_eq!(s.non_empty_buckets, 1);
967        assert_eq!(s.avg_rtt_ms, 42.0);
968        assert_eq!(s.max_bucket_size, 1);
969    }
970
971    #[test]
972    fn test_rtm_stats_avg_rtt() {
973        let mut rtm = make_rtm(DEFAULT_K);
974        // Two nodes in different buckets with RTTs 100 and 200
975        rtm.add_node(id_with_byte(0, 0x80), "a".to_string(), 100, 1000);
976        rtm.add_node(id_with_byte(0, 0x40), "b".to_string(), 200, 1001);
977        let s = rtm.stats();
978        assert_eq!(s.total_nodes, 2);
979        assert!((s.avg_rtt_ms - 150.0).abs() < f64::EPSILON);
980    }
981
982    // ── 14. Replacement cache bounds ─────────────────────────────────────────
983
984    #[test]
985    fn test_replacement_cache_bounded() {
986        let mut bucket = KBucket::new(1);
987        // Fill the one active slot.
988        bucket.add(dummy_entry(id_with_byte(0, 0), 10, 1000));
989        // Add more than DEFAULT_REPLACEMENT_CACHE_SIZE to the cache.
990        for i in 1u8..=(DEFAULT_REPLACEMENT_CACHE_SIZE as u8 + 10) {
991            bucket.add(dummy_entry(id_with_byte(0, i), 10, 1000));
992        }
993        assert!(
994            bucket.replacement_cache.len() <= DEFAULT_REPLACEMENT_CACHE_SIZE,
995            "replacement cache must not exceed its maximum size"
996        );
997    }
998
999    // ── 15. Multiple replacements ─────────────────────────────────────────────
1000
1001    #[test]
1002    fn test_multiple_replacements_sequential() {
1003        let mut bucket = KBucket::new(2);
1004        let id_a = id_with_byte(0, 1);
1005        let id_b = id_with_byte(0, 2);
1006        let id_c = id_with_byte(0, 3);
1007        let id_d = id_with_byte(0, 4);
1008        bucket.add(dummy_entry(id_a, 10, 1000));
1009        bucket.add(dummy_entry(id_b, 10, 1001));
1010        bucket.add(dummy_entry(id_c, 10, 1002)); // cache
1011        bucket.add(dummy_entry(id_d, 10, 1003)); // cache
1012                                                 // Remove id_a → id_d (back of cache) promoted.
1013        bucket.remove(&id_a);
1014        assert_eq!(bucket.len(), 2);
1015        assert!(bucket.entries.iter().any(|e| e.node_id == id_d));
1016        // Remove id_b → id_c promoted.
1017        bucket.remove(&id_b);
1018        assert_eq!(bucket.len(), 2);
1019        assert!(bucket.entries.iter().any(|e| e.node_id == id_c));
1020    }
1021
1022    // ── 16. RoutingTableManager – is_empty ───────────────────────────────────
1023
1024    #[test]
1025    fn test_rtm_is_empty_initial() {
1026        let rtm = make_rtm(DEFAULT_K);
1027        assert!(rtm.is_empty());
1028    }
1029
1030    #[test]
1031    fn test_rtm_is_not_empty_after_add() {
1032        let mut rtm = make_rtm(DEFAULT_K);
1033        rtm.add_node(id_with_byte(0, 1), "x".to_string(), 10, 0);
1034        assert!(!rtm.is_empty());
1035    }
1036
1037    // ── 17. with_defaults constructor ─────────────────────────────────────────
1038
1039    #[test]
1040    fn test_rtm_with_defaults() {
1041        let rtm = RoutingTableManager::with_defaults(zero_id());
1042        assert_eq!(rtm.k, DEFAULT_K);
1043        assert_eq!(rtm.alpha, super::DEFAULT_ALPHA);
1044        assert_eq!(rtm.buckets.len(), NUM_BUCKETS);
1045    }
1046
1047    // ── 18. bucket_index_for ──────────────────────────────────────────────────
1048
1049    #[test]
1050    fn test_rtm_bucket_index_for_consistency() {
1051        let local = id_with_byte(1, 0xAB);
1052        let rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
1053        let target = id_with_byte(1, 0x00);
1054        let idx = rtm.bucket_index_for(&target);
1055        assert_eq!(idx, bucket_index(&local, &target));
1056    }
1057
1058    // ── 19. Large-scale stress test ───────────────────────────────────────────
1059
1060    #[test]
1061    fn test_rtm_stress_add_remove_find() {
1062        let mut rng = Xorshift64::new(0xC0FFEE);
1063        let local = rng.next_node_id();
1064        let mut rtm = RoutingTableManager::new(local, 5, 3);
1065        let mut ids: Vec<NodeId> = Vec::new();
1066
1067        // Insert 200 nodes.
1068        for _ in 0..200 {
1069            let id = rng.next_node_id();
1070            rtm.add_node(id, "x".to_string(), rng.next_rtt(), rng.next_ts());
1071            ids.push(id);
1072        }
1073
1074        // Remove every other node; they may have been replaced by cache entries
1075        // so some removals might return false — that is fine.
1076        for id in ids.iter().step_by(2) {
1077            let _ = rtm.remove_node(id);
1078        }
1079
1080        // find_closest should always return at most k results.
1081        let target = rng.next_node_id();
1082        let result = rtm.find_closest(&target, 5);
1083        assert!(result.len() <= 5);
1084    }
1085
1086    // ── 20. Duplicate add via cache ───────────────────────────────────────────
1087
1088    #[test]
1089    fn test_kbucket_duplicate_in_cache_updated() {
1090        let mut bucket = KBucket::new(1);
1091        let id_a = id_with_byte(0, 1);
1092        let id_b = id_with_byte(0, 2);
1093        bucket.add(dummy_entry(id_a, 10, 1000));
1094        bucket.add(dummy_entry(id_b, 10, 1001)); // goes to cache
1095                                                 // Update id_b in cache via add.
1096        bucket.add(dummy_entry(id_b, 99, 9999));
1097        // Cache should still have exactly one entry for id_b with updated RTT.
1098        assert_eq!(bucket.replacement_cache.len(), 1);
1099        let cached = bucket
1100            .replacement_cache
1101            .front()
1102            .expect("test: replacement cache should have an entry after overflow add");
1103        assert_eq!(cached.node_id, id_b);
1104        assert_eq!(cached.rtt_ms, 99);
1105    }
1106
1107    // ── 21. find_closest does not include local node ──────────────────────────
1108
1109    #[test]
1110    fn test_rtm_find_closest_excludes_local() {
1111        let local = zero_id();
1112        let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
1113        // add_node silently drops the local id.
1114        rtm.add_node(local, "self".to_string(), 0, 0);
1115        let result = rtm.find_closest(&local, 20);
1116        assert!(result.iter().all(|e| e.node_id != local));
1117    }
1118
1119    // ── 22. NodeId Display ────────────────────────────────────────────────────
1120
1121    #[test]
1122    fn test_node_id_display() {
1123        let id = NodeId([0xABu8; 32]);
1124        let s = id.to_string();
1125        assert_eq!(s.len(), 64);
1126        assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
1127    }
1128
1129    // ── 23. NodeId default ────────────────────────────────────────────────────
1130
1131    #[test]
1132    fn test_node_id_default() {
1133        let id = NodeId::default();
1134        assert_eq!(id.0, [0u8; 32]);
1135    }
1136
1137    // ── 24. NodeId as_bytes ───────────────────────────────────────────────────
1138
1139    #[test]
1140    fn test_node_id_as_bytes_round_trip() {
1141        let bytes = [0x42u8; 32];
1142        let id = NodeId::new(bytes);
1143        assert_eq!(id.as_bytes(), &bytes);
1144    }
1145
1146    // ── 25. Bucket index determinism ─────────────────────────────────────────
1147
1148    #[test]
1149    fn test_bucket_index_deterministic() {
1150        let local = id_with_byte(5, 0xDE);
1151        let target = id_with_byte(5, 0xAD);
1152        let idx1 = bucket_index(&local, &target);
1153        let idx2 = bucket_index(&local, &target);
1154        assert_eq!(idx1, idx2);
1155    }
1156
1157    // ── 26. RTM – update_rtt clears failures ─────────────────────────────────
1158
1159    #[test]
1160    fn test_rtm_update_rtt_clears_failed_queries() {
1161        let mut rtm = make_rtm(DEFAULT_K);
1162        let id = id_with_byte(0, 0x80);
1163        rtm.add_node(id, "a".to_string(), 100, 1000);
1164        // Simulate two failures without eviction.
1165        rtm.mark_failed(&id, 1001);
1166        rtm.mark_failed(&id, 1002);
1167        // Successful contact resets failed_queries.
1168        rtm.update_rtt(&id, 50, 2000);
1169        let entry = rtm
1170            .iter_entries()
1171            .find(|e| e.node_id == id)
1172            .expect("test: node should still be in routing table after non-evicting failures");
1173        assert_eq!(entry.failed_queries, 0);
1174    }
1175
1176    // ── 27. KBucket – is_full ────────────────────────────────────────────────
1177
1178    #[test]
1179    fn test_kbucket_is_full() {
1180        let mut bucket = KBucket::new(2);
1181        assert!(!bucket.is_full());
1182        bucket.add(dummy_entry(id_with_byte(0, 1), 10, 1000));
1183        assert!(!bucket.is_full());
1184        bucket.add(dummy_entry(id_with_byte(0, 2), 10, 1001));
1185        assert!(bucket.is_full());
1186    }
1187
1188    // ── 28. find_closest returns fewer than count when table is small ─────────
1189
1190    #[test]
1191    fn test_find_closest_fewer_than_requested() {
1192        let mut rtm = make_rtm(DEFAULT_K);
1193        rtm.add_node(id_with_byte(0, 0x80), "a".to_string(), 10, 1000);
1194        rtm.add_node(id_with_byte(0, 0x40), "b".to_string(), 10, 1001);
1195        let result = rtm.find_closest(&id_with_byte(0, 0x80), 100);
1196        assert_eq!(result.len(), 2); // only 2 nodes in the table
1197    }
1198
1199    // ── 29. MAX_FAILED_QUERIES constant value ────────────────────────────────
1200
1201    #[test]
1202    fn test_max_failed_queries_constant() {
1203        // Standard Kademlia uses 3; verify our constant matches.
1204        assert_eq!(MAX_FAILED_QUERIES, 3);
1205    }
1206
1207    // ── 30. Fuzz-style: random seed reproducibility ───────────────────────────
1208
1209    #[test]
1210    fn test_xorshift64_reproducible() {
1211        let mut rng1 = Xorshift64::new(12345);
1212        let mut rng2 = Xorshift64::new(12345);
1213        for _ in 0..100 {
1214            assert_eq!(rng1.next_u64(), rng2.next_u64());
1215        }
1216    }
1217
1218    // ── 31. RTM – k=1 edge case ───────────────────────────────────────────────
1219
1220    #[test]
1221    fn test_rtm_k1_single_slot() {
1222        let local = zero_id();
1223        let mut rtm = RoutingTableManager::new(local, 1, 3);
1224        let id_a = id_with_byte(0, 0x80);
1225        let id_b = id_with_byte(0, 0xC0); // same bucket (bucket 0, MSB differs)
1226        rtm.add_node(id_a, "a".to_string(), 10, 1000);
1227        rtm.add_node(id_b, "b".to_string(), 10, 1001); // goes to cache
1228        assert_eq!(rtm.total_nodes(), 1); // only one active
1229    }
1230
1231    // ── 32. iter_entries ──────────────────────────────────────────────────────
1232
1233    #[test]
1234    fn test_rtm_iter_entries_count() {
1235        let mut rtm = make_rtm(DEFAULT_K);
1236        for i in 1u8..=8 {
1237            rtm.add_node(
1238                id_with_byte(0, i * 0x10),
1239                "x".to_string(),
1240                10,
1241                i as u64 * 100,
1242            );
1243        }
1244        let count = rtm.iter_entries().count();
1245        assert_eq!(count, rtm.total_nodes());
1246    }
1247}