Skip to main content

irontide_dht/
routing_table.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_possible_wrap,
4    clippy::cast_sign_loss,
5    clippy::unchecked_time_subtraction,
6    reason = "M175: routing table — node arithmetic and time deltas use post-bootstrap Instants; remaining unchecked-time sites are test fixtures"
7)]
8
9//! Kademlia routing table with k-buckets (BEP 5).
10//!
11//! The routing table maps 160-bit node IDs to socket addresses using
12//! a binary-tree of k-buckets. Each bucket holds up to `K` (8) nodes.
13//! Buckets are split when the bucket containing our own ID overflows.
14
15use std::collections::HashSet;
16use std::net::{IpAddr, SocketAddr};
17use std::time::{Duration, Instant};
18
19use irontide_core::Id20;
20
21/// Maximum nodes per bucket (Kademlia k parameter).
22pub const K: usize = 8;
23
24/// Maximum number of buckets (one per bit of the ID).
25const MAX_BUCKETS: usize = 160;
26
27/// Kademlia node liveness classification (BEP 5).
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum NodeStatus {
30    /// Node has responded or sent a query within the last 15 minutes.
31    Good,
32    /// Node has not been active recently but has not failed repeatedly.
33    Questionable,
34    /// Node has failed 2 or more consecutive queries.
35    Bad,
36}
37
38/// Age threshold for considering a node "active" (15 minutes).
39const ACTIVE_THRESHOLD: Duration = Duration::from_mins(15);
40
41/// A node in the routing table.
42#[derive(Debug, Clone)]
43pub struct RoutingNode {
44    /// Node's 20-byte Kademlia ID.
45    pub id: Id20,
46    /// Node's socket address.
47    pub addr: SocketAddr,
48    /// Timestamp of the last successful interaction.
49    pub last_seen: Instant,
50    /// Number of consecutive failed queries.
51    pub fail_count: u32,
52    /// Timestamp of the last response received from this node.
53    pub last_response: Option<Instant>,
54    /// Timestamp of the last query received from this node.
55    pub last_query: Option<Instant>,
56}
57
58impl RoutingNode {
59    /// Classify this node per Kademlia liveness rules.
60    ///
61    /// - `Bad`: `fail_count` >= 2
62    /// - `Good`: responded or sent a query within the last 15 minutes
63    /// - `Questionable`: otherwise
64    ///
65    /// M175 BUG FIX: previously computed `Instant::now() - ACTIVE_THRESHOLD`,
66    /// which panics during the first 15 min of process uptime (Instant
67    /// monotonic clock not yet at the threshold). Reformulated to compare
68    /// elapsed time forward via `Instant::duration_since`, which saturates at
69    /// zero on clock skew. M132-class avoidance — same bug shape as the
70    /// `fetch_sub(1)` underflow that shipped in M132.
71    #[must_use]
72    pub fn status(&self) -> NodeStatus {
73        if self.fail_count >= 2 {
74            return NodeStatus::Bad;
75        }
76        let now = Instant::now();
77        let recent = |t: Instant| now.duration_since(t) <= ACTIVE_THRESHOLD;
78        let active = self.last_response.is_some_and(recent) || self.last_query.is_some_and(recent);
79        if active {
80            NodeStatus::Good
81        } else {
82            NodeStatus::Questionable
83        }
84    }
85}
86
87/// A single k-bucket.
88#[derive(Debug, Clone)]
89struct KBucket {
90    nodes: Vec<RoutingNode>,
91}
92
93impl KBucket {
94    fn new() -> Self {
95        Self {
96            nodes: Vec::with_capacity(K),
97        }
98    }
99
100    fn is_full(&self) -> bool {
101        self.nodes.len() >= K
102    }
103
104    fn find(&self, id: &Id20) -> Option<usize> {
105        self.nodes.iter().position(|n| n.id == *id)
106    }
107
108    /// Return the node with the highest fail count, or the oldest if tied.
109    fn worst_node(&self) -> Option<usize> {
110        self.nodes
111            .iter()
112            .enumerate()
113            .max_by(|(_, a), (_, b)| {
114                a.fail_count
115                    .cmp(&b.fail_count)
116                    .then(b.last_seen.cmp(&a.last_seen))
117            })
118            .map(|(i, _)| i)
119    }
120}
121
122/// Default maximum number of nodes in the routing table.
123/// Matches rqbit's default and prevents unbounded growth from adversarial injection.
124const DEFAULT_MAX_NODES: usize = 512;
125
126/// Kademlia routing table.
127#[derive(Debug, Clone)]
128pub struct RoutingTable {
129    own_id: Id20,
130    buckets: Vec<KBucket>,
131    /// When enabled, tracks IPs to enforce one-node-per-IP (BEP 42).
132    ip_set: HashSet<IpAddr>,
133    /// Whether to enforce one-node-per-IP restriction.
134    restrict_ips: bool,
135    /// Maximum number of nodes allowed in the routing table.
136    max_nodes: usize,
137}
138
139/// Result of an insert operation.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum InsertResult {
142    /// Node was inserted (new or updated).
143    Inserted,
144    /// Bucket is full but could be split; caller should try again.
145    BucketFull,
146    /// Node was not inserted; cannot split further.
147    Rejected,
148}
149
150impl RoutingTable {
151    /// Create a new routing table with the given own node ID.
152    #[must_use]
153    pub fn new(own_id: Id20) -> Self {
154        Self::with_config(own_id, false, DEFAULT_MAX_NODES)
155    }
156
157    /// Create a new routing table with IP restriction setting.
158    #[must_use]
159    pub fn new_with_config(own_id: Id20, restrict_ips: bool) -> Self {
160        Self::with_config(own_id, restrict_ips, DEFAULT_MAX_NODES)
161    }
162
163    /// Create a new routing table with full configuration.
164    #[must_use]
165    pub fn with_config(own_id: Id20, restrict_ips: bool, max_nodes: usize) -> Self {
166        Self {
167            own_id,
168            buckets: vec![KBucket::new()],
169            ip_set: HashSet::new(),
170            restrict_ips,
171            max_nodes,
172        }
173    }
174
175    /// Our node ID.
176    #[must_use]
177    pub fn own_id(&self) -> &Id20 {
178        &self.own_id
179    }
180
181    /// Total number of nodes in the routing table.
182    #[must_use]
183    pub fn len(&self) -> usize {
184        self.buckets.iter().map(|b| b.nodes.len()).sum()
185    }
186
187    /// Whether the routing table is empty.
188    #[must_use]
189    pub fn is_empty(&self) -> bool {
190        self.len() == 0
191    }
192
193    /// Number of buckets.
194    #[must_use]
195    pub fn bucket_count(&self) -> usize {
196        self.buckets.len()
197    }
198
199    /// Insert or update a node. Returns `true` if the node was added/updated.
200    pub fn insert(&mut self, id: Id20, addr: SocketAddr) -> bool {
201        if id == self.own_id {
202            return false; // Never insert ourselves
203        }
204        self.insert_inner(id, addr)
205    }
206
207    fn insert_inner(&mut self, id: Id20, addr: SocketAddr) -> bool {
208        let ip = addr.ip();
209
210        // BEP 42: check if another node with this IP exists (and it's not the same node)
211        if self.restrict_ips && self.ip_set.contains(&ip) {
212            let bucket_idx = self.bucket_index(&id);
213            if self.buckets[bucket_idx].find(&id).is_none() {
214                return false; // Different node, same IP — reject
215            }
216        }
217
218        let bucket_idx = self.bucket_index(&id);
219
220        // Already known — update last_seen and address
221        if let Some(pos) = self.buckets[bucket_idx].find(&id) {
222            let old_ip = self.buckets[bucket_idx].nodes[pos].addr.ip();
223            self.buckets[bucket_idx].nodes[pos].last_seen = Instant::now();
224            self.buckets[bucket_idx].nodes[pos].addr = addr;
225            self.buckets[bucket_idx].nodes[pos].fail_count = 0;
226            // Update IP tracking if address changed
227            if self.restrict_ips && old_ip != ip {
228                self.ip_set.remove(&old_ip);
229                self.ip_set.insert(ip);
230            }
231            return true;
232        }
233
234        // Global node cap — when at limit, only allow insertion by evicting a
235        // bad node (fail_count > 0) from this bucket. This keeps the table
236        // bounded while still allowing fresh nodes to replace stale ones.
237        let at_cap = self.len() >= self.max_nodes;
238        if at_cap {
239            if let Some(worst_idx) = self.buckets[bucket_idx].worst_node()
240                && self.buckets[bucket_idx].nodes[worst_idx].fail_count > 0
241            {
242                if self.restrict_ips {
243                    self.ip_set
244                        .remove(&self.buckets[bucket_idx].nodes[worst_idx].addr.ip());
245                }
246                self.buckets[bucket_idx].nodes[worst_idx] = RoutingNode {
247                    id,
248                    addr,
249                    last_seen: Instant::now(),
250                    fail_count: 0,
251                    last_response: None,
252                    last_query: None,
253                };
254                if self.restrict_ips {
255                    self.ip_set.insert(ip);
256                }
257                return true;
258            }
259            // No bad nodes to evict — reject
260            return false;
261        }
262
263        // Room in bucket
264        if !self.buckets[bucket_idx].is_full() {
265            self.buckets[bucket_idx].nodes.push(RoutingNode {
266                id,
267                addr,
268                last_seen: Instant::now(),
269                fail_count: 0,
270                last_response: None,
271                last_query: None,
272            });
273            if self.restrict_ips {
274                self.ip_set.insert(ip);
275            }
276            return true;
277        }
278
279        // Bucket full — try to evict a failed node
280        if let Some(worst_idx) = self.buckets[bucket_idx].worst_node()
281            && self.buckets[bucket_idx].nodes[worst_idx].fail_count > 0
282        {
283            // Remove old node's IP from tracking (gap fix #7)
284            if self.restrict_ips {
285                self.ip_set
286                    .remove(&self.buckets[bucket_idx].nodes[worst_idx].addr.ip());
287            }
288            self.buckets[bucket_idx].nodes[worst_idx] = RoutingNode {
289                id,
290                addr,
291                last_seen: Instant::now(),
292                fail_count: 0,
293                last_response: None,
294                last_query: None,
295            };
296            if self.restrict_ips {
297                self.ip_set.insert(ip);
298            }
299            return true;
300        }
301
302        // Bucket full, all nodes good — try splitting if this is our bucket
303        if self.can_split(bucket_idx) {
304            self.split(bucket_idx);
305            // Retry insert after split
306            return self.insert_inner(id, addr);
307        }
308
309        false
310    }
311
312    /// Remove a node by ID. Returns `true` if it was present.
313    pub fn remove(&mut self, id: &Id20) -> bool {
314        let bucket_idx = self.bucket_index(id);
315        let bucket = &mut self.buckets[bucket_idx];
316        if let Some(pos) = bucket.find(id) {
317            if self.restrict_ips {
318                self.ip_set.remove(&bucket.nodes[pos].addr.ip());
319            }
320            bucket.nodes.remove(pos);
321            true
322        } else {
323            false
324        }
325    }
326
327    /// Mark a node as recently seen.
328    pub fn mark_seen(&mut self, id: &Id20) {
329        let bucket_idx = self.bucket_index(id);
330        if let Some(pos) = self.buckets[bucket_idx].find(id) {
331            self.buckets[bucket_idx].nodes[pos].last_seen = Instant::now();
332            self.buckets[bucket_idx].nodes[pos].fail_count = 0;
333        }
334    }
335
336    /// Increment a node's fail count.
337    pub fn mark_failed(&mut self, id: &Id20) {
338        let bucket_idx = self.bucket_index(id);
339        if let Some(pos) = self.buckets[bucket_idx].find(id) {
340            self.buckets[bucket_idx].nodes[pos].fail_count += 1;
341        }
342    }
343
344    /// Return the `count` closest nodes to `target`, sorted by XOR distance.
345    #[must_use]
346    pub fn closest(&self, target: &Id20, count: usize) -> Vec<RoutingNode> {
347        let mut all: Vec<&RoutingNode> = self.buckets.iter().flat_map(|b| &b.nodes).collect();
348        all.sort_by_key(|n| n.id.xor_distance(target));
349        all.into_iter().take(count).cloned().collect()
350    }
351
352    /// Return bucket indices that haven't been refreshed recently.
353    ///
354    /// M175 BUG FIX: same shape as `RoutingNode::status` — `Instant::now() - max_age`
355    /// panics if `max_age` exceeds process uptime. Compares elapsed time forward
356    /// instead.
357    #[must_use]
358    pub fn stale_buckets(&self, max_age: std::time::Duration) -> Vec<usize> {
359        let now = Instant::now();
360        self.buckets
361            .iter()
362            .enumerate()
363            .filter(|(_, b)| {
364                b.nodes.is_empty()
365                    || b.nodes
366                        .iter()
367                        .all(|n| now.duration_since(n.last_seen) > max_age)
368            })
369            .map(|(i, _)| i)
370            .collect()
371    }
372
373    /// Generate a random ID that falls within the given bucket index.
374    /// Useful for refreshing stale buckets.
375    #[must_use]
376    pub fn random_id_in_bucket(&self, bucket_idx: usize) -> Id20 {
377        let mut id = self.own_id;
378        // Flip the bit at position `bucket_idx` to land in that bucket's range.
379        // Buckets cover distances where the first differing bit is at position bucket_idx.
380        if bucket_idx < MAX_BUCKETS {
381            let byte_idx = bucket_idx / 8;
382            let bit_idx = 7 - (bucket_idx % 8);
383            id.0[byte_idx] ^= 1 << bit_idx;
384        }
385        id
386    }
387
388    /// Return all nodes in the routing table as (id, addr) pairs.
389    #[must_use]
390    pub fn all_nodes(&self) -> Vec<(Id20, SocketAddr)> {
391        self.buckets
392            .iter()
393            .flat_map(|b| b.nodes.iter().map(|n| (n.id, n.addr)))
394            .collect()
395    }
396
397    /// Get a reference to a node by ID.
398    #[must_use]
399    pub fn get(&self, id: &Id20) -> Option<&RoutingNode> {
400        let bucket_idx = self.bucket_index(id);
401        self.buckets[bucket_idx]
402            .find(id)
403            .map(|pos| &self.buckets[bucket_idx].nodes[pos])
404    }
405
406    /// Get a mutable reference to a node by ID.
407    pub fn get_mut(&mut self, id: &Id20) -> Option<&mut RoutingNode> {
408        let bucket_idx = self.bucket_index(id);
409        let pos = self.buckets[bucket_idx].find(id)?;
410        Some(&mut self.buckets[bucket_idx].nodes[pos])
411    }
412
413    /// Record a successful response from a node, resetting its fail count.
414    pub fn mark_response(&mut self, id: &Id20) {
415        let bucket_idx = self.bucket_index(id);
416        if let Some(pos) = self.buckets[bucket_idx].find(id) {
417            self.buckets[bucket_idx].nodes[pos].last_response = Some(Instant::now());
418            self.buckets[bucket_idx].nodes[pos].fail_count = 0;
419        }
420    }
421
422    /// Record an incoming query from a node.
423    pub fn mark_query(&mut self, id: &Id20) {
424        let bucket_idx = self.bucket_index(id);
425        if let Some(pos) = self.buckets[bucket_idx].find(id) {
426            self.buckets[bucket_idx].nodes[pos].last_query = Some(Instant::now());
427        }
428    }
429
430    /// Mark all nodes in the routing table as Questionable (M97).
431    ///
432    /// Called when saved-state verification fails — loaded nodes may be stale.
433    /// Setting `last_response = None` and `last_query = None` makes nodes
434    /// Questionable (never responded, never queried). `fail_count = 0` ensures
435    /// they are Questionable rather than Bad.
436    pub fn mark_all_questionable(&mut self) {
437        for bucket in &mut self.buckets {
438            for node in &mut bucket.nodes {
439                node.last_response = None;
440                node.last_query = None;
441                node.fail_count = 0;
442            }
443        }
444    }
445
446    /// Return all nodes whose status is `Questionable`.
447    #[must_use]
448    pub fn questionable_nodes(&self) -> Vec<(Id20, SocketAddr)> {
449        self.buckets
450            .iter()
451            .flat_map(|b| {
452                b.nodes
453                    .iter()
454                    .filter(|n| n.status() == NodeStatus::Questionable)
455                    .map(|n| (n.id, n.addr))
456            })
457            .collect()
458    }
459
460    // ---- Internal ----
461
462    /// Determine which bucket a node ID belongs to.
463    ///
464    /// The bucket index is the number of leading matching bits between
465    /// `own_id` and `id`. If the table has fewer buckets, the last bucket
466    /// is a catch-all.
467    fn bucket_index(&self, id: &Id20) -> usize {
468        let distance = self.own_id.xor_distance(id);
469        let leading_zeros = leading_zeros_160(&distance);
470        // Clamp to the last bucket
471        leading_zeros.min(self.buckets.len() - 1)
472    }
473
474    /// Whether we can split the bucket at `idx`.
475    /// Only the last bucket (which contains our own ID's distance range) can be split.
476    fn can_split(&self, idx: usize) -> bool {
477        idx == self.buckets.len() - 1 && self.buckets.len() < MAX_BUCKETS
478    }
479
480    /// Split the last bucket into two.
481    fn split(&mut self, idx: usize) {
482        debug_assert!(idx == self.buckets.len() - 1);
483        let old_bucket = &mut self.buckets[idx];
484        let nodes = std::mem::take(&mut old_bucket.nodes);
485
486        let mut near = KBucket::new();
487        let mut far = KBucket::new();
488
489        let new_depth = self.buckets.len(); // This will be the index of the new bucket
490
491        for node in nodes {
492            let distance = self.own_id.xor_distance(&node.id);
493            let lz = leading_zeros_160(&distance);
494            if lz >= new_depth {
495                near.nodes.push(node);
496            } else {
497                far.nodes.push(node);
498            }
499        }
500
501        // idx stays as the "far" bucket, push "near" as the new last bucket
502        self.buckets[idx] = far;
503        self.buckets.push(near);
504    }
505}
506
507/// Count leading zero bits in a 160-bit (20-byte) value.
508fn leading_zeros_160(id: &Id20) -> usize {
509    let mut zeros = 0;
510    for &byte in id.as_bytes() {
511        if byte == 0 {
512            zeros += 8;
513        } else {
514            zeros += byte.leading_zeros() as usize;
515            break;
516        }
517    }
518    zeros
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use pretty_assertions::assert_eq;
525
526    fn id(byte: u8) -> Id20 {
527        let mut bytes = [0u8; 20];
528        bytes[19] = byte;
529        Id20(bytes)
530    }
531
532    fn addr(port: u16) -> SocketAddr {
533        format!("10.0.0.1:{port}").parse().unwrap()
534    }
535
536    #[test]
537    fn insert_and_retrieve() {
538        let mut rt = RoutingTable::new(Id20::ZERO);
539        assert!(rt.insert(id(1), addr(1)));
540        assert_eq!(rt.len(), 1);
541        assert!(rt.get(&id(1)).is_some());
542    }
543
544    /// M175 regression: `RoutingNode::status()` previously used
545    /// `Instant::now() - ACTIVE_THRESHOLD`, which panics during the first
546    /// 15 minutes of process uptime (test process uptime is far below that).
547    /// Reformulated using forward `duration_since` so the call is panic-free
548    /// regardless of process age.
549    #[test]
550    fn routing_node_status_does_not_panic_on_fresh_process() {
551        let now = Instant::now();
552        let node = RoutingNode {
553            id: id(1),
554            addr: addr(1),
555            last_seen: now,
556            fail_count: 0,
557            last_response: Some(now),
558            last_query: None,
559        };
560        // Just calling status() at all would panic with the pre-M175 form.
561        assert_eq!(node.status(), NodeStatus::Good);
562
563        // None timestamps: should not panic and should be Questionable.
564        let stale = RoutingNode {
565            id: id(2),
566            addr: addr(2),
567            last_seen: now,
568            fail_count: 0,
569            last_response: None,
570            last_query: None,
571        };
572        assert_eq!(stale.status(), NodeStatus::Questionable);
573    }
574
575    /// M175 regression: `RoutingTable::stale_buckets` previously used
576    /// `Instant::now() - max_age` and panicked when `max_age` exceeded
577    /// process uptime. Should now compare elapsed time forward.
578    #[test]
579    fn stale_buckets_does_not_panic_on_large_max_age() {
580        let rt = RoutingTable::new(Id20::ZERO);
581        // 24-hour max_age — far exceeds any test process uptime.
582        let _ = rt.stale_buckets(std::time::Duration::from_hours(24));
583    }
584
585    #[test]
586    fn insert_self_rejected() {
587        let mut rt = RoutingTable::new(Id20::ZERO);
588        assert!(!rt.insert(Id20::ZERO, addr(1)));
589        assert_eq!(rt.len(), 0);
590    }
591
592    #[test]
593    fn update_existing_node() {
594        let mut rt = RoutingTable::new(Id20::ZERO);
595        rt.insert(id(1), addr(1));
596        rt.insert(id(1), addr(2)); // Update address
597        assert_eq!(rt.len(), 1);
598        assert_eq!(rt.get(&id(1)).unwrap().addr, addr(2));
599    }
600
601    #[test]
602    fn remove_node() {
603        let mut rt = RoutingTable::new(Id20::ZERO);
604        rt.insert(id(1), addr(1));
605        assert!(rt.remove(&id(1)));
606        assert_eq!(rt.len(), 0);
607        assert!(!rt.remove(&id(1))); // Already gone
608    }
609
610    #[test]
611    fn closest_nodes_sorted() {
612        let mut rt = RoutingTable::new(Id20::ZERO);
613        rt.insert(id(1), addr(1));
614        rt.insert(id(5), addr(5));
615        rt.insert(id(3), addr(3));
616        rt.insert(id(10), addr(10));
617
618        let closest = rt.closest(&Id20::ZERO, 3);
619        assert_eq!(closest.len(), 3);
620        // XOR distance from ZERO is just the value itself
621        assert_eq!(closest[0].id, id(1));
622        assert_eq!(closest[1].id, id(3));
623        assert_eq!(closest[2].id, id(5));
624    }
625
626    #[test]
627    fn closest_fewer_than_count() {
628        let mut rt = RoutingTable::new(Id20::ZERO);
629        rt.insert(id(1), addr(1));
630        let closest = rt.closest(&Id20::ZERO, 10);
631        assert_eq!(closest.len(), 1);
632    }
633
634    #[test]
635    fn bucket_splitting() {
636        let mut rt = RoutingTable::new(Id20::ZERO);
637        // Insert 9 nodes — should trigger split since K=8
638        for i in 1..=9u8 {
639            rt.insert(id(i), addr(u16::from(i)));
640        }
641        assert!(rt.bucket_count() > 1);
642        assert_eq!(rt.len(), 9);
643    }
644
645    #[test]
646    fn evict_failed_node() {
647        let mut rt = RoutingTable::new(Id20::ZERO);
648        // Fill bucket to capacity
649        for i in 1..=K as u8 {
650            rt.insert(id(i), addr(u16::from(i)));
651        }
652        assert_eq!(rt.len(), K);
653
654        // Mark a node as failed
655        rt.mark_failed(&id(1));
656        rt.mark_failed(&id(1));
657
658        // This should evict the failed node
659        let new_id = id(100);
660        assert!(rt.insert(new_id, addr(100)));
661        assert!(rt.get(&new_id).is_some());
662    }
663
664    #[test]
665    fn mark_seen_resets_fail_count() {
666        let mut rt = RoutingTable::new(Id20::ZERO);
667        rt.insert(id(1), addr(1));
668        rt.mark_failed(&id(1));
669        assert_eq!(rt.get(&id(1)).unwrap().fail_count, 1);
670        rt.mark_seen(&id(1));
671        assert_eq!(rt.get(&id(1)).unwrap().fail_count, 0);
672    }
673
674    #[test]
675    fn stale_buckets_detection() {
676        let mut rt = RoutingTable::new(Id20::ZERO);
677        // Empty routing table — all buckets are stale
678        let stale = rt.stale_buckets(std::time::Duration::from_mins(15));
679        assert_eq!(stale.len(), 1);
680
681        // Insert a node — bucket should not be stale
682        rt.insert(id(1), addr(1));
683        let stale = rt.stale_buckets(std::time::Duration::from_mins(15));
684        assert!(stale.is_empty());
685    }
686
687    #[test]
688    fn leading_zeros_correct() {
689        assert_eq!(leading_zeros_160(&Id20::ZERO), 160);
690        assert_eq!(leading_zeros_160(&id(1)), 159);
691        assert_eq!(leading_zeros_160(&id(128)), 152);
692        let mut full = [0xFFu8; 20];
693        assert_eq!(leading_zeros_160(&Id20(full)), 0);
694        full[0] = 0x01;
695        full[1..].fill(0);
696        assert_eq!(leading_zeros_160(&Id20(full)), 7);
697    }
698
699    #[test]
700    fn random_id_in_bucket_differs() {
701        let rt = RoutingTable::new(Id20::ZERO);
702        let rand_id = rt.random_id_in_bucket(0);
703        assert_ne!(rand_id, Id20::ZERO);
704    }
705
706    // ── BEP 42 IP restriction tests ────────────────────────────────
707
708    #[test]
709    fn restrict_ips_rejects_second_node_same_ip() {
710        let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
711        let ip_addr: SocketAddr = "10.0.0.1:6881".parse().unwrap();
712        assert!(rt.insert(id(1), ip_addr));
713        // Second node with same IP but different ID — rejected
714        let ip_addr2: SocketAddr = "10.0.0.1:6882".parse().unwrap();
715        assert!(!rt.insert(id(2), ip_addr2));
716        assert_eq!(rt.len(), 1);
717    }
718
719    #[test]
720    fn restrict_ips_allows_same_node_update() {
721        let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
722        let addr1: SocketAddr = "10.0.0.1:6881".parse().unwrap();
723        let addr2: SocketAddr = "10.0.0.1:6882".parse().unwrap();
724        assert!(rt.insert(id(1), addr1));
725        // Same node ID updating its port — allowed
726        assert!(rt.insert(id(1), addr2));
727        assert_eq!(rt.len(), 1);
728        assert_eq!(rt.get(&id(1)).unwrap().addr, addr2);
729    }
730
731    #[test]
732    fn no_restrict_ips_allows_multiple_nodes_same_ip() {
733        let mut rt = RoutingTable::new_with_config(Id20::ZERO, false);
734        let addr1: SocketAddr = "10.0.0.1:6881".parse().unwrap();
735        let addr2: SocketAddr = "10.0.0.1:6882".parse().unwrap();
736        assert!(rt.insert(id(1), addr1));
737        assert!(rt.insert(id(2), addr2));
738        assert_eq!(rt.len(), 2);
739    }
740
741    #[test]
742    fn restrict_ips_remove_frees_ip_slot() {
743        let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
744        let addr: SocketAddr = "10.0.0.1:6881".parse().unwrap();
745        assert!(rt.insert(id(1), addr));
746        assert!(rt.remove(&id(1)));
747        // IP slot is now free — different node with same IP can insert
748        assert!(rt.insert(id(2), addr));
749        assert_eq!(rt.len(), 1);
750    }
751
752    // ── Liveness / NodeStatus tests ────────────────────────────────
753
754    #[test]
755    fn node_status_bad_on_two_failures() {
756        let mut rt = RoutingTable::new(Id20::ZERO);
757        rt.insert(id(1), addr(1));
758        rt.mark_failed(&id(1));
759        assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Questionable);
760        rt.mark_failed(&id(1));
761        assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Bad);
762    }
763
764    #[test]
765    fn node_status_good_after_mark_response() {
766        let mut rt = RoutingTable::new(Id20::ZERO);
767        rt.insert(id(1), addr(1));
768        // Freshly inserted nodes have no last_response/last_query, so Questionable
769        assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Questionable);
770        rt.mark_response(&id(1));
771        assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Good);
772    }
773
774    #[test]
775    fn node_status_good_after_mark_query() {
776        let mut rt = RoutingTable::new(Id20::ZERO);
777        rt.insert(id(1), addr(1));
778        rt.mark_query(&id(1));
779        assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Good);
780    }
781
782    #[test]
783    fn mark_response_resets_fail_count() {
784        let mut rt = RoutingTable::new(Id20::ZERO);
785        rt.insert(id(1), addr(1));
786        rt.mark_failed(&id(1));
787        rt.mark_failed(&id(1));
788        assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Bad);
789        rt.mark_response(&id(1));
790        assert_eq!(rt.get(&id(1)).unwrap().fail_count, 0);
791        assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Good);
792    }
793
794    #[test]
795    fn mark_response_noop_for_unknown_node() {
796        let mut rt = RoutingTable::new(Id20::ZERO);
797        // Should not panic when node is not in the table
798        rt.mark_response(&id(42));
799    }
800
801    #[test]
802    fn mark_query_noop_for_unknown_node() {
803        let mut rt = RoutingTable::new(Id20::ZERO);
804        rt.mark_query(&id(42));
805    }
806
807    #[test]
808    fn get_mut_finds_node() {
809        let mut rt = RoutingTable::new(Id20::ZERO);
810        rt.insert(id(1), addr(1));
811        let node = rt.get_mut(&id(1));
812        assert!(node.is_some());
813        // Mutate through the reference
814        node.unwrap().fail_count = 99;
815        assert_eq!(rt.get(&id(1)).unwrap().fail_count, 99);
816    }
817
818    #[test]
819    fn get_mut_returns_none_for_missing_node() {
820        let mut rt = RoutingTable::new(Id20::ZERO);
821        assert!(rt.get_mut(&id(1)).is_none());
822    }
823
824    #[test]
825    fn questionable_nodes_filters_correctly() {
826        let mut rt = RoutingTable::new(Id20::ZERO);
827        rt.insert(id(1), addr(1)); // Questionable (no activity)
828        rt.insert(id(2), addr(2)); // Will be Good
829        rt.insert(id(3), addr(3)); // Will be Bad
830        rt.mark_response(&id(2));
831        rt.mark_failed(&id(3));
832        rt.mark_failed(&id(3));
833
834        let q = rt.questionable_nodes();
835        assert_eq!(q.len(), 1);
836        assert_eq!(q[0].0, id(1));
837    }
838
839    #[test]
840    fn questionable_nodes_empty_when_all_good_or_bad() {
841        let mut rt = RoutingTable::new(Id20::ZERO);
842        rt.insert(id(1), addr(1));
843        rt.insert(id(2), addr(2));
844        rt.mark_response(&id(1));
845        rt.mark_failed(&id(2));
846        rt.mark_failed(&id(2));
847
848        assert!(rt.questionable_nodes().is_empty());
849    }
850
851    #[test]
852    fn worst_node_evicts_oldest_on_tied_fail_counts() {
853        // Build a bucket manually to control insertion order and last_seen values
854        let mut bucket = KBucket::new();
855        let now = Instant::now();
856        // Node A: inserted earlier (older last_seen), fail_count=1
857        bucket.nodes.push(RoutingNode {
858            id: id(1),
859            addr: addr(1),
860            last_seen: now - std::time::Duration::from_secs(100),
861            fail_count: 1,
862            last_response: None,
863            last_query: None,
864        });
865        // Node B: inserted more recently (newer last_seen), fail_count=1
866        bucket.nodes.push(RoutingNode {
867            id: id(2),
868            addr: addr(2),
869            last_seen: now - std::time::Duration::from_secs(10),
870            fail_count: 1,
871            last_response: None,
872            last_query: None,
873        });
874        // worst_node should return index 0 (the oldest node)
875        let worst = bucket.worst_node().unwrap();
876        assert_eq!(
877            bucket.nodes[worst].id,
878            id(1),
879            "oldest node should be evicted on tied fail counts"
880        );
881    }
882
883    #[test]
884    fn worst_node_prefers_highest_fail_count() {
885        let mut bucket = KBucket::new();
886        let now = Instant::now();
887        bucket.nodes.push(RoutingNode {
888            id: id(1),
889            addr: addr(1),
890            last_seen: now,
891            fail_count: 3,
892            last_response: None,
893            last_query: None,
894        });
895        bucket.nodes.push(RoutingNode {
896            id: id(2),
897            addr: addr(2),
898            last_seen: now - std::time::Duration::from_secs(1000),
899            fail_count: 1,
900            last_response: None,
901            last_query: None,
902        });
903        let worst = bucket.worst_node().unwrap();
904        assert_eq!(
905            bucket.nodes[worst].id,
906            id(1),
907            "highest fail_count should win regardless of age"
908        );
909    }
910
911    #[test]
912    fn restrict_ips_eviction_frees_ip_slot() {
913        let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
914        // Fill bucket to capacity, each with different IP
915        for i in 1..=K as u8 {
916            let a: SocketAddr = format!("10.0.0.{i}:6881").parse().unwrap();
917            rt.insert(id(i), a);
918        }
919        assert_eq!(rt.len(), K);
920
921        // Mark a node as failed
922        rt.mark_failed(&id(1));
923        rt.mark_failed(&id(1));
924
925        // New node with a different IP should evict the failed node
926        let new_addr: SocketAddr = "10.0.0.100:6881".parse().unwrap();
927        assert!(rt.insert(id(100), new_addr));
928        assert_eq!(rt.len(), K);
929        // The old IP (10.0.0.1) should be freed
930        let old_addr: SocketAddr = "10.0.0.1:6882".parse().unwrap();
931        assert!(rt.insert(id(200), old_addr));
932    }
933
934    // ── Node cap tests ─────────────────────────────────────────────
935
936    #[test]
937    fn routing_table_node_cap_rejects_at_limit() {
938        // With max_nodes=4, inserting a 5th node should fail when no bad nodes
939        // exist in the target bucket.
940        let mut rt = RoutingTable::with_config(Id20::ZERO, false, 4);
941        for i in 1..=4u8 {
942            assert!(
943                rt.insert(id(i), addr(u16::from(i))),
944                "insert {i} should succeed"
945            );
946        }
947        assert_eq!(rt.len(), 4);
948        // 5th insert — all nodes are good, should be rejected
949        assert!(!rt.insert(id(5), addr(5)));
950        assert_eq!(rt.len(), 4);
951    }
952
953    #[test]
954    fn routing_table_node_cap_allows_eviction() {
955        // At the cap, a new node can still be inserted if a bad node (fail_count > 0)
956        // exists in the target bucket and can be evicted.
957        let mut rt = RoutingTable::with_config(Id20::ZERO, false, 4);
958        for i in 1..=4u8 {
959            rt.insert(id(i), addr(u16::from(i)));
960        }
961        assert_eq!(rt.len(), 4);
962
963        // Mark node 1 as failed so it can be evicted
964        rt.mark_failed(&id(1));
965
966        // Insert at cap succeeds by evicting the failed node
967        assert!(rt.insert(id(5), addr(5)));
968        assert_eq!(rt.len(), 4);
969        assert!(rt.get(&id(5)).is_some());
970        assert!(rt.get(&id(1)).is_none());
971    }
972
973    #[test]
974    fn routing_table_node_cap_allows_update() {
975        // Updating an existing node must succeed even when at the cap.
976        let mut rt = RoutingTable::with_config(Id20::ZERO, false, 4);
977        for i in 1..=4u8 {
978            rt.insert(id(i), addr(u16::from(i)));
979        }
980        assert_eq!(rt.len(), 4);
981
982        // Update existing node — new address, same ID
983        assert!(rt.insert(id(2), addr(200)));
984        assert_eq!(rt.len(), 4);
985        assert_eq!(rt.get(&id(2)).unwrap().addr, addr(200));
986    }
987
988    #[test]
989    fn routing_table_default_cap_512() {
990        let rt = RoutingTable::new(Id20::ZERO);
991        // The default max_nodes should be 512
992        assert_eq!(rt.max_nodes, DEFAULT_MAX_NODES);
993        assert_eq!(rt.max_nodes, 512);
994    }
995
996    #[test]
997    fn mark_all_questionable_resets_liveness() {
998        let own_id = Id20([0x00; 20]);
999        let mut rt = RoutingTable::new(own_id);
1000
1001        // Insert two nodes and mark them as responsive
1002        let node1 = Id20([0x80; 20]);
1003        let node2 = Id20([0x40; 20]);
1004        let addr1: SocketAddr = "192.0.2.1:6881".parse().unwrap();
1005        let addr2: SocketAddr = "192.0.2.2:6881".parse().unwrap();
1006
1007        rt.insert(node1, addr1);
1008        rt.insert(node2, addr2);
1009        rt.mark_response(&node1);
1010        rt.mark_response(&node2);
1011
1012        // Verify nodes are Good
1013        assert_eq!(rt.get(&node1).unwrap().status(), NodeStatus::Good);
1014        assert_eq!(rt.get(&node2).unwrap().status(), NodeStatus::Good);
1015
1016        // Mark all questionable
1017        rt.mark_all_questionable();
1018
1019        // Verify nodes are now Questionable
1020        assert_eq!(rt.get(&node1).unwrap().status(), NodeStatus::Questionable);
1021        assert_eq!(rt.get(&node2).unwrap().status(), NodeStatus::Questionable);
1022        assert_eq!(rt.get(&node1).unwrap().fail_count, 0);
1023        assert_eq!(rt.get(&node2).unwrap().fail_count, 0);
1024    }
1025}