1use std::collections::HashMap;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16use crate::mesh_discovery::{DiscoveredPeer, DiscoveryBackend, DiscoveryError, DiscoverySource};
17
18pub const K_BUCKET_SIZE: usize = 20;
22
23pub const ALPHA: usize = 3;
25
26pub const KEY_BITS: usize = 256;
28
29pub type DhtKey = [u8; 32];
33
34pub 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
43pub 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
55pub fn bucket_index(distance: &DhtKey) -> usize {
60 let lz = leading_zeros(distance);
61 if lz >= 256 { 0 } else { 255 - lz }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68pub struct DhtEntry {
69 pub key: String,
71 pub node_id: String,
73 pub address: String,
75 pub platform: String,
77 pub last_seen: u64,
79 pub governance_genesis_prefix: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct NamespacedDhtKey {
93 pub genesis_prefix: String,
95 pub key_type: String,
97 pub name: String,
99}
100
101impl NamespacedDhtKey {
102 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 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 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 pub fn to_key_string(&self) -> String {
131 format!("{}:{}:{}", self.key_type, self.genesis_prefix, self.name)
132 }
133
134 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 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
155pub struct KademliaTable {
159 local_key: DhtKey,
161 buckets: Vec<Vec<DhtEntry>>,
163 records: HashMap<String, DhtEntry>,
165}
166
167impl KademliaTable {
168 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 pub fn local_key(&self) -> &DhtKey {
179 &self.local_key
180 }
181
182 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 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 if bucket.len() < K_BUCKET_SIZE {
197 bucket.push(entry.clone());
198 }
199 self.records.insert(entry.node_id.clone(), entry);
205 }
206
207 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 pub fn put(&mut self, key: String, entry: DhtEntry) {
228 self.records.insert(key, entry);
229 }
230
231 pub fn get(&self, key: &str) -> Option<&DhtEntry> {
233 self.records.get(key)
234 }
235
236 pub fn peer_count(&self) -> usize {
238 self.records.len()
239 }
240
241 pub fn bucket_len(&self, idx: usize) -> usize {
243 self.buckets.get(idx).map_or(0, |b| b.len())
244 }
245}
246
247pub 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 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 pub fn add_bootstrap_peer(&mut self, peer_key: DhtKey, entry: DhtEntry) {
285 self.table.add_peer(peer_key, entry);
286 }
287
288 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 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 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 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 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 pub fn table(&self) -> &KademliaTable {
327 &self.table
328 }
329
330 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 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#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[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 #[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; 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; assert_eq!(leading_zeros(&key), 21);
437 }
438
439 #[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; 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; assert_eq!(bucket_index(&key), 0);
458 }
459
460 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 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]; 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 for i in 0..(K_BUCKET_SIZE + 5) {
540 let mut pk = [0u8; 32];
541 pk[0] = 0x80; pk[1] = i as u8; table.add_peer(
544 pk,
545 make_entry(&format!("peer-{i}"), &format!("10.0.0.{i}:9489")),
546 );
547 }
548
549 assert_eq!(table.bucket_len(255), K_BUCKET_SIZE);
551 assert_eq!(table.peer_count(), K_BUCKET_SIZE + 5);
553 }
554
555 #[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 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 #[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 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 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 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 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}