Skip to main content

dig_dht/
routing.rs

1//! The Kademlia routing table: 256 k-buckets of [`Contact`]s, keyed by XOR-distance
2//! [`bucket_index`](crate::key::Distance::bucket_index) from this node.
3//!
4//! A node keeps, for each possible shared-prefix length with its own id, a bucket of up to `k`
5//! contacts it knows at that distance. Buckets are **least-recently-seen ordered**: a fresh contact
6//! goes to the back (most-recently-seen); when a full bucket gets a new contact, Kademlia keeps the
7//! **existing** least-recently-seen node if it is still alive (long-lived nodes are the most
8//! valuable — they resist eviction attacks) and drops the newcomer, only replacing the LRS node once
9//! it is confirmed dead. This module implements that policy with an explicit
10//! [`InsertOutcome`] so the service can ping-and-replace.
11//!
12//! [`closest`](RoutingTable::closest) returns the `k` contacts nearest a target across all buckets —
13//! the seed set for an iterative lookup.
14
15use serde::{Deserialize, Serialize};
16
17use dig_nat::PeerId;
18
19use crate::key::Key;
20use crate::record::{sort_and_cap_addresses, CandidateAddr};
21
22/// A known peer in the routing table / on the wire: its `peer_id` (64-hex) and candidate addresses.
23///
24/// This is the DHT's contact record and the `find_node` / `find_providers.closer` wire shape. The
25/// `addresses` are the same `{ host, port, kind }` candidates as an L7 `dig.getPeers` peer, so a
26/// contact converts directly to a `dig_nat::PeerTarget`.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Contact {
29    /// The peer's identity — 64-hex `peer_id = SHA-256(SPKI DER)`.
30    pub peer_id: String,
31    /// Candidate addresses to reach the peer, ordered IPv6-first then most-direct-first by
32    /// [`AddressKind::rank`](crate::record::AddressKind::rank) and bounded to
33    /// [`MAX_ADDRESSES_PER_RECORD`](crate::record::MAX_ADDRESSES_PER_RECORD) — held by BOTH
34    /// [`Contact::new`] and deserialization, so a contact off the wire carries the same guarantee
35    /// as a constructed one.
36    #[serde(deserialize_with = "crate::record::deserialize_capped_addresses")]
37    pub addresses: Vec<CandidateAddr>,
38}
39
40impl Contact {
41    /// Build a contact from a decoded [`PeerId`] and its candidate addresses. `addresses` is sorted
42    /// IPv6-first-then-rank (see `sort_addresses_ipv6_first`) — ecosystem-wide IPv6-first,
43    /// IPv4-fallback for peer communication.
44    pub fn new(peer_id: &PeerId, mut addresses: Vec<CandidateAddr>) -> Self {
45        sort_and_cap_addresses(&mut addresses);
46        Contact {
47            peer_id: peer_id.to_hex(),
48            addresses,
49        }
50    }
51
52    /// The peer's [`PeerId`] decoded from the 64-hex field, or `None` if malformed.
53    pub fn peer_id(&self) -> Option<PeerId> {
54        PeerId::from_hex(&self.peer_id)
55    }
56
57    /// This contact's key in the DHT keyspace (its `peer_id`), or `None` if the hex is malformed.
58    pub fn key(&self) -> Option<Key> {
59        self.peer_id().as_ref().map(Key::from_peer_id)
60    }
61
62    /// The FIRST candidate only — the IPv6-preferred, most-direct dialable address, if any.
63    ///
64    /// **Prefer [`dial_candidates`](Self::dial_candidates) for dialing**: one address means one
65    /// attempt and no fallback, so an unusable IPv6 candidate masks a working IPv4 one (§5.2). Use
66    /// this only where a single representative address is what is wanted (a log line, a display
67    /// string, a metric label).
68    pub fn best_address(&self) -> Option<&CandidateAddr> {
69        self.addresses.iter().find(|a| a.kind.is_dialable())
70    }
71
72    /// This contact's dialable candidates in §5.2 dial order — see
73    /// [`record::dial_candidates`](crate::record::dial_candidates) for the ordering contract.
74    pub fn dial_candidates(&self) -> Vec<&CandidateAddr> {
75        crate::record::dial_candidates(&self.addresses)
76    }
77}
78
79/// What happened when a contact was offered to a full bucket — drives the service's
80/// ping-and-replace maintenance.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum InsertOutcome {
83    /// The contact was inserted (bucket had room, or it was already present and got refreshed).
84    Inserted,
85    /// The bucket is full. The `candidate` was NOT inserted; the caller should ping `lru` (the
86    /// least-recently-seen contact) and, if it fails to respond, call
87    /// [`RoutingTable::replace`] to evict it for the candidate.
88    Full {
89        /// The least-recently-seen contact currently holding the contested slot.
90        lru: Contact,
91    },
92}
93
94/// One k-bucket: up to `capacity` (= `k`) contacts, least-recently-seen at the FRONT, most-recently
95/// seen at the BACK.
96#[derive(Debug, Clone)]
97struct KBucket {
98    contacts: Vec<Contact>,
99    capacity: usize,
100}
101
102impl KBucket {
103    fn new(capacity: usize) -> Self {
104        KBucket {
105            contacts: Vec::new(),
106            capacity,
107        }
108    }
109
110    /// Offer `contact` to the bucket (LRS-ordered):
111    /// - already present → move to the back (mark most-recently-seen) → [`InsertOutcome::Inserted`].
112    /// - room available → push to the back → [`InsertOutcome::Inserted`].
113    /// - full → [`InsertOutcome::Full`] naming the LRS contact for the caller to ping-and-replace.
114    fn offer(&mut self, contact: Contact) -> InsertOutcome {
115        if let Some(pos) = self
116            .contacts
117            .iter()
118            .position(|c| c.peer_id == contact.peer_id)
119        {
120            // Seen again: refresh recency by moving to the back, updating addresses.
121            self.contacts.remove(pos);
122            self.contacts.push(contact);
123            return InsertOutcome::Inserted;
124        }
125        if self.contacts.len() < self.capacity {
126            self.contacts.push(contact);
127            return InsertOutcome::Inserted;
128        }
129        // Full — the least-recently-seen contact is at the front.
130        InsertOutcome::Full {
131            lru: self.contacts[0].clone(),
132        }
133    }
134
135    /// Evict `lru_peer_id` (if it is still the front contact) and insert `candidate` at the back.
136    /// No-op returning `false` if the front contact changed since the [`InsertOutcome::Full`] (the
137    /// LRU responded to a ping and got refreshed, or the bucket changed) — the candidate is dropped.
138    fn replace(&mut self, lru_peer_id: &str, candidate: Contact) -> bool {
139        match self.contacts.first() {
140            Some(front) if front.peer_id == lru_peer_id => {
141                self.contacts.remove(0);
142                self.contacts.push(candidate);
143                true
144            }
145            _ => false,
146        }
147    }
148
149    fn remove(&mut self, peer_id: &str) -> bool {
150        if let Some(pos) = self.contacts.iter().position(|c| c.peer_id == peer_id) {
151            self.contacts.remove(pos);
152            true
153        } else {
154            false
155        }
156    }
157}
158
159/// The routing table: 256 buckets keyed by shared-prefix length with `local_key`.
160///
161/// Bucket `i` holds contacts whose XOR distance from this node has
162/// [`bucket_index`](crate::key::Distance::bucket_index) `i` — i.e. contacts that share exactly
163/// `255 - i` leading bits with this node. Contacts closer in the tree land in low-index buckets.
164#[derive(Debug, Clone)]
165pub struct RoutingTable {
166    local_key: Key,
167    buckets: Vec<KBucket>,
168    k: usize,
169}
170
171impl RoutingTable {
172    /// A new empty table for the node whose id is `local_id`, with bucket size `k`.
173    pub fn new(local_id: &PeerId, k: usize) -> Self {
174        RoutingTable {
175            local_key: Key::from_peer_id(local_id),
176            buckets: (0..256).map(|_| KBucket::new(k)).collect(),
177            k,
178        }
179    }
180
181    /// This node's own key.
182    pub fn local_key(&self) -> Key {
183        self.local_key
184    }
185
186    /// The bucket index a contact with key `key` falls in (`None` for our own key).
187    fn bucket_for(&self, key: &Key) -> Option<usize> {
188        self.local_key.distance(key).bucket_index()
189    }
190
191    /// Offer a contact to the table. A contact whose key equals ours is ignored
192    /// ([`InsertOutcome::Inserted`], a no-op). Otherwise routes to the right bucket and applies the
193    /// LRS policy — see [`InsertOutcome`].
194    pub fn insert(&mut self, contact: Contact) -> InsertOutcome {
195        let Some(key) = contact.key() else {
196            // Malformed peer_id — reject silently as "inserted" (nothing to do; never a Full).
197            return InsertOutcome::Inserted;
198        };
199        match self.bucket_for(&key) {
200            None => InsertOutcome::Inserted, // our own id; never stored
201            Some(idx) => self.buckets[idx].offer(contact),
202        }
203    }
204
205    /// Complete a ping-and-replace: after an [`InsertOutcome::Full`] whose `lru` did not respond,
206    /// evict `lru` and insert `candidate`. Returns whether the eviction happened (false if the LRU
207    /// slot changed in the meantime — then `candidate` is dropped, per Kademlia).
208    pub fn replace(&mut self, lru: &Contact, candidate: Contact) -> bool {
209        let Some(key) = candidate.key() else {
210            return false;
211        };
212        match self.bucket_for(&key) {
213            None => false,
214            Some(idx) => self.buckets[idx].replace(&lru.peer_id, candidate),
215        }
216    }
217
218    /// Remove a contact (e.g. a peer that failed a liveness ping). Returns whether it was present.
219    pub fn remove(&mut self, peer_id: &str) -> bool {
220        let Some(pid) = PeerId::from_hex(peer_id) else {
221            return false;
222        };
223        match self.bucket_for(&Key::from_peer_id(&pid)) {
224            None => false,
225            Some(idx) => self.buckets[idx].remove(peer_id),
226        }
227    }
228
229    /// The `k` contacts closest (XOR distance) to `target` across all buckets, closest-first.
230    ///
231    /// This is the seed set for an iterative lookup and the answer to a `find_node`. It scans all
232    /// buckets (256 × ≤ k contacts) and returns the `k` with the smallest distance to `target`.
233    pub fn closest(&self, target: &Key) -> Vec<Contact> {
234        let mut all: Vec<(Contact, crate::key::Distance)> = self
235            .buckets
236            .iter()
237            .flat_map(|b| b.contacts.iter())
238            .filter_map(|c| c.key().map(|k| (c.clone(), target.distance(&k))))
239            .collect();
240        all.sort_by_key(|(_, dist)| *dist);
241        all.into_iter().take(self.k).map(|(c, _)| c).collect()
242    }
243
244    /// Total contacts stored across all buckets (diagnostics / tests).
245    pub fn len(&self) -> usize {
246        self.buckets.iter().map(|b| b.contacts.len()).sum()
247    }
248
249    /// Whether the table holds no contacts.
250    pub fn is_empty(&self) -> bool {
251        self.len() == 0
252    }
253
254    /// Indices of buckets that currently hold at least one contact (used by bucket-refresh to focus
255    /// on populated regions; empty buckets far from the node are never realistically filled).
256    pub fn non_empty_bucket_indices(&self) -> Vec<usize> {
257        self.buckets
258            .iter()
259            .enumerate()
260            .filter(|(_, b)| !b.contacts.is_empty())
261            .map(|(i, _)| i)
262            .collect()
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::record::{AddressKind, CandidateAddr};
270
271    fn pid(bytes: [u8; 32]) -> PeerId {
272        PeerId::from_bytes(bytes)
273    }
274
275    fn contact(bytes: [u8; 32]) -> Contact {
276        Contact::new(&pid(bytes), vec![CandidateAddr::direct("h", 1)])
277    }
278
279    fn local() -> PeerId {
280        pid([0u8; 32])
281    }
282
283    #[test]
284    fn insert_and_len() {
285        let mut t = RoutingTable::new(&local(), 20);
286        assert!(t.is_empty());
287        let mut b = [0u8; 32];
288        b[0] = 0x80;
289        assert_eq!(t.insert(contact(b)), InsertOutcome::Inserted);
290        assert_eq!(t.len(), 1);
291    }
292
293    #[test]
294    fn own_id_is_never_stored() {
295        let mut t = RoutingTable::new(&local(), 20);
296        assert_eq!(t.insert(contact([0u8; 32])), InsertOutcome::Inserted);
297        assert_eq!(t.len(), 0);
298    }
299
300    #[test]
301    fn reinsert_refreshes_recency_not_count() {
302        let mut t = RoutingTable::new(&local(), 20);
303        let mut b = [0u8; 32];
304        b[0] = 0x01;
305        t.insert(contact(b));
306        t.insert(contact(b));
307        assert_eq!(t.len(), 1);
308    }
309
310    #[test]
311    fn full_bucket_reports_lru_and_does_not_grow() {
312        // Force all contacts into the SAME bucket (index 255 = MSB of distance set) with k = 2.
313        // Keys with the top bit set share bucket 255 relative to local id 0.
314        let mut t = RoutingTable::new(&local(), 2);
315        let mut a = [0u8; 32];
316        a[0] = 0x80;
317        a[31] = 0x01;
318        let mut b = [0u8; 32];
319        b[0] = 0x80;
320        b[31] = 0x02;
321        let mut c = [0u8; 32];
322        c[0] = 0x80;
323        c[31] = 0x03;
324        assert_eq!(t.insert(contact(a)), InsertOutcome::Inserted);
325        assert_eq!(t.insert(contact(b)), InsertOutcome::Inserted);
326        // Third into the full k=2 bucket → Full, naming the LRS (first inserted = `a`).
327        match t.insert(contact(c)) {
328            InsertOutcome::Full { lru } => assert_eq!(lru, contact(a)),
329            other => panic!("expected Full, got {other:?}"),
330        }
331        assert_eq!(t.len(), 2, "full bucket must not grow past k");
332    }
333
334    #[test]
335    fn replace_evicts_lru_for_candidate() {
336        let mut t = RoutingTable::new(&local(), 2);
337        let mut a = [0u8; 32];
338        a[0] = 0x80;
339        a[31] = 0x01;
340        let mut b = [0u8; 32];
341        b[0] = 0x80;
342        b[31] = 0x02;
343        let mut c = [0u8; 32];
344        c[0] = 0x80;
345        c[31] = 0x03;
346        t.insert(contact(a));
347        t.insert(contact(b));
348        let lru = match t.insert(contact(c)) {
349            InsertOutcome::Full { lru } => lru,
350            other => panic!("expected Full, got {other:?}"),
351        };
352        // LRU `a` "did not respond" → replace with candidate `c`.
353        assert!(t.replace(&lru, contact(c)));
354        assert_eq!(t.len(), 2);
355        // `a` gone, `c` present.
356        let keys: Vec<String> = t
357            .buckets
358            .iter()
359            .flat_map(|bk| bk.contacts.iter())
360            .map(|x| x.peer_id.clone())
361            .collect();
362        assert!(!keys.contains(&contact(a).peer_id));
363        assert!(keys.contains(&contact(c).peer_id));
364    }
365
366    #[test]
367    fn replace_is_noop_if_lru_slot_changed() {
368        let mut t = RoutingTable::new(&local(), 2);
369        let mut a = [0u8; 32];
370        a[0] = 0x80;
371        a[31] = 0x01;
372        let mut b = [0u8; 32];
373        b[0] = 0x80;
374        b[31] = 0x02;
375        let mut c = [0u8; 32];
376        c[0] = 0x80;
377        c[31] = 0x03;
378        t.insert(contact(a));
379        t.insert(contact(b));
380        let lru = match t.insert(contact(c)) {
381            InsertOutcome::Full { lru } => lru,
382            _ => unreachable!(),
383        };
384        // Simulate the LRU (`a`) responding to a ping → it is refreshed to the back.
385        t.insert(contact(a));
386        // Now the front is `b`, not `a`; replace(a, c) must be a no-op and drop `c`.
387        assert!(!t.replace(&lru, contact(c)));
388        assert_eq!(t.len(), 2);
389    }
390
391    #[test]
392    fn remove_contact() {
393        let mut t = RoutingTable::new(&local(), 20);
394        let mut b = [0u8; 32];
395        b[0] = 0x40;
396        let c = contact(b);
397        t.insert(c.clone());
398        assert!(t.remove(&c.peer_id));
399        assert!(!t.remove(&c.peer_id));
400        assert!(t.is_empty());
401    }
402
403    #[test]
404    fn closest_returns_k_sorted_by_distance() {
405        let mut t = RoutingTable::new(&local(), 20);
406        // Insert contacts at varied distances.
407        for i in 1u8..=10 {
408            let mut b = [0u8; 32];
409            b[0] = i; // distinct top byte → distinct distance from local 0
410            t.insert(contact(b));
411        }
412        let target = Key::from_bytes([0u8; 32]);
413        let closest = t.closest(&target);
414        assert_eq!(closest.len(), 10);
415        // Verify ascending distance order.
416        let dists: Vec<_> = closest
417            .iter()
418            .map(|c| target.distance(&c.key().unwrap()))
419            .collect();
420        for w in dists.windows(2) {
421            assert!(w[0] <= w[1], "closest must be distance-sorted");
422        }
423    }
424
425    #[test]
426    fn closest_caps_at_k() {
427        let mut t = RoutingTable::new(&local(), 3);
428        for i in 1u8..=10 {
429            let mut b = [0u8; 32];
430            b[0] = i;
431            t.insert(contact(b));
432        }
433        assert_eq!(t.closest(&Key::from_bytes([0u8; 32])).len(), 3);
434    }
435
436    #[test]
437    fn non_empty_bucket_indices_tracks_population() {
438        let mut t = RoutingTable::new(&local(), 20);
439        assert!(t.non_empty_bucket_indices().is_empty());
440        let mut b = [0u8; 32];
441        b[0] = 0x80; // bucket 255
442        t.insert(contact(b));
443        assert_eq!(t.non_empty_bucket_indices(), vec![255]);
444    }
445
446    #[test]
447    fn contact_key_and_best_address() {
448        let c = contact({
449            let mut b = [0u8; 32];
450            b[0] = 0x22;
451            b
452        });
453        assert!(c.key().is_some());
454        assert_eq!(c.best_address().unwrap().kind, AddressKindDirect());
455    }
456
457    // Small helper to avoid importing AddressKind into every assert.
458    #[allow(non_snake_case)]
459    fn AddressKindDirect() -> crate::record::AddressKind {
460        crate::record::AddressKind::Direct
461    }
462
463    #[test]
464    fn contact_new_sorts_addresses_ipv6_first() {
465        let c = Contact::new(
466            &pid([1u8; 32]),
467            vec![
468                CandidateAddr::direct("203.0.113.7", 9444), // IPv4 direct, fed first
469                CandidateAddr::direct("2001:db8::1", 9444), // IPv6 direct, fed second
470                CandidateAddr {
471                    host: "198.51.100.2".into(),
472                    port: 1,
473                    kind: AddressKind::Reflexive,
474                },
475                CandidateAddr {
476                    host: "2001:db8::2".into(),
477                    port: 1,
478                    kind: AddressKind::Reflexive,
479                },
480            ],
481        );
482        let hosts: Vec<&str> = c.addresses.iter().map(|a| a.host.as_str()).collect();
483        assert_eq!(
484            hosts,
485            vec!["2001:db8::1", "2001:db8::2", "203.0.113.7", "198.51.100.2"],
486            "contact addresses must be IPv6-first, then ranked by AddressKind"
487        );
488    }
489
490    #[test]
491    fn contact_best_address_prefers_ipv6_over_ipv4_at_same_rank() {
492        let c = Contact::new(
493            &pid([1u8; 32]),
494            vec![
495                CandidateAddr::direct("203.0.113.7", 9444), // IPv4 direct, fed first
496                CandidateAddr::direct("2001:db8::1", 9444), // IPv6 direct, fed second
497            ],
498        );
499        assert_eq!(c.best_address().unwrap().host, "2001:db8::1");
500    }
501
502    #[test]
503    fn contact_new_caps_addresses_at_the_constant() {
504        // MEDIUM (SECURITY_AUDIT_P2P.md #179): a contact must never carry an unbounded address
505        // list — memory blowup + outbound amplification when re-served to every querying peer.
506        let many: Vec<CandidateAddr> = (0..1000)
507            .map(|i| CandidateAddr::direct(format!("203.0.113.{}", i % 255), 9444))
508            .collect();
509        let c = Contact::new(&pid([1u8; 32]), many);
510        assert_eq!(c.addresses.len(), crate::record::MAX_ADDRESSES_PER_RECORD);
511    }
512
513    #[test]
514    fn contact_deserialization_bounds_the_address_count() {
515        // #1514: a `Contact` off the wire is folded into the routing table AND re-served to every
516        // peer that queries us, so the address bound must hold at the decode boundary itself — not
517        // only at whichever ingest site remembered to cap it.
518        let addrs: Vec<String> = (0..1000)
519            .map(|i| {
520                format!(
521                    r#"{{"host":"198.51.100.{}","port":1,"kind":"relay"}}"#,
522                    i % 255
523                )
524            })
525            .collect();
526        let json = format!(
527            r#"{{"peer_id":"{}","addresses":[{}]}}"#,
528            "cc".repeat(32),
529            addrs.join(",")
530        );
531        let c: Contact = serde_json::from_str(&json).unwrap();
532        assert_eq!(c.addresses.len(), crate::record::MAX_ADDRESSES_PER_RECORD);
533    }
534
535    #[test]
536    fn contact_dial_candidates_order_v6_then_v4_then_unresolvable() {
537        // #1594: a contact is dialed exactly like a provider, so it must expose the SAME ordered
538        // candidate list rather than leaving each consumer to re-derive §5.2.
539        let c = Contact::new(
540            &pid([1u8; 32]),
541            vec![
542                CandidateAddr::direct("not-a-literal", 9444),
543                CandidateAddr::direct("203.0.113.7", 9444),
544                CandidateAddr::direct("2001:db8::1", 9444),
545                CandidateAddr::relay_marker(),
546            ],
547        );
548        let hosts: Vec<&str> = c
549            .dial_candidates()
550            .iter()
551            .map(|a| a.host.as_str())
552            .collect();
553        assert_eq!(hosts, vec!["2001:db8::1", "203.0.113.7", "not-a-literal"]);
554    }
555}