1use std::collections::{HashMap, HashSet};
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub enum PeerCapability {
63 BitswapV1,
65 BitswapV2,
67 DHTKademlia,
69 DHTCorvus,
71 GossipSub,
73 GraphSync,
75 SemanticSearch,
77 TensorLogic,
79 Relay,
81 NatTraversal,
83 Custom(String),
85}
86
87impl PeerCapability {
88 pub fn key(&self) -> String {
91 match self {
92 PeerCapability::BitswapV1 => "bitswap_v1".to_string(),
93 PeerCapability::BitswapV2 => "bitswap_v2".to_string(),
94 PeerCapability::DHTKademlia => "dht_kademlia".to_string(),
95 PeerCapability::DHTCorvus => "dht_corvus".to_string(),
96 PeerCapability::GossipSub => "gossipsub".to_string(),
97 PeerCapability::GraphSync => "graphsync".to_string(),
98 PeerCapability::SemanticSearch => "semantic_search".to_string(),
99 PeerCapability::TensorLogic => "tensor_logic".to_string(),
100 PeerCapability::Relay => "relay".to_string(),
101 PeerCapability::NatTraversal => "nat_traversal".to_string(),
102 PeerCapability::Custom(s) => format!("custom:{s}"),
103 }
104 }
105
106 pub fn static_name(&self) -> &'static str {
109 match self {
110 PeerCapability::BitswapV1 => "bitswap_v1",
111 PeerCapability::BitswapV2 => "bitswap_v2",
112 PeerCapability::DHTKademlia => "dht_kademlia",
113 PeerCapability::DHTCorvus => "dht_corvus",
114 PeerCapability::GossipSub => "gossipsub",
115 PeerCapability::GraphSync => "graphsync",
116 PeerCapability::SemanticSearch => "semantic_search",
117 PeerCapability::TensorLogic => "tensor_logic",
118 PeerCapability::Relay => "relay",
119 PeerCapability::NatTraversal => "nat_traversal",
120 PeerCapability::Custom(_) => "custom",
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
133pub struct PeerCapabilitySet {
134 pub peer_id: String,
136 pub capabilities: HashSet<PeerCapability>,
138 pub version: String,
140 pub advertised_at: u64,
142 pub last_verified: Option<u64>,
145}
146
147impl PeerCapabilitySet {
148 #[inline]
150 pub fn has(&self, cap: &PeerCapability) -> bool {
151 self.capabilities.contains(cap)
152 }
153
154 pub fn has_all(&self, caps: &[PeerCapability]) -> bool {
156 caps.iter().all(|c| self.capabilities.contains(c))
157 }
158
159 pub fn has_any(&self, caps: &[PeerCapability]) -> bool {
162 if caps.is_empty() {
163 return true;
164 }
165 caps.iter().any(|c| self.capabilities.contains(c))
166 }
167
168 #[inline]
172 pub fn is_expired(&self, now_ms: u64, ttl_ms: u64) -> bool {
173 now_ms >= self.advertised_at.saturating_add(ttl_ms)
174 }
175}
176
177#[derive(Debug, Clone)]
182pub struct CapabilityConfig {
183 pub local_capabilities: Vec<PeerCapability>,
185 pub require_all: Vec<PeerCapability>,
188 pub require_any: Vec<PeerCapability>,
192 pub max_peers: usize,
195 pub ttl_ms: u64,
198}
199
200impl Default for CapabilityConfig {
201 fn default() -> Self {
202 Self {
203 local_capabilities: Vec::new(),
204 require_all: Vec::new(),
205 require_any: Vec::new(),
206 max_peers: 1024,
207 ttl_ms: 300_000, }
209 }
210}
211
212#[derive(Debug, Clone, Default)]
216pub struct CapabilityStats {
217 pub total_registered: u64,
220 pub total_removed: u64,
222 pub peers_filtered_out: u64,
225 pub capability_distribution: HashMap<String, u64>,
228}
229
230pub struct CapabilityRegistry {
243 config: CapabilityConfig,
244 peers: HashMap<String, PeerCapabilitySet>,
245 stats: CapabilityStats,
246}
247
248impl CapabilityRegistry {
249 pub fn new(config: CapabilityConfig) -> Self {
251 Self {
252 config,
253 peers: HashMap::new(),
254 stats: CapabilityStats::default(),
255 }
256 }
257
258 pub fn register(&mut self, peer: PeerCapabilitySet) -> bool {
270 if !self.meets_requirements(&peer) {
271 self.stats.peers_filtered_out = self.stats.peers_filtered_out.saturating_add(1);
272 return false;
273 }
274
275 let is_new = !self.peers.contains_key(&peer.peer_id);
277 if is_new && self.peers.len() >= self.config.max_peers {
278 self.stats.peers_filtered_out = self.stats.peers_filtered_out.saturating_add(1);
279 return false;
280 }
281
282 if let Some(old) = self.peers.get(&peer.peer_id) {
285 for cap in &old.capabilities {
286 let key = cap.key();
287 let count = self.stats.capability_distribution.entry(key).or_insert(0);
288 *count = count.saturating_sub(1);
289 }
290 }
291
292 for cap in &peer.capabilities {
294 *self
295 .stats
296 .capability_distribution
297 .entry(cap.key())
298 .or_insert(0) += 1;
299 }
300
301 self.peers.insert(peer.peer_id.clone(), peer);
302 self.stats.total_registered = self.stats.total_registered.saturating_add(1);
303 true
304 }
305
306 pub fn meets_requirements(&self, peer: &PeerCapabilitySet) -> bool {
315 if !peer.has_all(&self.config.require_all) {
317 return false;
318 }
319 if !self.config.require_any.is_empty() && !peer.has_any(&self.config.require_any) {
321 return false;
322 }
323 true
324 }
325
326 pub fn remove(&mut self, peer_id: &str) -> bool {
332 if let Some(removed) = self.peers.remove(peer_id) {
333 for cap in &removed.capabilities {
334 let key = cap.key();
335 let count = self.stats.capability_distribution.entry(key).or_insert(0);
336 *count = count.saturating_sub(1);
337 }
338 self.stats.total_removed = self.stats.total_removed.saturating_add(1);
339 true
340 } else {
341 false
342 }
343 }
344
345 pub fn get_peer(&self, peer_id: &str) -> Option<&PeerCapabilitySet> {
349 self.peers.get(peer_id)
350 }
351
352 pub fn peers_with_capability(&self, cap: &PeerCapability) -> Vec<&PeerCapabilitySet> {
354 self.peers.values().filter(|p| p.has(cap)).collect()
355 }
356
357 pub fn peers_with_all(&self, caps: &[PeerCapability]) -> Vec<&PeerCapabilitySet> {
359 self.peers.values().filter(|p| p.has_all(caps)).collect()
360 }
361
362 pub fn peers_with_any(&self, caps: &[PeerCapability]) -> Vec<&PeerCapabilitySet> {
364 self.peers.values().filter(|p| p.has_any(caps)).collect()
365 }
366
367 pub fn purge_expired(&mut self, now: u64) -> usize {
373 let ttl_ms = self.config.ttl_ms;
374
375 let expired_ids: Vec<String> = self
377 .peers
378 .iter()
379 .filter(|(_, p)| p.is_expired(now, ttl_ms))
380 .map(|(id, _)| id.clone())
381 .collect();
382
383 let count = expired_ids.len();
384 for id in expired_ids {
385 self.remove(&id);
387 }
392 count
393 }
394
395 pub fn capability_name(cap: &PeerCapability) -> &'static str {
403 cap.static_name()
404 }
405
406 pub fn peer_count(&self) -> usize {
410 self.peers.len()
411 }
412
413 pub fn stats(&self) -> &CapabilityStats {
419 &self.stats
420 }
421
422 pub fn local_capabilities(&self) -> &[PeerCapability] {
424 &self.config.local_capabilities
425 }
426}
427
428#[cfg(test)]
431mod tests {
432 use super::*;
433
434 fn default_config() -> CapabilityConfig {
437 CapabilityConfig {
438 local_capabilities: vec![PeerCapability::BitswapV2, PeerCapability::DHTKademlia],
439 require_all: Vec::new(),
440 require_any: Vec::new(),
441 max_peers: 10,
442 ttl_ms: 60_000,
443 }
444 }
445
446 fn make_peer(
447 peer_id: &str,
448 caps: impl IntoIterator<Item = PeerCapability>,
449 advertised_at: u64,
450 ) -> PeerCapabilitySet {
451 PeerCapabilitySet {
452 peer_id: peer_id.to_string(),
453 capabilities: caps.into_iter().collect(),
454 version: "0.2.0".to_string(),
455 advertised_at,
456 last_verified: None,
457 }
458 }
459
460 #[test]
463 fn test_register_and_count() {
464 let mut reg = CapabilityRegistry::new(default_config());
465 let peer = make_peer("peer1", [PeerCapability::BitswapV1], 1_000);
466 assert!(reg.register(peer));
467 assert_eq!(reg.peer_count(), 1);
468 }
469
470 #[test]
473 fn test_stats_total_registered() {
474 let mut reg = CapabilityRegistry::new(default_config());
475 reg.register(make_peer("p1", [PeerCapability::Relay], 0));
476 reg.register(make_peer("p2", [PeerCapability::GossipSub], 0));
477 assert_eq!(reg.stats().total_registered, 2);
478 }
479
480 #[test]
483 fn test_require_all_accept() {
484 let config = CapabilityConfig {
485 require_all: vec![PeerCapability::BitswapV1, PeerCapability::DHTKademlia],
486 ..default_config()
487 };
488 let mut reg = CapabilityRegistry::new(config);
489 let peer = make_peer(
490 "p1",
491 [PeerCapability::BitswapV1, PeerCapability::DHTKademlia],
492 0,
493 );
494 assert!(reg.register(peer));
495 }
496
497 #[test]
500 fn test_require_all_reject() {
501 let config = CapabilityConfig {
502 require_all: vec![PeerCapability::BitswapV1, PeerCapability::DHTKademlia],
503 ..default_config()
504 };
505 let mut reg = CapabilityRegistry::new(config);
506 let peer = make_peer("p1", [PeerCapability::BitswapV1], 0);
508 assert!(!reg.register(peer));
509 assert_eq!(reg.peer_count(), 0);
510 assert_eq!(reg.stats().peers_filtered_out, 1);
511 }
512
513 #[test]
516 fn test_require_any_accept() {
517 let config = CapabilityConfig {
518 require_any: vec![PeerCapability::Relay, PeerCapability::NatTraversal],
519 ..default_config()
520 };
521 let mut reg = CapabilityRegistry::new(config);
522 let peer = make_peer("p1", [PeerCapability::Relay], 0);
523 assert!(reg.register(peer));
524 }
525
526 #[test]
529 fn test_require_any_reject() {
530 let config = CapabilityConfig {
531 require_any: vec![PeerCapability::Relay, PeerCapability::NatTraversal],
532 ..default_config()
533 };
534 let mut reg = CapabilityRegistry::new(config);
535 let peer = make_peer("p1", [PeerCapability::GossipSub], 0);
536 assert!(!reg.register(peer));
537 assert_eq!(reg.stats().peers_filtered_out, 1);
538 }
539
540 #[test]
543 fn test_empty_requirements_accept_all() {
544 let mut reg = CapabilityRegistry::new(default_config());
545 let peer = make_peer("p1", [], 0);
547 assert!(reg.register(peer));
548 }
549
550 #[test]
553 fn test_peers_with_capability() {
554 let mut reg = CapabilityRegistry::new(default_config());
555 reg.register(make_peer("p1", [PeerCapability::BitswapV1], 0));
556 reg.register(make_peer(
557 "p2",
558 [PeerCapability::BitswapV1, PeerCapability::Relay],
559 0,
560 ));
561 reg.register(make_peer("p3", [PeerCapability::Relay], 0));
562
563 let bitswap_peers = reg.peers_with_capability(&PeerCapability::BitswapV1);
564 assert_eq!(bitswap_peers.len(), 2);
565
566 let relay_peers = reg.peers_with_capability(&PeerCapability::Relay);
567 assert_eq!(relay_peers.len(), 2);
568 }
569
570 #[test]
573 fn test_peers_with_all() {
574 let mut reg = CapabilityRegistry::new(default_config());
575 reg.register(make_peer(
576 "p1",
577 [PeerCapability::BitswapV1, PeerCapability::DHTKademlia],
578 0,
579 ));
580 reg.register(make_peer("p2", [PeerCapability::BitswapV1], 0));
581 reg.register(make_peer("p3", [PeerCapability::DHTKademlia], 0));
582
583 let both = reg.peers_with_all(&[PeerCapability::BitswapV1, PeerCapability::DHTKademlia]);
584 assert_eq!(both.len(), 1);
585 assert_eq!(both[0].peer_id, "p1");
586 }
587
588 #[test]
591 fn test_peers_with_any() {
592 let mut reg = CapabilityRegistry::new(default_config());
593 reg.register(make_peer("p1", [PeerCapability::BitswapV2], 0));
594 reg.register(make_peer("p2", [PeerCapability::SemanticSearch], 0));
595 reg.register(make_peer("p3", [PeerCapability::GraphSync], 0));
596
597 let result =
598 reg.peers_with_any(&[PeerCapability::BitswapV2, PeerCapability::SemanticSearch]);
599 assert_eq!(result.len(), 2);
600 }
601
602 #[test]
605 fn test_peers_with_any_empty_slice() {
606 let mut reg = CapabilityRegistry::new(default_config());
607 reg.register(make_peer("p1", [PeerCapability::Relay], 0));
608 reg.register(make_peer("p2", [PeerCapability::GossipSub], 0));
609
610 let result = reg.peers_with_any(&[]);
612 assert_eq!(result.len(), 2);
613 }
614
615 #[test]
618 fn test_peers_with_all_empty_slice() {
619 let mut reg = CapabilityRegistry::new(default_config());
620 reg.register(make_peer("p1", [PeerCapability::Relay], 0));
621 reg.register(make_peer("p2", [PeerCapability::GossipSub], 0));
622
623 let result = reg.peers_with_all(&[]);
625 assert_eq!(result.len(), 2);
626 }
627
628 #[test]
631 fn test_remove_existing() {
632 let mut reg = CapabilityRegistry::new(default_config());
633 reg.register(make_peer("p1", [PeerCapability::Relay], 0));
634 assert_eq!(reg.peer_count(), 1);
635 assert!(reg.remove("p1"));
636 assert_eq!(reg.peer_count(), 0);
637 assert_eq!(reg.stats().total_removed, 1);
638 }
639
640 #[test]
643 fn test_remove_nonexistent() {
644 let mut reg = CapabilityRegistry::new(default_config());
645 assert!(!reg.remove("nobody"));
646 assert_eq!(reg.stats().total_removed, 0);
647 }
648
649 #[test]
652 fn test_purge_expired() {
653 let config = CapabilityConfig {
654 ttl_ms: 1_000,
655 ..default_config()
656 };
657 let mut reg = CapabilityRegistry::new(config);
658
659 reg.register(make_peer("old", [PeerCapability::Relay], 0));
661 reg.register(make_peer("fresh", [PeerCapability::GossipSub], 5_000));
663
664 let purged = reg.purge_expired(2_000);
665 assert_eq!(purged, 1);
666 assert_eq!(reg.peer_count(), 1);
667 assert!(reg.get_peer("fresh").is_some());
668 assert!(reg.get_peer("old").is_none());
669 }
670
671 #[test]
674 fn test_purge_expired_none() {
675 let mut reg = CapabilityRegistry::new(default_config());
676 reg.register(make_peer("p1", [PeerCapability::Relay], 100_000));
677 let purged = reg.purge_expired(1_000);
678 assert_eq!(purged, 0);
679 assert_eq!(reg.peer_count(), 1);
680 }
681
682 #[test]
685 fn test_purge_expired_all() {
686 let config = CapabilityConfig {
687 ttl_ms: 500,
688 ..default_config()
689 };
690 let mut reg = CapabilityRegistry::new(config);
691 reg.register(make_peer("p1", [PeerCapability::Relay], 0));
692 reg.register(make_peer("p2", [PeerCapability::GossipSub], 100));
693 let purged = reg.purge_expired(10_000);
694 assert_eq!(purged, 2);
695 assert_eq!(reg.peer_count(), 0);
696 }
697
698 #[test]
701 fn test_max_peers_limit() {
702 let config = CapabilityConfig {
703 max_peers: 2,
704 ..default_config()
705 };
706 let mut reg = CapabilityRegistry::new(config);
707 assert!(reg.register(make_peer("p1", [PeerCapability::Relay], 0)));
708 assert!(reg.register(make_peer("p2", [PeerCapability::Relay], 0)));
709 assert!(!reg.register(make_peer("p3", [PeerCapability::Relay], 0)));
711 assert_eq!(reg.peer_count(), 2);
712 assert_eq!(reg.stats().peers_filtered_out, 1);
713 }
714
715 #[test]
718 fn test_reregister_replaces_entry() {
719 let mut reg = CapabilityRegistry::new(default_config());
720 reg.register(make_peer("p1", [PeerCapability::BitswapV1], 100));
721 reg.register(make_peer(
723 "p1",
724 [PeerCapability::BitswapV2, PeerCapability::DHTKademlia],
725 200,
726 ));
727 assert_eq!(reg.peer_count(), 1);
729 let stored = reg.get_peer("p1").expect("peer should exist");
731 assert!(stored.has(&PeerCapability::BitswapV2));
732 assert!(!stored.has(&PeerCapability::BitswapV1));
733 }
734
735 #[test]
738 fn test_capability_distribution() {
739 let mut reg = CapabilityRegistry::new(default_config());
740 reg.register(make_peer(
741 "p1",
742 [PeerCapability::Relay, PeerCapability::GossipSub],
743 0,
744 ));
745 reg.register(make_peer("p2", [PeerCapability::Relay], 0));
746
747 let dist = ®.stats().capability_distribution;
748 assert_eq!(dist.get("relay").copied().unwrap_or(0), 2);
749 assert_eq!(dist.get("gossipsub").copied().unwrap_or(0), 1);
750 }
751
752 #[test]
755 fn test_distribution_decrements_on_remove() {
756 let mut reg = CapabilityRegistry::new(default_config());
757 reg.register(make_peer("p1", [PeerCapability::Relay], 0));
758 reg.register(make_peer("p2", [PeerCapability::Relay], 0));
759
760 assert_eq!(
761 reg.stats()
762 .capability_distribution
763 .get("relay")
764 .copied()
765 .unwrap_or(0),
766 2
767 );
768 reg.remove("p1");
769 assert_eq!(
770 reg.stats()
771 .capability_distribution
772 .get("relay")
773 .copied()
774 .unwrap_or(0),
775 1
776 );
777 }
778
779 #[test]
782 fn test_custom_capability() {
783 let mut reg = CapabilityRegistry::new(default_config());
784 let custom = PeerCapability::Custom("my_protocol/1.0".to_string());
785 reg.register(make_peer("p1", [custom.clone()], 0));
786
787 let results = reg.peers_with_capability(&custom);
788 assert_eq!(results.len(), 1);
789 assert_eq!(results[0].peer_id, "p1");
790 }
791
792 #[test]
795 fn test_capability_name() {
796 assert_eq!(
797 CapabilityRegistry::capability_name(&PeerCapability::BitswapV1),
798 "bitswap_v1"
799 );
800 assert_eq!(
801 CapabilityRegistry::capability_name(&PeerCapability::DHTCorvus),
802 "dht_corvus"
803 );
804 assert_eq!(
805 CapabilityRegistry::capability_name(&PeerCapability::TensorLogic),
806 "tensor_logic"
807 );
808 assert_eq!(
809 CapabilityRegistry::capability_name(&PeerCapability::Custom("foo".to_string())),
810 "custom"
811 );
812 }
813
814 #[test]
817 fn test_local_capabilities() {
818 let config = CapabilityConfig {
819 local_capabilities: vec![PeerCapability::BitswapV2, PeerCapability::SemanticSearch],
820 ..default_config()
821 };
822 let reg = CapabilityRegistry::new(config);
823 let local = reg.local_capabilities();
824 assert_eq!(local.len(), 2);
825 assert!(local.contains(&PeerCapability::BitswapV2));
826 assert!(local.contains(&PeerCapability::SemanticSearch));
827 }
828
829 #[test]
832 fn test_get_peer_none() {
833 let reg = CapabilityRegistry::new(default_config());
834 assert!(reg.get_peer("nonexistent").is_none());
835 }
836
837 #[test]
840 fn test_require_all_and_any_combined() {
841 let config = CapabilityConfig {
842 require_all: vec![PeerCapability::BitswapV1],
843 require_any: vec![PeerCapability::Relay, PeerCapability::NatTraversal],
844 ..default_config()
845 };
846 let mut reg = CapabilityRegistry::new(config);
847
848 let ok = make_peer("ok", [PeerCapability::BitswapV1, PeerCapability::Relay], 0);
850 assert!(reg.register(ok));
851
852 let no_any = make_peer(
854 "no_any",
855 [PeerCapability::BitswapV1, PeerCapability::GossipSub],
856 0,
857 );
858 assert!(!reg.register(no_any));
859
860 let no_all = make_peer("no_all", [PeerCapability::Relay], 0);
862 assert!(!reg.register(no_all));
863
864 assert_eq!(reg.peer_count(), 1);
865 }
866
867 #[test]
870 fn test_meets_requirements_direct() {
871 let config = CapabilityConfig {
872 require_all: vec![PeerCapability::GossipSub],
873 require_any: vec![],
874 ..default_config()
875 };
876 let reg = CapabilityRegistry::new(config);
877
878 let good = make_peer("g", [PeerCapability::GossipSub], 0);
879 assert!(reg.meets_requirements(&good));
880
881 let bad = make_peer("b", [PeerCapability::Relay], 0);
882 assert!(!reg.meets_requirements(&bad));
883 }
884
885 #[test]
888 fn test_ttl_boundary_exact() {
889 let set = make_peer("p", [PeerCapability::Relay], 0);
891 assert!(set.is_expired(1_000, 1_000));
892
893 assert!(!set.is_expired(999, 1_000));
895 }
896
897 #[test]
900 fn test_distribution_after_reregister() {
901 let mut reg = CapabilityRegistry::new(default_config());
902 reg.register(make_peer("p1", [PeerCapability::Relay], 0));
903 reg.register(make_peer("p1", [PeerCapability::GossipSub], 100));
905
906 let dist = ®.stats().capability_distribution;
907 assert_eq!(dist.get("relay").copied().unwrap_or(0), 0);
909 assert_eq!(dist.get("gossipsub").copied().unwrap_or(0), 1);
910 }
911
912 #[test]
915 fn test_overlap_queries() {
916 let mut reg = CapabilityRegistry::new(default_config());
917 reg.register(make_peer(
918 "p1",
919 [PeerCapability::BitswapV1, PeerCapability::BitswapV2],
920 0,
921 ));
922 reg.register(make_peer("p2", [PeerCapability::BitswapV1], 0));
923 reg.register(make_peer("p3", [PeerCapability::BitswapV2], 0));
924
925 let both = reg.peers_with_all(&[PeerCapability::BitswapV1, PeerCapability::BitswapV2]);
926 assert_eq!(both.len(), 1);
927
928 let either = reg.peers_with_any(&[PeerCapability::BitswapV1, PeerCapability::BitswapV2]);
929 assert_eq!(either.len(), 3);
930 }
931}