1use std::collections::VecDeque;
37
38pub const DEFAULT_K: usize = 20;
44
45pub const DEFAULT_ALPHA: usize = 3;
47
48pub const DEFAULT_REPLACEMENT_CACHE_SIZE: usize = 20;
50
51const MAX_FAILED_QUERIES: u32 = 3;
53
54const NUM_BUCKETS: usize = 256;
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
63pub struct NodeId(pub [u8; 32]);
64
65impl NodeId {
66 pub fn new(bytes: [u8; 32]) -> Self {
68 Self(bytes)
69 }
70
71 pub fn as_bytes(&self) -> &[u8; 32] {
73 &self.0
74 }
75
76 pub fn xor_distance(&self, other: &NodeId) -> [u8; 32] {
78 xor_distance(self, other)
79 }
80}
81
82impl std::fmt::Display for NodeId {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 for byte in &self.0 {
85 write!(f, "{byte:02x}")?;
86 }
87 Ok(())
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct BucketEntry {
98 pub node_id: NodeId,
100 pub address: String,
102 pub last_seen: u64,
104 pub rtt_ms: u64,
106 pub failed_queries: u32,
108}
109
110impl BucketEntry {
111 pub fn new(node_id: NodeId, address: String, rtt_ms: u64, last_seen: u64) -> Self {
113 Self {
114 node_id,
115 address,
116 last_seen,
117 rtt_ms,
118 failed_queries: 0,
119 }
120 }
121}
122
123#[derive(Clone, Debug)]
133pub struct KBucket {
134 pub entries: Vec<BucketEntry>,
137 pub replacement_cache: VecDeque<BucketEntry>,
139 pub max_size: usize,
141}
142
143impl KBucket {
144 pub fn new(max_size: usize) -> Self {
146 Self {
147 entries: Vec::with_capacity(max_size),
148 replacement_cache: VecDeque::new(),
149 max_size,
150 }
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.entries.is_empty()
156 }
157
158 pub fn len(&self) -> usize {
160 self.entries.len()
161 }
162
163 pub fn is_full(&self) -> bool {
165 self.entries.len() >= self.max_size
166 }
167
168 fn find_entry_index(&self, node_id: &NodeId) -> Option<usize> {
170 self.entries.iter().position(|e| &e.node_id == node_id)
171 }
172
173 fn find_cache_index(&self, node_id: &NodeId) -> Option<usize> {
175 self.replacement_cache
176 .iter()
177 .position(|e| &e.node_id == node_id)
178 }
179
180 pub fn add(&mut self, entry: BucketEntry) {
188 if let Some(idx) = self.find_entry_index(&entry.node_id) {
189 let mut existing = self.entries.remove(idx);
191 existing.last_seen = entry.last_seen;
192 existing.rtt_ms = entry.rtt_ms;
193 existing.failed_queries = 0; self.entries.push(existing);
195 return;
196 }
197
198 if !self.is_full() {
199 self.entries.push(entry);
200 return;
201 }
202
203 if let Some(idx) = self.find_cache_index(&entry.node_id) {
205 if let Some(cached) = self.replacement_cache.get_mut(idx) {
207 cached.last_seen = entry.last_seen;
208 cached.rtt_ms = entry.rtt_ms;
209 cached.failed_queries = 0;
210 }
211 } else {
212 if self.replacement_cache.len() >= DEFAULT_REPLACEMENT_CACHE_SIZE {
214 self.replacement_cache.pop_front();
215 }
216 self.replacement_cache.push_back(entry);
217 }
218 }
219
220 pub fn remove(&mut self, node_id: &NodeId) -> bool {
226 if let Some(idx) = self.find_entry_index(node_id) {
227 self.entries.remove(idx);
228 if let Some(replacement) = self.replacement_cache.pop_back() {
230 self.entries.push(replacement);
231 }
232 true
233 } else {
234 false
235 }
236 }
237
238 pub fn mark_failed(&mut self, node_id: &NodeId, now: u64) -> bool {
242 if let Some(idx) = self.find_entry_index(node_id) {
243 self.entries[idx].failed_queries += 1;
244 if self.entries[idx].failed_queries >= MAX_FAILED_QUERIES {
245 self.entries.remove(idx);
246 if let Some(replacement) = self.replacement_cache.pop_back() {
248 self.entries.push(replacement);
249 }
250 return true;
251 }
252 self.entries[idx].last_seen = now;
254 }
255 false
256 }
257
258 pub fn update_rtt(&mut self, node_id: &NodeId, rtt_ms: u64, now: u64) {
260 if let Some(idx) = self.find_entry_index(node_id) {
261 self.entries[idx].rtt_ms = rtt_ms;
262 self.entries[idx].last_seen = now;
263 self.entries[idx].failed_queries = 0;
264 }
265 }
266}
267
268#[derive(Clone, Debug)]
274pub struct RoutingTableStats {
275 pub total_nodes: usize,
277 pub non_empty_buckets: usize,
279 pub avg_rtt_ms: f64,
281 pub max_bucket_size: usize,
283}
284
285pub fn xor_distance(a: &NodeId, b: &NodeId) -> [u8; 32] {
291 let mut dist = [0u8; 32];
292 for (i, byte) in dist.iter_mut().enumerate() {
293 *byte = a.0[i] ^ b.0[i];
294 }
295 dist
296}
297
298pub fn bucket_index(local: &NodeId, target: &NodeId) -> usize {
304 let dist = xor_distance(local, target);
305 let mut leading = 0usize;
306 for byte in &dist {
307 if *byte == 0 {
308 leading += 8;
309 } else {
310 leading += byte.leading_zeros() as usize;
311 break;
312 }
313 }
314 leading.min(255)
315}
316
317pub struct RoutingTableManager {
333 pub buckets: Vec<KBucket>,
335 pub local_id: NodeId,
337 pub k: usize,
339 pub alpha: usize,
341}
342
343impl RoutingTableManager {
344 pub fn new(local_id: NodeId, k: usize, alpha: usize) -> Self {
352 let k = k.max(1); let buckets = (0..NUM_BUCKETS).map(|_| KBucket::new(k)).collect();
354 Self {
355 buckets,
356 local_id,
357 k,
358 alpha,
359 }
360 }
361
362 pub fn with_defaults(local_id: NodeId) -> Self {
364 Self::new(local_id, DEFAULT_K, DEFAULT_ALPHA)
365 }
366
367 pub fn add_node(&mut self, node_id: NodeId, address: String, rtt_ms: u64, now: u64) {
377 if node_id == self.local_id {
378 return;
379 }
380 let idx = bucket_index(&self.local_id, &node_id);
381 let entry = BucketEntry::new(node_id, address, rtt_ms, now);
382 self.buckets[idx].add(entry);
383 }
384
385 pub fn remove_node(&mut self, node_id: &NodeId) -> bool {
392 let idx = bucket_index(&self.local_id, node_id);
393 self.buckets[idx].remove(node_id)
394 }
395
396 pub fn mark_failed(&mut self, node_id: &NodeId, now: u64) {
401 let idx = bucket_index(&self.local_id, node_id);
402 self.buckets[idx].mark_failed(node_id, now);
403 }
404
405 pub fn update_rtt(&mut self, node_id: &NodeId, rtt_ms: u64, now: u64) {
407 let idx = bucket_index(&self.local_id, node_id);
408 self.buckets[idx].update_rtt(node_id, rtt_ms, now);
409 }
410
411 pub fn find_closest(&self, target: &NodeId, count: usize) -> Vec<BucketEntry> {
417 let mut candidates: Vec<([u8; 32], BucketEntry)> = self
419 .buckets
420 .iter()
421 .flat_map(|b| b.entries.iter())
422 .map(|e| (xor_distance(target, &e.node_id), e.clone()))
423 .collect();
424
425 candidates.sort_by_key(|a| a.0);
427 candidates.truncate(count);
428 candidates.into_iter().map(|(_, e)| e).collect()
429 }
430
431 pub fn bucket_sizes(&self) -> Vec<usize> {
435 self.buckets.iter().map(|b| b.len()).collect()
436 }
437
438 pub fn total_nodes(&self) -> usize {
440 self.buckets.iter().map(|b| b.len()).sum()
441 }
442
443 pub fn stats(&self) -> RoutingTableStats {
445 let total_nodes = self.total_nodes();
446 let non_empty_buckets = self.buckets.iter().filter(|b| !b.is_empty()).count();
447 let max_bucket_size = self.buckets.iter().map(|b| b.len()).max().unwrap_or(0);
448
449 let avg_rtt_ms = if total_nodes == 0 {
450 0.0
451 } else {
452 let rtt_sum: u64 = self
453 .buckets
454 .iter()
455 .flat_map(|b| b.entries.iter())
456 .map(|e| e.rtt_ms)
457 .sum();
458 rtt_sum as f64 / total_nodes as f64
459 };
460
461 RoutingTableStats {
462 total_nodes,
463 non_empty_buckets,
464 avg_rtt_ms,
465 max_bucket_size,
466 }
467 }
468
469 pub fn bucket_index_for(&self, target: &NodeId) -> usize {
473 bucket_index(&self.local_id, target)
474 }
475
476 pub fn iter_entries(&self) -> impl Iterator<Item = &BucketEntry> {
478 self.buckets.iter().flat_map(|b| b.entries.iter())
479 }
480
481 pub fn is_empty(&self) -> bool {
483 self.buckets.iter().all(|b| b.is_empty())
484 }
485}
486
487#[cfg(test)]
492mod tests {
493 use super::{
494 bucket_index, xor_distance, BucketEntry, KBucket, NodeId, RoutingTableManager, DEFAULT_K,
495 DEFAULT_REPLACEMENT_CACHE_SIZE, MAX_FAILED_QUERIES, NUM_BUCKETS,
496 };
497
498 struct Xorshift64 {
501 state: u64,
502 }
503
504 impl Xorshift64 {
505 fn new(seed: u64) -> Self {
506 Self {
508 state: if seed == 0 { 0xdeadbeef_cafebabe } else { seed },
509 }
510 }
511
512 fn next_u64(&mut self) -> u64 {
513 let mut x = self.state;
514 x ^= x << 13;
515 x ^= x >> 7;
516 x ^= x << 17;
517 self.state = x;
518 x
519 }
520
521 fn next_node_id(&mut self) -> NodeId {
522 let mut bytes = [0u8; 32];
523 for chunk in bytes.chunks_exact_mut(8) {
524 let val = self.next_u64().to_le_bytes();
525 chunk.copy_from_slice(&val);
526 }
527 NodeId(bytes)
528 }
529
530 fn next_rtt(&mut self) -> u64 {
531 (self.next_u64() % 500) + 1
532 }
533
534 fn next_ts(&mut self) -> u64 {
535 (self.next_u64() % 1_000_000) + 1_000_000
536 }
537 }
538
539 fn zero_id() -> NodeId {
542 NodeId([0u8; 32])
543 }
544
545 fn id_with_byte(byte_idx: usize, value: u8) -> NodeId {
547 let mut bytes = [0u8; 32];
548 bytes[byte_idx] = value;
549 NodeId(bytes)
550 }
551
552 fn make_rtm(k: usize) -> RoutingTableManager {
553 RoutingTableManager::new(zero_id(), k, 3)
554 }
555
556 fn dummy_entry(id: NodeId, rtt: u64, ts: u64) -> BucketEntry {
557 BucketEntry::new(id, format!("127.0.0.1:{}", rtt), rtt, ts)
558 }
559
560 #[test]
563 fn test_xor_distance_identical() {
564 let id = NodeId([0xABu8; 32]);
565 let dist = xor_distance(&id, &id);
566 assert_eq!(dist, [0u8; 32]);
567 }
568
569 #[test]
570 fn test_xor_distance_complementary() {
571 let a = NodeId([0xFF; 32]);
572 let b = NodeId([0x00; 32]);
573 let dist = xor_distance(&a, &b);
574 assert_eq!(dist, [0xFF; 32]);
575 }
576
577 #[test]
578 fn test_xor_distance_symmetry() {
579 let mut rng = Xorshift64::new(42);
580 let a = rng.next_node_id();
581 let b = rng.next_node_id();
582 assert_eq!(xor_distance(&a, &b), xor_distance(&b, &a));
583 }
584
585 #[test]
586 fn test_xor_distance_triangle_inequality() {
587 let mut rng = Xorshift64::new(7);
590 let a = rng.next_node_id();
591 assert_eq!(xor_distance(&a, &a), [0u8; 32]);
592 }
593
594 #[test]
597 fn test_bucket_index_equal_ids() {
598 let id = NodeId([0u8; 32]);
599 assert_eq!(bucket_index(&id, &id), 255);
601 }
602
603 #[test]
604 fn test_bucket_index_first_bit_differs() {
605 let local = zero_id();
606 let mut target_bytes = [0u8; 32];
607 target_bytes[0] = 0x80; let target = NodeId(target_bytes);
609 assert_eq!(bucket_index(&local, &target), 0);
610 }
611
612 #[test]
613 fn test_bucket_index_second_bit_differs() {
614 let local = zero_id();
615 let mut target_bytes = [0u8; 32];
616 target_bytes[0] = 0x40; let target = NodeId(target_bytes);
618 assert_eq!(bucket_index(&local, &target), 1);
619 }
620
621 #[test]
622 fn test_bucket_index_second_byte() {
623 let local = zero_id();
624 let mut target_bytes = [0u8; 32];
625 target_bytes[1] = 0x80; let target = NodeId(target_bytes);
627 assert_eq!(bucket_index(&local, &target), 8);
628 }
629
630 #[test]
631 fn test_bucket_index_last_byte() {
632 let local = zero_id();
633 let mut target_bytes = [0u8; 32];
634 target_bytes[31] = 0x01; let target = NodeId(target_bytes);
636 assert_eq!(bucket_index(&local, &target), 255);
637 }
638
639 #[test]
642 fn test_kbucket_add_up_to_capacity() {
643 let mut bucket = KBucket::new(3);
644 for i in 0..3u8 {
645 let id = id_with_byte(0, i + 1);
646 bucket.add(dummy_entry(id, 10, 1000));
647 }
648 assert_eq!(bucket.len(), 3);
649 assert!(bucket.replacement_cache.is_empty());
650 }
651
652 #[test]
653 fn test_kbucket_overflow_goes_to_cache() {
654 let mut bucket = KBucket::new(2);
655 bucket.add(dummy_entry(id_with_byte(0, 1), 10, 1000));
656 bucket.add(dummy_entry(id_with_byte(0, 2), 10, 1001));
657 bucket.add(dummy_entry(id_with_byte(0, 3), 10, 1002)); assert_eq!(bucket.len(), 2);
659 assert_eq!(bucket.replacement_cache.len(), 1);
660 }
661
662 #[test]
663 fn test_kbucket_lru_move_to_tail() {
664 let mut bucket = KBucket::new(4);
665 let id_a = id_with_byte(0, 1);
666 let id_b = id_with_byte(0, 2);
667 bucket.add(dummy_entry(id_a, 10, 1000));
668 bucket.add(dummy_entry(id_b, 10, 1001));
669 bucket.add(dummy_entry(id_a, 20, 2000));
671 assert_eq!(bucket.entries.last().map(|e| e.node_id), Some(id_a));
673 assert_eq!(bucket.entries.len(), 2);
674 }
675
676 #[test]
677 fn test_kbucket_lru_resets_failures() {
678 let mut bucket = KBucket::new(4);
679 let id = id_with_byte(0, 1);
680 bucket.add(dummy_entry(id, 10, 1000));
681 bucket.entries[0].failed_queries = 2;
683 bucket.add(dummy_entry(id, 15, 2000));
685 assert_eq!(bucket.entries.last().map(|e| e.failed_queries), Some(0));
686 }
687
688 #[test]
691 fn test_kbucket_remove_existing() {
692 let mut bucket = KBucket::new(4);
693 let id = id_with_byte(0, 1);
694 bucket.add(dummy_entry(id, 10, 1000));
695 assert!(bucket.remove(&id));
696 assert!(bucket.is_empty());
697 }
698
699 #[test]
700 fn test_kbucket_remove_absent() {
701 let mut bucket = KBucket::new(4);
702 assert!(!bucket.remove(&id_with_byte(0, 99)));
703 }
704
705 #[test]
706 fn test_kbucket_remove_promotes_replacement() {
707 let mut bucket = KBucket::new(2);
708 let id_a = id_with_byte(0, 1);
709 let id_b = id_with_byte(0, 2);
710 let id_c = id_with_byte(0, 3); bucket.add(dummy_entry(id_a, 10, 1000));
712 bucket.add(dummy_entry(id_b, 10, 1001));
713 bucket.add(dummy_entry(id_c, 10, 1002));
714 assert_eq!(bucket.replacement_cache.len(), 1);
715 bucket.remove(&id_a);
716 assert_eq!(bucket.len(), 2);
718 assert!(bucket.entries.iter().any(|e| e.node_id == id_c));
719 assert!(bucket.replacement_cache.is_empty());
720 }
721
722 #[test]
725 fn test_kbucket_mark_failed_eviction() {
726 let mut bucket = KBucket::new(4);
727 let id = id_with_byte(0, 1);
728 bucket.add(dummy_entry(id, 10, 1000));
729 bucket.mark_failed(&id, 1001);
731 bucket.mark_failed(&id, 1002);
732 assert!(!bucket.is_empty());
733 let evicted = bucket.mark_failed(&id, 1003);
735 assert!(evicted);
736 assert!(bucket.is_empty());
737 }
738
739 #[test]
740 fn test_kbucket_mark_failed_promotes_replacement() {
741 let mut bucket = KBucket::new(1);
742 let id_a = id_with_byte(0, 1);
743 let id_b = id_with_byte(0, 2);
744 bucket.add(dummy_entry(id_a, 10, 1000));
745 bucket.add(dummy_entry(id_b, 10, 1001)); for ts in 1..=3u64 {
748 bucket.mark_failed(&id_a, 1000 + ts);
749 }
750 assert_eq!(bucket.len(), 1);
752 assert_eq!(bucket.entries[0].node_id, id_b);
753 }
754
755 #[test]
758 fn test_kbucket_update_rtt() {
759 let mut bucket = KBucket::new(4);
760 let id = id_with_byte(0, 1);
761 bucket.add(dummy_entry(id, 100, 1000));
762 bucket.update_rtt(&id, 50, 2000);
763 let entry = bucket
764 .entries
765 .iter()
766 .find(|e| e.node_id == id)
767 .expect("test: entry should be found in bucket after add");
768 assert_eq!(entry.rtt_ms, 50);
769 assert_eq!(entry.last_seen, 2000);
770 assert_eq!(entry.failed_queries, 0);
771 }
772
773 #[test]
776 fn test_rtm_add_node_basic() {
777 let mut rtm = make_rtm(DEFAULT_K);
778 let id = id_with_byte(0, 0x80); rtm.add_node(id, "addr".to_string(), 10, 1000);
780 assert_eq!(rtm.total_nodes(), 1);
781 }
782
783 #[test]
784 fn test_rtm_add_node_ignores_local() {
785 let mut rtm = make_rtm(DEFAULT_K);
786 rtm.add_node(zero_id(), "self".to_string(), 0, 0);
787 assert_eq!(rtm.total_nodes(), 0);
788 }
789
790 #[test]
791 fn test_rtm_add_many_nodes_distributed() {
792 let mut rng = Xorshift64::new(1234);
793 let local = rng.next_node_id();
794 let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
795 for _ in 0..100 {
796 let id = rng.next_node_id();
797 rtm.add_node(id, "a".to_string(), rng.next_rtt(), rng.next_ts());
798 }
799 assert!(rtm.total_nodes() > 0);
800 let non_empty = rtm.bucket_sizes().iter().filter(|&&s| s > 0).count();
802 assert!(non_empty > 1, "expected multiple non-empty buckets");
803 }
804
805 #[test]
808 fn test_rtm_remove_node_present() {
809 let mut rtm = make_rtm(DEFAULT_K);
810 let id = id_with_byte(0, 0x80);
811 rtm.add_node(id, "addr".to_string(), 10, 1000);
812 assert!(rtm.remove_node(&id));
813 assert_eq!(rtm.total_nodes(), 0);
814 }
815
816 #[test]
817 fn test_rtm_remove_node_absent() {
818 let mut rtm = make_rtm(DEFAULT_K);
819 assert!(!rtm.remove_node(&id_with_byte(0, 1)));
820 }
821
822 #[test]
825 fn test_rtm_find_closest_empty() {
826 let rtm = make_rtm(DEFAULT_K);
827 let target = id_with_byte(0, 0x80);
828 let result = rtm.find_closest(&target, 5);
829 assert!(result.is_empty());
830 }
831
832 #[test]
833 fn test_rtm_find_closest_count_limit() {
834 let mut rng = Xorshift64::new(99);
835 let local = rng.next_node_id();
836 let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
837 for _ in 0..50 {
838 let id = rng.next_node_id();
839 rtm.add_node(id, "a".to_string(), rng.next_rtt(), rng.next_ts());
840 }
841 let target = rng.next_node_id();
842 let result = rtm.find_closest(&target, 10);
843 assert!(result.len() <= 10);
844 }
845
846 #[test]
847 fn test_rtm_find_closest_sorted_order() {
848 let mut rng = Xorshift64::new(777);
849 let local = rng.next_node_id();
850 let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
851 let target = rng.next_node_id();
852 for _ in 0..30 {
853 let id = rng.next_node_id();
854 rtm.add_node(id, "a".to_string(), rng.next_rtt(), rng.next_ts());
855 }
856 let result = rtm.find_closest(&target, 10);
857 let distances: Vec<[u8; 32]> = result
859 .iter()
860 .map(|e| xor_distance(&target, &e.node_id))
861 .collect();
862 for pair in distances.windows(2) {
863 assert!(pair[0] <= pair[1], "result must be sorted by XOR distance");
864 }
865 }
866
867 #[test]
868 fn test_rtm_find_closest_returns_nearest() {
869 let local = zero_id();
870 let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
871 let id_near = id_with_byte(0, 0x80); let id_far = id_with_byte(0, 0x40); rtm.add_node(id_near, "near".to_string(), 5, 1000);
875 rtm.add_node(id_far, "far".to_string(), 5, 1000);
876 let result = rtm.find_closest(&id_near, 2);
878 assert_eq!(result[0].node_id, id_near);
879 }
880
881 #[test]
884 fn test_rtm_mark_failed_eviction() {
885 let mut rtm = make_rtm(DEFAULT_K);
886 let id = id_with_byte(0, 0x80);
887 rtm.add_node(id, "a".to_string(), 10, 1000);
888 for ts in 1..=3u64 {
889 rtm.mark_failed(&id, 1000 + ts);
890 }
891 assert_eq!(rtm.total_nodes(), 0);
892 }
893
894 #[test]
895 fn test_rtm_mark_failed_noop_on_absent() {
896 let mut rtm = make_rtm(DEFAULT_K);
897 rtm.mark_failed(&id_with_byte(0, 99), 9999);
899 assert_eq!(rtm.total_nodes(), 0);
900 }
901
902 #[test]
905 fn test_rtm_update_rtt() {
906 let mut rtm = make_rtm(DEFAULT_K);
907 let id = id_with_byte(0, 0x80);
908 rtm.add_node(id, "a".to_string(), 100, 1000);
909 rtm.update_rtt(&id, 25, 2000);
910 let entry = rtm
911 .iter_entries()
912 .find(|e| e.node_id == id)
913 .expect("test: entry should be found in routing table");
914 assert_eq!(entry.rtt_ms, 25);
915 assert_eq!(entry.last_seen, 2000);
916 }
917
918 #[test]
919 fn test_rtm_update_rtt_noop_on_absent() {
920 let mut rtm = make_rtm(DEFAULT_K);
921 rtm.update_rtt(&id_with_byte(0, 7), 50, 9999);
923 }
924
925 #[test]
928 fn test_rtm_bucket_sizes_length() {
929 let rtm = make_rtm(DEFAULT_K);
930 assert_eq!(rtm.bucket_sizes().len(), NUM_BUCKETS);
931 }
932
933 #[test]
934 fn test_rtm_total_nodes_zero_on_empty() {
935 let rtm = make_rtm(DEFAULT_K);
936 assert_eq!(rtm.total_nodes(), 0);
937 }
938
939 #[test]
940 fn test_rtm_total_nodes_after_add() {
941 let mut rtm = make_rtm(DEFAULT_K);
942 for i in 1u8..=5 {
943 rtm.add_node(id_with_byte(0, i * 0x10), "a".to_string(), 10, 1000);
944 }
945 assert_eq!(rtm.total_nodes(), 5);
946 }
947
948 #[test]
951 fn test_rtm_stats_empty() {
952 let rtm = make_rtm(DEFAULT_K);
953 let s = rtm.stats();
954 assert_eq!(s.total_nodes, 0);
955 assert_eq!(s.non_empty_buckets, 0);
956 assert_eq!(s.avg_rtt_ms, 0.0);
957 assert_eq!(s.max_bucket_size, 0);
958 }
959
960 #[test]
961 fn test_rtm_stats_single_node() {
962 let mut rtm = make_rtm(DEFAULT_K);
963 rtm.add_node(id_with_byte(0, 0x80), "a".to_string(), 42, 1000);
964 let s = rtm.stats();
965 assert_eq!(s.total_nodes, 1);
966 assert_eq!(s.non_empty_buckets, 1);
967 assert_eq!(s.avg_rtt_ms, 42.0);
968 assert_eq!(s.max_bucket_size, 1);
969 }
970
971 #[test]
972 fn test_rtm_stats_avg_rtt() {
973 let mut rtm = make_rtm(DEFAULT_K);
974 rtm.add_node(id_with_byte(0, 0x80), "a".to_string(), 100, 1000);
976 rtm.add_node(id_with_byte(0, 0x40), "b".to_string(), 200, 1001);
977 let s = rtm.stats();
978 assert_eq!(s.total_nodes, 2);
979 assert!((s.avg_rtt_ms - 150.0).abs() < f64::EPSILON);
980 }
981
982 #[test]
985 fn test_replacement_cache_bounded() {
986 let mut bucket = KBucket::new(1);
987 bucket.add(dummy_entry(id_with_byte(0, 0), 10, 1000));
989 for i in 1u8..=(DEFAULT_REPLACEMENT_CACHE_SIZE as u8 + 10) {
991 bucket.add(dummy_entry(id_with_byte(0, i), 10, 1000));
992 }
993 assert!(
994 bucket.replacement_cache.len() <= DEFAULT_REPLACEMENT_CACHE_SIZE,
995 "replacement cache must not exceed its maximum size"
996 );
997 }
998
999 #[test]
1002 fn test_multiple_replacements_sequential() {
1003 let mut bucket = KBucket::new(2);
1004 let id_a = id_with_byte(0, 1);
1005 let id_b = id_with_byte(0, 2);
1006 let id_c = id_with_byte(0, 3);
1007 let id_d = id_with_byte(0, 4);
1008 bucket.add(dummy_entry(id_a, 10, 1000));
1009 bucket.add(dummy_entry(id_b, 10, 1001));
1010 bucket.add(dummy_entry(id_c, 10, 1002)); bucket.add(dummy_entry(id_d, 10, 1003)); bucket.remove(&id_a);
1014 assert_eq!(bucket.len(), 2);
1015 assert!(bucket.entries.iter().any(|e| e.node_id == id_d));
1016 bucket.remove(&id_b);
1018 assert_eq!(bucket.len(), 2);
1019 assert!(bucket.entries.iter().any(|e| e.node_id == id_c));
1020 }
1021
1022 #[test]
1025 fn test_rtm_is_empty_initial() {
1026 let rtm = make_rtm(DEFAULT_K);
1027 assert!(rtm.is_empty());
1028 }
1029
1030 #[test]
1031 fn test_rtm_is_not_empty_after_add() {
1032 let mut rtm = make_rtm(DEFAULT_K);
1033 rtm.add_node(id_with_byte(0, 1), "x".to_string(), 10, 0);
1034 assert!(!rtm.is_empty());
1035 }
1036
1037 #[test]
1040 fn test_rtm_with_defaults() {
1041 let rtm = RoutingTableManager::with_defaults(zero_id());
1042 assert_eq!(rtm.k, DEFAULT_K);
1043 assert_eq!(rtm.alpha, super::DEFAULT_ALPHA);
1044 assert_eq!(rtm.buckets.len(), NUM_BUCKETS);
1045 }
1046
1047 #[test]
1050 fn test_rtm_bucket_index_for_consistency() {
1051 let local = id_with_byte(1, 0xAB);
1052 let rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
1053 let target = id_with_byte(1, 0x00);
1054 let idx = rtm.bucket_index_for(&target);
1055 assert_eq!(idx, bucket_index(&local, &target));
1056 }
1057
1058 #[test]
1061 fn test_rtm_stress_add_remove_find() {
1062 let mut rng = Xorshift64::new(0xC0FFEE);
1063 let local = rng.next_node_id();
1064 let mut rtm = RoutingTableManager::new(local, 5, 3);
1065 let mut ids: Vec<NodeId> = Vec::new();
1066
1067 for _ in 0..200 {
1069 let id = rng.next_node_id();
1070 rtm.add_node(id, "x".to_string(), rng.next_rtt(), rng.next_ts());
1071 ids.push(id);
1072 }
1073
1074 for id in ids.iter().step_by(2) {
1077 let _ = rtm.remove_node(id);
1078 }
1079
1080 let target = rng.next_node_id();
1082 let result = rtm.find_closest(&target, 5);
1083 assert!(result.len() <= 5);
1084 }
1085
1086 #[test]
1089 fn test_kbucket_duplicate_in_cache_updated() {
1090 let mut bucket = KBucket::new(1);
1091 let id_a = id_with_byte(0, 1);
1092 let id_b = id_with_byte(0, 2);
1093 bucket.add(dummy_entry(id_a, 10, 1000));
1094 bucket.add(dummy_entry(id_b, 10, 1001)); bucket.add(dummy_entry(id_b, 99, 9999));
1097 assert_eq!(bucket.replacement_cache.len(), 1);
1099 let cached = bucket
1100 .replacement_cache
1101 .front()
1102 .expect("test: replacement cache should have an entry after overflow add");
1103 assert_eq!(cached.node_id, id_b);
1104 assert_eq!(cached.rtt_ms, 99);
1105 }
1106
1107 #[test]
1110 fn test_rtm_find_closest_excludes_local() {
1111 let local = zero_id();
1112 let mut rtm = RoutingTableManager::new(local, DEFAULT_K, 3);
1113 rtm.add_node(local, "self".to_string(), 0, 0);
1115 let result = rtm.find_closest(&local, 20);
1116 assert!(result.iter().all(|e| e.node_id != local));
1117 }
1118
1119 #[test]
1122 fn test_node_id_display() {
1123 let id = NodeId([0xABu8; 32]);
1124 let s = id.to_string();
1125 assert_eq!(s.len(), 64);
1126 assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
1127 }
1128
1129 #[test]
1132 fn test_node_id_default() {
1133 let id = NodeId::default();
1134 assert_eq!(id.0, [0u8; 32]);
1135 }
1136
1137 #[test]
1140 fn test_node_id_as_bytes_round_trip() {
1141 let bytes = [0x42u8; 32];
1142 let id = NodeId::new(bytes);
1143 assert_eq!(id.as_bytes(), &bytes);
1144 }
1145
1146 #[test]
1149 fn test_bucket_index_deterministic() {
1150 let local = id_with_byte(5, 0xDE);
1151 let target = id_with_byte(5, 0xAD);
1152 let idx1 = bucket_index(&local, &target);
1153 let idx2 = bucket_index(&local, &target);
1154 assert_eq!(idx1, idx2);
1155 }
1156
1157 #[test]
1160 fn test_rtm_update_rtt_clears_failed_queries() {
1161 let mut rtm = make_rtm(DEFAULT_K);
1162 let id = id_with_byte(0, 0x80);
1163 rtm.add_node(id, "a".to_string(), 100, 1000);
1164 rtm.mark_failed(&id, 1001);
1166 rtm.mark_failed(&id, 1002);
1167 rtm.update_rtt(&id, 50, 2000);
1169 let entry = rtm
1170 .iter_entries()
1171 .find(|e| e.node_id == id)
1172 .expect("test: node should still be in routing table after non-evicting failures");
1173 assert_eq!(entry.failed_queries, 0);
1174 }
1175
1176 #[test]
1179 fn test_kbucket_is_full() {
1180 let mut bucket = KBucket::new(2);
1181 assert!(!bucket.is_full());
1182 bucket.add(dummy_entry(id_with_byte(0, 1), 10, 1000));
1183 assert!(!bucket.is_full());
1184 bucket.add(dummy_entry(id_with_byte(0, 2), 10, 1001));
1185 assert!(bucket.is_full());
1186 }
1187
1188 #[test]
1191 fn test_find_closest_fewer_than_requested() {
1192 let mut rtm = make_rtm(DEFAULT_K);
1193 rtm.add_node(id_with_byte(0, 0x80), "a".to_string(), 10, 1000);
1194 rtm.add_node(id_with_byte(0, 0x40), "b".to_string(), 10, 1001);
1195 let result = rtm.find_closest(&id_with_byte(0, 0x80), 100);
1196 assert_eq!(result.len(), 2); }
1198
1199 #[test]
1202 fn test_max_failed_queries_constant() {
1203 assert_eq!(MAX_FAILED_QUERIES, 3);
1205 }
1206
1207 #[test]
1210 fn test_xorshift64_reproducible() {
1211 let mut rng1 = Xorshift64::new(12345);
1212 let mut rng2 = Xorshift64::new(12345);
1213 for _ in 0..100 {
1214 assert_eq!(rng1.next_u64(), rng2.next_u64());
1215 }
1216 }
1217
1218 #[test]
1221 fn test_rtm_k1_single_slot() {
1222 let local = zero_id();
1223 let mut rtm = RoutingTableManager::new(local, 1, 3);
1224 let id_a = id_with_byte(0, 0x80);
1225 let id_b = id_with_byte(0, 0xC0); rtm.add_node(id_a, "a".to_string(), 10, 1000);
1227 rtm.add_node(id_b, "b".to_string(), 10, 1001); assert_eq!(rtm.total_nodes(), 1); }
1230
1231 #[test]
1234 fn test_rtm_iter_entries_count() {
1235 let mut rtm = make_rtm(DEFAULT_K);
1236 for i in 1u8..=8 {
1237 rtm.add_node(
1238 id_with_byte(0, i * 0x10),
1239 "x".to_string(),
1240 10,
1241 i as u64 * 100,
1242 );
1243 }
1244 let count = rtm.iter_entries().count();
1245 assert_eq!(count, rtm.total_nodes());
1246 }
1247}