Skip to main content

clawft_kernel/
mesh_kad.rs

1//! Kademlia DHT for WeftOS mesh peer discovery (K6.2).
2//!
3//! Implements a simplified Kademlia Distributed Hash Table for
4//! wide-area peer discovery. Uses XOR distance metric on 256-bit
5//! node keys and k-bucket routing tables.
6//!
7//! DHT keys are **namespaced** with the governance genesis hash prefix
8//! so that nodes from different governance clusters occupy disjoint
9//! key-spaces.
10
11use std::collections::HashMap;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16use crate::mesh_discovery::{DiscoveredPeer, DiscoveryBackend, DiscoveryError, DiscoverySource};
17
18// ── Constants ────────────────────────────────────────────────────
19
20/// Maximum peers per k-bucket.
21pub const K_BUCKET_SIZE: usize = 20;
22
23/// Parallel lookup factor.
24pub const ALPHA: usize = 3;
25
26/// Key-space size in bits (SHA-256).
27pub const KEY_BITS: usize = 256;
28
29// ── Key primitives ───────────────────────────────────────────────
30
31/// A 256-bit DHT key.
32pub type DhtKey = [u8; 32];
33
34/// XOR distance between two DHT keys.
35pub fn xor_distance(a: &DhtKey, b: &DhtKey) -> DhtKey {
36    let mut result = [0u8; 32];
37    for i in 0..32 {
38        result[i] = a[i] ^ b[i];
39    }
40    result
41}
42
43/// Count leading zero bits in a DHT key (0..=256).
44///
45/// Returns 256 for the all-zero key.
46pub fn leading_zeros(key: &DhtKey) -> usize {
47    for (i, &byte) in key.iter().enumerate() {
48        if byte != 0 {
49            return i * 8 + byte.leading_zeros() as usize;
50        }
51    }
52    256
53}
54
55/// Bucket index for a given XOR distance.
56///
57/// Bucket 0 is the *farthest* (MSB set), bucket 255 is the *closest*
58/// (only the LSB set). Returns 0 for the all-zero distance (self).
59pub fn bucket_index(distance: &DhtKey) -> usize {
60    let lz = leading_zeros(distance);
61    if lz >= 256 { 0 } else { 255 - lz }
62}
63
64// ── DHT entry ────────────────────────────────────────────────────
65
66/// A record stored in the DHT.
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68pub struct DhtEntry {
69    /// Record key string.
70    pub key: String,
71    /// Owning node identifier.
72    pub node_id: String,
73    /// Network address.
74    pub address: String,
75    /// Platform type.
76    pub platform: String,
77    /// Unix timestamp of last announcement.
78    pub last_seen: u64,
79    /// Governance genesis hash prefix (first 16 hex chars).
80    pub governance_genesis_prefix: String,
81}
82
83// ── Namespaced DHT keys ──────────────────────────────────────────
84
85/// A governance-namespaced DHT key.
86///
87/// Format: `<key_type>:<genesis_prefix>:<name>`
88///
89/// This ensures that nodes from different governance clusters never
90/// collide in the DHT keyspace.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct NamespacedDhtKey {
93    /// Governance genesis hash prefix (first 16 hex chars).
94    pub genesis_prefix: String,
95    /// Key type (`svc`, `node`, `agent`).
96    pub key_type: String,
97    /// Key name.
98    pub name: String,
99}
100
101impl NamespacedDhtKey {
102    /// Create a service key: `svc:<genesis[0..16]>:<name>`.
103    pub fn service(genesis_hash: &str, name: &str) -> Self {
104        Self {
105            genesis_prefix: genesis_hash.chars().take(16).collect(),
106            key_type: "svc".into(),
107            name: name.into(),
108        }
109    }
110
111    /// Create a node presence key: `node:<genesis[0..16]>:<node_id>`.
112    pub fn node(genesis_hash: &str, node_id: &str) -> Self {
113        Self {
114            genesis_prefix: genesis_hash.chars().take(16).collect(),
115            key_type: "node".into(),
116            name: node_id.into(),
117        }
118    }
119
120    /// Create an agent key: `agent:<genesis[0..16]>:<agent_id>`.
121    pub fn agent(genesis_hash: &str, agent_id: &str) -> Self {
122        Self {
123            genesis_prefix: genesis_hash.chars().take(16).collect(),
124            key_type: "agent".into(),
125            name: agent_id.into(),
126        }
127    }
128
129    /// Format as a string key.
130    pub fn to_key_string(&self) -> String {
131        format!("{}:{}:{}", self.key_type, self.genesis_prefix, self.name)
132    }
133
134    /// Parse from a string key.
135    pub fn from_key_string(s: &str) -> Option<Self> {
136        let parts: Vec<&str> = s.splitn(3, ':').collect();
137        if parts.len() == 3 {
138            Some(Self {
139                key_type: parts[0].into(),
140                genesis_prefix: parts[1].into(),
141                name: parts[2].into(),
142            })
143        } else {
144            None
145        }
146    }
147
148    /// Check if this key belongs to a specific governance cluster.
149    pub fn matches_genesis(&self, genesis_hash: &str) -> bool {
150        let prefix: String = genesis_hash.chars().take(16).collect();
151        self.genesis_prefix == prefix
152    }
153}
154
155// ── Kademlia routing table ───────────────────────────────────────
156
157/// Simplified Kademlia routing table with k-buckets.
158pub struct KademliaTable {
159    /// Our node's key.
160    local_key: DhtKey,
161    /// k-buckets indexed by distance (256 buckets, 0 = closest).
162    buckets: Vec<Vec<DhtEntry>>,
163    /// All stored records (keyed by string key).
164    records: HashMap<String, DhtEntry>,
165}
166
167impl KademliaTable {
168    /// Create a new routing table with the given local key.
169    pub fn new(local_key: DhtKey) -> Self {
170        Self {
171            local_key,
172            buckets: (0..256).map(|_| Vec::new()).collect(),
173            records: HashMap::new(),
174        }
175    }
176
177    /// Our local DHT key.
178    pub fn local_key(&self) -> &DhtKey {
179        &self.local_key
180    }
181
182    /// Add a peer to the routing table.
183    pub fn add_peer(&mut self, peer_key: DhtKey, entry: DhtEntry) {
184        let dist = xor_distance(&self.local_key, &peer_key);
185        let idx = bucket_index(&dist);
186        let bucket = &mut self.buckets[idx];
187
188        // Update if already present.
189        if let Some(existing) = bucket.iter_mut().find(|e| e.node_id == entry.node_id) {
190            *existing = entry.clone();
191            self.records.insert(entry.node_id.clone(), entry);
192            return;
193        }
194
195        // Add if bucket not full.
196        if bucket.len() < K_BUCKET_SIZE {
197            bucket.push(entry.clone());
198        }
199        // If full, the entry is still stored in `records` for lookup
200        // but not in the routing bucket (standard Kademlia: evict only
201        // if the tail peer is unreachable, which we skip in this
202        // simplified implementation).
203
204        self.records.insert(entry.node_id.clone(), entry);
205    }
206
207    /// Find the `k` closest peers to a target key.
208    pub fn find_closest(&self, target: &DhtKey, k: usize) -> Vec<&DhtEntry> {
209        let mut all_entries: Vec<(&DhtEntry, DhtKey)> = self
210            .records
211            .values()
212            .map(|e| {
213                let mut key = [0u8; 32];
214                let bytes = e.node_id.as_bytes();
215                let len = bytes.len().min(32);
216                key[..len].copy_from_slice(&bytes[..len]);
217                let dist = xor_distance(target, &key);
218                (e, dist)
219            })
220            .collect();
221
222        all_entries.sort_by_key(|a| a.1);
223        all_entries.into_iter().take(k).map(|(e, _)| e).collect()
224    }
225
226    /// Store a record in the DHT.
227    pub fn put(&mut self, key: String, entry: DhtEntry) {
228        self.records.insert(key, entry);
229    }
230
231    /// Retrieve a record from the DHT by key.
232    pub fn get(&self, key: &str) -> Option<&DhtEntry> {
233        self.records.get(key)
234    }
235
236    /// Number of peers/records in the table.
237    pub fn peer_count(&self) -> usize {
238        self.records.len()
239    }
240
241    /// Number of entries in a specific bucket.
242    pub fn bucket_len(&self, idx: usize) -> usize {
243        self.buckets.get(idx).map_or(0, |b| b.len())
244    }
245}
246
247// ── Kademlia discovery backend ───────────────────────────────────
248
249/// Kademlia DHT discovery backend.
250///
251/// Manages a local routing table and provides service/node registration
252/// with governance-namespaced keys.
253pub struct KademliaDiscovery {
254    table: KademliaTable,
255    governance_genesis: String,
256    local_node_id: String,
257    local_address: String,
258    platform: String,
259    pending: Vec<DiscoveredPeer>,
260    active: bool,
261}
262
263impl KademliaDiscovery {
264    /// Create a new Kademlia discovery backend.
265    pub fn new(
266        local_key: DhtKey,
267        node_id: String,
268        address: String,
269        platform: String,
270        governance_genesis: String,
271    ) -> Self {
272        Self {
273            table: KademliaTable::new(local_key),
274            governance_genesis,
275            local_node_id: node_id,
276            local_address: address,
277            platform,
278            pending: Vec::new(),
279            active: false,
280        }
281    }
282
283    /// Add a bootstrap peer to the routing table.
284    pub fn add_bootstrap_peer(&mut self, peer_key: DhtKey, entry: DhtEntry) {
285        self.table.add_peer(peer_key, entry);
286    }
287
288    /// Store a service in the DHT with governance namespacing.
289    pub fn put_service(&mut self, service_name: &str, entry: DhtEntry) {
290        let key = NamespacedDhtKey::service(&self.governance_genesis, service_name);
291        self.table.put(key.to_key_string(), entry);
292    }
293
294    /// Resolve a service from the DHT.
295    pub fn get_service(&self, service_name: &str) -> Option<&DhtEntry> {
296        let key = NamespacedDhtKey::service(&self.governance_genesis, service_name);
297        self.table.get(&key.to_key_string())
298    }
299
300    /// Store a node presence record in the DHT.
301    pub fn put_node(&mut self, node_id: &str, entry: DhtEntry) {
302        let key = NamespacedDhtKey::node(&self.governance_genesis, node_id);
303        self.table.put(key.to_key_string(), entry);
304    }
305
306    /// Resolve a node from the DHT.
307    pub fn get_node(&self, node_id: &str) -> Option<&DhtEntry> {
308        let key = NamespacedDhtKey::node(&self.governance_genesis, node_id);
309        self.table.get(&key.to_key_string())
310    }
311
312    /// Add a peer discovered externally (e.g. from mDNS) into the
313    /// DHT and queue it for delivery via `poll`.
314    pub fn inject_peer(&mut self, peer_key: DhtKey, entry: DhtEntry) {
315        let peer = DiscoveredPeer {
316            node_id: entry.node_id.clone(),
317            address: entry.address.clone(),
318            platform: entry.platform.clone(),
319            source: DiscoverySource::Kademlia,
320        };
321        self.table.add_peer(peer_key, entry);
322        self.pending.push(peer);
323    }
324
325    /// Access the routing table.
326    pub fn table(&self) -> &KademliaTable {
327        &self.table
328    }
329
330    /// Governance genesis hash.
331    pub fn governance_genesis(&self) -> &str {
332        &self.governance_genesis
333    }
334}
335
336#[async_trait]
337impl DiscoveryBackend for KademliaDiscovery {
338    fn name(&self) -> &str {
339        "kademlia"
340    }
341
342    async fn start(&mut self) -> Result<(), DiscoveryError> {
343        self.active = true;
344
345        // Register ourselves in the DHT under the governance namespace.
346        let self_key = NamespacedDhtKey::node(&self.governance_genesis, &self.local_node_id);
347        self.table.put(
348            self_key.to_key_string(),
349            DhtEntry {
350                key: self_key.to_key_string(),
351                node_id: self.local_node_id.clone(),
352                address: self.local_address.clone(),
353                platform: self.platform.clone(),
354                last_seen: std::time::SystemTime::now()
355                    .duration_since(std::time::UNIX_EPOCH)
356                    .unwrap_or_default()
357                    .as_secs(),
358                governance_genesis_prefix: self.governance_genesis.chars().take(16).collect(),
359            },
360        );
361        Ok(())
362    }
363
364    async fn poll(&mut self) -> Vec<DiscoveredPeer> {
365        if !self.active {
366            return vec![];
367        }
368        std::mem::take(&mut self.pending)
369    }
370
371    async fn stop(&mut self) -> Result<(), DiscoveryError> {
372        self.active = false;
373        self.pending.clear();
374        Ok(())
375    }
376}
377
378// ── Tests ────────────────────────────────────────────────────────
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    // ── XOR distance ─────────────────────────────────────────────
385
386    #[test]
387    fn xor_distance_identity() {
388        let key = [0xABu8; 32];
389        let dist = xor_distance(&key, &key);
390        assert_eq!(dist, [0u8; 32], "distance to self must be zero");
391    }
392
393    #[test]
394    fn xor_distance_symmetric() {
395        let a = [0x01u8; 32];
396        let b = [0xFFu8; 32];
397        assert_eq!(xor_distance(&a, &b), xor_distance(&b, &a));
398    }
399
400    #[test]
401    fn xor_distance_known_value() {
402        let mut a = [0u8; 32];
403        let mut b = [0u8; 32];
404        a[0] = 0b1010_0000;
405        b[0] = 0b0110_0000;
406        let dist = xor_distance(&a, &b);
407        assert_eq!(dist[0], 0b1100_0000);
408        assert_eq!(dist[1..], [0u8; 31]);
409    }
410
411    // ── Leading zeros ────────────────────────────────────────────
412
413    #[test]
414    fn leading_zeros_all_zero() {
415        assert_eq!(leading_zeros(&[0u8; 32]), 256);
416    }
417
418    #[test]
419    fn leading_zeros_first_bit_set() {
420        let mut key = [0u8; 32];
421        key[0] = 0x80; // 1000_0000
422        assert_eq!(leading_zeros(&key), 0);
423    }
424
425    #[test]
426    fn leading_zeros_last_bit_set() {
427        let mut key = [0u8; 32];
428        key[31] = 0x01;
429        assert_eq!(leading_zeros(&key), 255);
430    }
431
432    #[test]
433    fn leading_zeros_mid_byte() {
434        let mut key = [0u8; 32];
435        key[2] = 0x04; // 0000_0100 => 5 zeros in this byte, + 16 from first 2 bytes = 21
436        assert_eq!(leading_zeros(&key), 21);
437    }
438
439    // ── Bucket index ─────────────────────────────────────────────
440
441    #[test]
442    fn bucket_index_zero_distance() {
443        assert_eq!(bucket_index(&[0u8; 32]), 0);
444    }
445
446    #[test]
447    fn bucket_index_max_distance() {
448        let mut key = [0u8; 32];
449        key[0] = 0x80; // MSB set => leading_zeros = 0 => bucket 255
450        assert_eq!(bucket_index(&key), 255);
451    }
452
453    #[test]
454    fn bucket_index_lsb_only() {
455        let mut key = [0u8; 32];
456        key[31] = 0x01; // leading_zeros = 255 => bucket 0
457        assert_eq!(bucket_index(&key), 0);
458    }
459
460    // ── KademliaTable ────────────────────────────────────────────
461
462    fn make_entry(node_id: &str, addr: &str) -> DhtEntry {
463        DhtEntry {
464            key: node_id.to_string(),
465            node_id: node_id.to_string(),
466            address: addr.to_string(),
467            platform: "linux".into(),
468            last_seen: 1000,
469            governance_genesis_prefix: "abcdef0123456789".into(),
470        }
471    }
472
473    #[test]
474    fn table_add_and_get() {
475        let local_key = [0u8; 32];
476        let mut table = KademliaTable::new(local_key);
477
478        let mut peer_key = [0u8; 32];
479        peer_key[0] = 0x01;
480        let entry = make_entry("peer-1", "10.0.0.1:9489");
481
482        table.add_peer(peer_key, entry.clone());
483        assert_eq!(table.peer_count(), 1);
484
485        // Stored in records by node_id.
486        let found = table.get("peer-1").unwrap();
487        assert_eq!(found.address, "10.0.0.1:9489");
488    }
489
490    #[test]
491    fn table_add_peer_updates_existing() {
492        let local_key = [0u8; 32];
493        let mut table = KademliaTable::new(local_key);
494
495        let mut peer_key = [0u8; 32];
496        peer_key[0] = 0x01;
497
498        table.add_peer(peer_key, make_entry("peer-1", "old-addr"));
499        table.add_peer(peer_key, make_entry("peer-1", "new-addr"));
500
501        assert_eq!(table.peer_count(), 1);
502        assert_eq!(table.get("peer-1").unwrap().address, "new-addr");
503    }
504
505    #[test]
506    fn table_find_closest() {
507        let local_key = [0u8; 32];
508        let mut table = KademliaTable::new(local_key);
509
510        for i in 0..5u8 {
511            let mut pk = [0u8; 32];
512            pk[0] = i + 1;
513            table.add_peer(
514                pk,
515                make_entry(&format!("n-{i}"), &format!("10.0.0.{i}:9489")),
516            );
517        }
518
519        let target = [0u8; 32]; // same as local
520        let closest = table.find_closest(&target, 3);
521        assert_eq!(closest.len(), 3);
522    }
523
524    #[test]
525    fn table_put_get_by_string_key() {
526        let mut table = KademliaTable::new([0u8; 32]);
527        let entry = make_entry("some-node", "addr");
528        table.put("custom-key".into(), entry);
529        assert!(table.get("custom-key").is_some());
530        assert!(table.get("nonexistent").is_none());
531    }
532
533    #[test]
534    fn table_bucket_size_limit() {
535        let local_key = [0u8; 32];
536        let mut table = KademliaTable::new(local_key);
537
538        // Insert K_BUCKET_SIZE + 5 peers that all land in the same bucket.
539        for i in 0..(K_BUCKET_SIZE + 5) {
540            let mut pk = [0u8; 32];
541            pk[0] = 0x80; // all go to bucket 255
542            pk[1] = i as u8; // different keys
543            table.add_peer(
544                pk,
545                make_entry(&format!("peer-{i}"), &format!("10.0.0.{i}:9489")),
546            );
547        }
548
549        // Bucket 255 should be capped at K_BUCKET_SIZE.
550        assert_eq!(table.bucket_len(255), K_BUCKET_SIZE);
551        // But all are in records.
552        assert_eq!(table.peer_count(), K_BUCKET_SIZE + 5);
553    }
554
555    // ── NamespacedDhtKey ─────────────────────────────────────────
556
557    #[test]
558    fn namespaced_key_service() {
559        let key = NamespacedDhtKey::service("abcdef0123456789extra", "my-svc");
560        assert_eq!(key.to_key_string(), "svc:abcdef0123456789:my-svc");
561    }
562
563    #[test]
564    fn namespaced_key_node() {
565        let key = NamespacedDhtKey::node("deadbeefdeadbeefmore", "node-42");
566        assert_eq!(key.to_key_string(), "node:deadbeefdeadbeef:node-42");
567    }
568
569    #[test]
570    fn namespaced_key_agent() {
571        let key = NamespacedDhtKey::agent("1234567890abcdef", "agent-x");
572        assert_eq!(key.to_key_string(), "agent:1234567890abcdef:agent-x");
573    }
574
575    #[test]
576    fn namespaced_key_roundtrip() {
577        let original = NamespacedDhtKey::service("abcdef0123456789", "test-svc");
578        let s = original.to_key_string();
579        let parsed = NamespacedDhtKey::from_key_string(&s).unwrap();
580        assert_eq!(original, parsed);
581    }
582
583    #[test]
584    fn namespaced_key_parse_invalid() {
585        assert!(NamespacedDhtKey::from_key_string("nocolons").is_none());
586        assert!(NamespacedDhtKey::from_key_string("one:two").is_none());
587    }
588
589    #[test]
590    fn namespaced_key_matches_genesis() {
591        let key = NamespacedDhtKey::service("abcdef0123456789extra", "svc");
592        assert!(key.matches_genesis("abcdef0123456789extra"));
593        assert!(key.matches_genesis("abcdef0123456789different_suffix"));
594        assert!(!key.matches_genesis("0000000000000000"));
595    }
596
597    #[test]
598    fn governance_prefix_isolation() {
599        // Two different governance clusters should produce different keys.
600        let key_a = NamespacedDhtKey::service("aaaa000000000000", "api-gateway");
601        let key_b = NamespacedDhtKey::service("bbbb000000000000", "api-gateway");
602
603        assert_ne!(key_a.to_key_string(), key_b.to_key_string());
604        assert!(!key_a.matches_genesis("bbbb000000000000"));
605        assert!(!key_b.matches_genesis("aaaa000000000000"));
606    }
607
608    // ── KademliaDiscovery lifecycle ──────────────────────────────
609
610    #[tokio::test]
611    async fn kademlia_discovery_lifecycle() {
612        let mut disc = KademliaDiscovery::new(
613            [0u8; 32],
614            "kad-node-1".into(),
615            "10.0.0.1:9489".into(),
616            "linux".into(),
617            "deadbeef01234567abcdef".into(),
618        );
619
620        assert_eq!(disc.name(), "kademlia");
621        assert!(!disc.active);
622
623        disc.start().await.unwrap();
624        assert!(disc.active);
625
626        // Self-registration should have created a record.
627        let self_key = NamespacedDhtKey::node("deadbeef01234567abcdef", "kad-node-1");
628        assert!(disc.table().get(&self_key.to_key_string()).is_some());
629
630        disc.stop().await.unwrap();
631        assert!(!disc.active);
632    }
633
634    #[tokio::test]
635    async fn kademlia_put_get_service() {
636        let mut disc = KademliaDiscovery::new(
637            [0u8; 32],
638            "node-1".into(),
639            "10.0.0.1:9489".into(),
640            "linux".into(),
641            "aabbccdd00112233".into(),
642        );
643        disc.start().await.unwrap();
644
645        let entry = DhtEntry {
646            key: "api-gw".into(),
647            node_id: "provider-1".into(),
648            address: "10.0.0.5:8080".into(),
649            platform: "linux".into(),
650            last_seen: 2000,
651            governance_genesis_prefix: "aabbccdd00112233".into(),
652        };
653        disc.put_service("api-gateway", entry.clone());
654
655        let found = disc.get_service("api-gateway").unwrap();
656        assert_eq!(found.node_id, "provider-1");
657        assert_eq!(found.address, "10.0.0.5:8080");
658
659        // Different service name should not match.
660        assert!(disc.get_service("auth-service").is_none());
661    }
662
663    #[tokio::test]
664    async fn kademlia_inject_peer_queued() {
665        let mut disc = KademliaDiscovery::new(
666            [0u8; 32],
667            "local".into(),
668            "127.0.0.1:9489".into(),
669            "linux".into(),
670            "0000".into(),
671        );
672        disc.start().await.unwrap();
673
674        let mut pk = [0u8; 32];
675        pk[0] = 0x42;
676        disc.inject_peer(pk, make_entry("remote-peer", "10.0.0.9:9489"));
677
678        let peers = disc.poll().await;
679        assert_eq!(peers.len(), 1);
680        assert_eq!(peers[0].node_id, "remote-peer");
681        assert_eq!(peers[0].source, DiscoverySource::Kademlia);
682
683        // Second poll should be empty.
684        let second = disc.poll().await;
685        assert!(second.is_empty());
686    }
687
688    #[tokio::test]
689    async fn kademlia_poll_inactive_empty() {
690        let mut disc =
691            KademliaDiscovery::new([0u8; 32], "n".into(), "a".into(), "p".into(), "g".into());
692        // Not started.
693        let peers = disc.poll().await;
694        assert!(peers.is_empty());
695    }
696
697    #[test]
698    fn dht_entry_serde_roundtrip() {
699        let entry = make_entry("serde-node", "10.0.0.1:9489");
700        let json = serde_json::to_string(&entry).unwrap();
701        let back: DhtEntry = serde_json::from_str(&json).unwrap();
702        assert_eq!(entry, back);
703    }
704}