Skip to main content

ipfrs_network/
peer_capabilities.rs

1//! Peer Capability Advertisement and Negotiation
2//!
3//! This module provides a high-level registry for advertising and negotiating
4//! capabilities between P2P peers. Unlike the lower-level `capability_registry`
5//! module (which is tick-based and protocol-oriented), this module is designed
6//! around wall-clock TTL semantics, structured requirement matching
7//! (`require_all` / `require_any`), and a richer set of first-class capability
8//! variants aligned with the IPFRS protocol suite.
9//!
10//! # Key Types
11//!
12//! - [`PeerCapability`] — enumeration of well-known and custom capabilities.
13//! - [`PeerCapabilitySet`] — a single peer's advertised capability snapshot.
14//! - [`CapabilityConfig`] — local configuration: what we advertise, what we
15//!   require, how many peers we track, and TTL.
16//! - [`CapabilityRegistry`] — the central in-memory registry; register, query,
17//!   expire, and remove peer capability sets.
18//! - [`CapabilityStats`] — counters and per-capability distribution histograms.
19//!
20//! # Example
21//!
22//! ```rust
23//! use ipfrs_network::peer_capabilities::{
24//!     CapabilityConfig, CapabilityRegistry, PeerCapability, PeerCapabilitySet,
25//! };
26//! use std::collections::HashSet;
27//!
28//! let config = CapabilityConfig {
29//!     local_capabilities: vec![PeerCapability::BitswapV2, PeerCapability::DHTKademlia],
30//!     require_all: vec![PeerCapability::BitswapV1],
31//!     require_any: vec![PeerCapability::DHTKademlia, PeerCapability::DHTCorvus],
32//!     max_peers: 256,
33//!     ttl_ms: 60_000,
34//! };
35//!
36//! let mut registry = CapabilityRegistry::new(config);
37//!
38//! let peer = PeerCapabilitySet {
39//!     peer_id: "QmPeer1".to_string(),
40//!     capabilities: [PeerCapability::BitswapV1, PeerCapability::DHTKademlia]
41//!         .into_iter()
42//!         .collect(),
43//!     version: "0.2.0".to_string(),
44//!     advertised_at: 1_000,
45//!     last_verified: None,
46//! };
47//!
48//! assert!(registry.register(peer));
49//! assert_eq!(registry.peer_count(), 1);
50//! ```
51
52use std::collections::{HashMap, HashSet};
53
54// ── PeerCapability ────────────────────────────────────────────────────────────
55
56/// A well-known or custom capability that a peer may advertise.
57///
58/// Variants cover the full IPFRS protocol suite: block exchange, DHT flavours,
59/// pub/sub, graph sync, semantic search, tensor logic, relay, NAT traversal,
60/// and an open-ended `Custom` escape hatch.
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub enum PeerCapability {
63    /// Bitswap block-exchange protocol, version 1.
64    BitswapV1,
65    /// Bitswap block-exchange protocol, version 2 (session-aware).
66    BitswapV2,
67    /// Standard Kademlia DHT routing.
68    DHTKademlia,
69    /// Corvus DHT — IPFRS-native DHT with semantic routing extensions.
70    DHTCorvus,
71    /// GossipSub pub/sub mesh messaging.
72    GossipSub,
73    /// GraphSync — IPLD graph synchronisation protocol.
74    GraphSync,
75    /// Semantic vector-similarity search over DHT keyspace.
76    SemanticSearch,
77    /// TensorLogic rule engine and inference capability.
78    TensorLogic,
79    /// Circuit-relay service (the peer acts as a relay).
80    Relay,
81    /// NAT traversal via AutoNAT / DCUtR / hole-punching.
82    NatTraversal,
83    /// Application-defined capability identified by an arbitrary string.
84    Custom(String),
85}
86
87impl PeerCapability {
88    /// Returns a stable ASCII key that uniquely identifies this capability
89    /// variant.  Used for the stats distribution histogram.
90    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    /// Returns a `&'static str` label for well-known variants.  Custom
107    /// capabilities return `"custom"`.
108    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// ── PeerCapabilitySet ─────────────────────────────────────────────────────────
126
127/// The full capability advertisement for a single remote peer.
128///
129/// This is the unit stored inside [`CapabilityRegistry`].  The
130/// `advertised_at` field carries a Unix-epoch millisecond timestamp which is
131/// compared against the registry's `ttl_ms` to determine freshness.
132#[derive(Debug, Clone)]
133pub struct PeerCapabilitySet {
134    /// Unique peer identifier (string form of the libp2p `PeerId`).
135    pub peer_id: String,
136    /// Set of capabilities this peer claims to support.
137    pub capabilities: HashSet<PeerCapability>,
138    /// Peer's self-reported IPFRS protocol version string, e.g. `"0.2.0"`.
139    pub version: String,
140    /// Unix-epoch millisecond timestamp when this record was first advertised.
141    pub advertised_at: u64,
142    /// Unix-epoch millisecond timestamp of the most recent successful
143    /// verification, if any.
144    pub last_verified: Option<u64>,
145}
146
147impl PeerCapabilitySet {
148    /// Returns `true` if this peer supports `cap`.
149    #[inline]
150    pub fn has(&self, cap: &PeerCapability) -> bool {
151        self.capabilities.contains(cap)
152    }
153
154    /// Returns `true` if this peer supports **all** capabilities in `caps`.
155    pub fn has_all(&self, caps: &[PeerCapability]) -> bool {
156        caps.iter().all(|c| self.capabilities.contains(c))
157    }
158
159    /// Returns `true` if this peer supports **at least one** capability in
160    /// `caps`.  Returns `true` when `caps` is empty (vacuous truth).
161    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    /// Returns `true` if the record has expired given `now_ms` and `ttl_ms`.
169    ///
170    /// Expiry is defined as `now_ms >= advertised_at + ttl_ms`.
171    #[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// ── CapabilityConfig ──────────────────────────────────────────────────────────
178
179/// Configuration for the local node's capability advertisement and peer
180/// filtering policy.
181#[derive(Debug, Clone)]
182pub struct CapabilityConfig {
183    /// Capabilities that this local node advertises to remote peers.
184    pub local_capabilities: Vec<PeerCapability>,
185    /// Peers that do not have **all** of these capabilities are rejected during
186    /// [`CapabilityRegistry::register`].
187    pub require_all: Vec<PeerCapability>,
188    /// Peers that have **none** of these capabilities are rejected during
189    /// [`CapabilityRegistry::register`].  An empty list means no
190    /// `require_any` constraint.
191    pub require_any: Vec<PeerCapability>,
192    /// Maximum number of peers the registry will hold simultaneously.
193    /// Attempts to register beyond this limit return `false`.
194    pub max_peers: usize,
195    /// Time-to-live in milliseconds after which a `PeerCapabilitySet` is
196    /// considered stale and eligible for purging.
197    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, // 5 minutes
208        }
209    }
210}
211
212// ── CapabilityStats ───────────────────────────────────────────────────────────
213
214/// Runtime statistics for a [`CapabilityRegistry`].
215#[derive(Debug, Clone, Default)]
216pub struct CapabilityStats {
217    /// Total number of successful [`CapabilityRegistry::register`] calls since
218    /// the registry was created.
219    pub total_registered: u64,
220    /// Total number of successful [`CapabilityRegistry::remove`] calls.
221    pub total_removed: u64,
222    /// Number of registration attempts that were rejected because the peer
223    /// did not meet the configured requirements.
224    pub peers_filtered_out: u64,
225    /// Per-capability count of how many currently registered peers declare each
226    /// capability.  Keys are produced by [`PeerCapability::key`].
227    pub capability_distribution: HashMap<String, u64>,
228}
229
230// ── CapabilityRegistry ────────────────────────────────────────────────────────
231
232/// Central registry for peer capability advertisements.
233///
234/// The registry enforces:
235/// - **Requirement filtering** — peers must satisfy `require_all` and
236///   `require_any` from the [`CapabilityConfig`] to be accepted.
237/// - **Capacity limiting** — at most `max_peers` entries are held.
238/// - **TTL expiry** — stale entries can be purged via [`CapabilityRegistry::purge_expired`].
239///
240/// All methods take `&mut self`; the registry is not internally synchronised.
241/// Wrap in an `Arc<Mutex<_>>` for concurrent access.
242pub struct CapabilityRegistry {
243    config: CapabilityConfig,
244    peers: HashMap<String, PeerCapabilitySet>,
245    stats: CapabilityStats,
246}
247
248impl CapabilityRegistry {
249    /// Create a new registry with the given configuration.
250    pub fn new(config: CapabilityConfig) -> Self {
251        Self {
252            config,
253            peers: HashMap::new(),
254            stats: CapabilityStats::default(),
255        }
256    }
257
258    // ── Registration ──────────────────────────────────────────────────────────
259
260    /// Register `peer` in the registry.
261    ///
262    /// Returns `true` on success, `false` when:
263    /// - The peer does not meet the configured requirements
264    ///   (`meets_requirements`), or
265    /// - The registry is at capacity (`max_peers`).
266    ///
267    /// If the same `peer_id` is already present the old entry is replaced
268    /// (the stats counters still increment as if it were a new registration).
269    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        // Enforce max_peers only when the peer is genuinely new.
276        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 replacing an existing entry, remove the old capability counts
283        // before adding the new ones.
284        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        // Update the capability histogram with the new peer's capabilities.
293        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    // ── Requirement Checking ──────────────────────────────────────────────────
307
308    /// Returns `true` if `peer` satisfies the configured `require_all` and
309    /// `require_any` constraints.
310    ///
311    /// - `require_all`: the peer must possess every listed capability.
312    /// - `require_any`: the peer must possess at least one listed capability
313    ///   (no constraint when the list is empty).
314    pub fn meets_requirements(&self, peer: &PeerCapabilitySet) -> bool {
315        // All of require_all must be present.
316        if !peer.has_all(&self.config.require_all) {
317            return false;
318        }
319        // At least one of require_any must be present (skip check when empty).
320        if !self.config.require_any.is_empty() && !peer.has_any(&self.config.require_any) {
321            return false;
322        }
323        true
324    }
325
326    // ── Removal ───────────────────────────────────────────────────────────────
327
328    /// Remove the peer identified by `peer_id` from the registry.
329    ///
330    /// Returns `true` if the peer was present and removed, `false` otherwise.
331    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    // ── Lookup ────────────────────────────────────────────────────────────────
346
347    /// Retrieve the [`PeerCapabilitySet`] for `peer_id`, if present.
348    pub fn get_peer(&self, peer_id: &str) -> Option<&PeerCapabilitySet> {
349        self.peers.get(peer_id)
350    }
351
352    /// Return all peers that advertise `cap`.
353    pub fn peers_with_capability(&self, cap: &PeerCapability) -> Vec<&PeerCapabilitySet> {
354        self.peers.values().filter(|p| p.has(cap)).collect()
355    }
356
357    /// Return all peers that advertise **every** capability in `caps`.
358    pub fn peers_with_all(&self, caps: &[PeerCapability]) -> Vec<&PeerCapabilitySet> {
359        self.peers.values().filter(|p| p.has_all(caps)).collect()
360    }
361
362    /// Return all peers that advertise **at least one** capability in `caps`.
363    pub fn peers_with_any(&self, caps: &[PeerCapability]) -> Vec<&PeerCapabilitySet> {
364        self.peers.values().filter(|p| p.has_any(caps)).collect()
365    }
366
367    // ── Expiry ────────────────────────────────────────────────────────────────
368
369    /// Remove all peers whose `advertised_at` is older than `now - ttl_ms`.
370    ///
371    /// Returns the number of entries purged.
372    pub fn purge_expired(&mut self, now: u64) -> usize {
373        let ttl_ms = self.config.ttl_ms;
374
375        // Collect keys to remove first to satisfy the borrow checker.
376        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            // Use `remove` to update stats counters correctly.
386            self.remove(&id);
387            // `remove` increments `total_removed`; adjust back so TTL-based
388            // purges are distinguishable from explicit removals if the caller
389            // tracks `total_removed` themselves.  Currently we keep them in
390            // the same bucket — callers can diff the before/after value.
391        }
392        count
393    }
394
395    // ── Capability Naming ─────────────────────────────────────────────────────
396
397    /// Returns a `&'static str` label for well-known capability variants.
398    ///
399    /// For [`PeerCapability::Custom`] the returned string is always
400    /// `"custom"`.  Use [`PeerCapability::key`] when you need the full
401    /// dynamic key including the custom payload.
402    pub fn capability_name(cap: &PeerCapability) -> &'static str {
403        cap.static_name()
404    }
405
406    // ── Accessors ─────────────────────────────────────────────────────────────
407
408    /// Returns the number of peers currently in the registry.
409    pub fn peer_count(&self) -> usize {
410        self.peers.len()
411    }
412
413    /// Returns a reference to the runtime statistics snapshot.
414    ///
415    /// The snapshot reflects the live state of the registry; the histogram
416    /// entries are decremented on removal, so they track currently registered
417    /// peers rather than lifetime totals.
418    pub fn stats(&self) -> &CapabilityStats {
419        &self.stats
420    }
421
422    /// Returns the local capabilities configured for this node.
423    pub fn local_capabilities(&self) -> &[PeerCapability] {
424        &self.config.local_capabilities
425    }
426}
427
428// ── Tests ─────────────────────────────────────────────────────────────────────
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    // ── Helpers ───────────────────────────────────────────────────────────────
435
436    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    // ── 1. Basic registration ─────────────────────────────────────────────────
461
462    #[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    // ── 2. Stats total_registered increments ─────────────────────────────────
471
472    #[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    // ── 3. require_all — accept matching peer ─────────────────────────────────
481
482    #[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    // ── 4. require_all — reject peer missing one capability ───────────────────
498
499    #[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        // Peer only has BitswapV1 — missing DHTKademlia.
507        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    // ── 5. require_any — accept peer with one matching ────────────────────────
514
515    #[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    // ── 6. require_any — reject peer with none matching ───────────────────────
527
528    #[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    // ── 7. Empty requirements accept any peer ─────────────────────────────────
541
542    #[test]
543    fn test_empty_requirements_accept_all() {
544        let mut reg = CapabilityRegistry::new(default_config());
545        // No capabilities at all — should still pass with no requirements.
546        let peer = make_peer("p1", [], 0);
547        assert!(reg.register(peer));
548    }
549
550    // ── 8. peers_with_capability ──────────────────────────────────────────────
551
552    #[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    // ── 9. peers_with_all ─────────────────────────────────────────────────────
571
572    #[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    // ── 10. peers_with_any ────────────────────────────────────────────────────
589
590    #[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    // ── 11. peers_with_any — empty slice returns all peers ────────────────────
603
604    #[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        // Empty caps slice → vacuous truth → all peers returned.
611        let result = reg.peers_with_any(&[]);
612        assert_eq!(result.len(), 2);
613    }
614
615    // ── 12. peers_with_all — empty slice returns all peers ────────────────────
616
617    #[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        // Empty caps slice → every peer has_all([]) trivially.
624        let result = reg.peers_with_all(&[]);
625        assert_eq!(result.len(), 2);
626    }
627
628    // ── 13. remove ────────────────────────────────────────────────────────────
629
630    #[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    // ── 14. remove non-existent returns false ─────────────────────────────────
641
642    #[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    // ── 15. purge_expired ─────────────────────────────────────────────────────
650
651    #[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        // Peer advertised at t=0, now is t=2000 → expired.
660        reg.register(make_peer("old", [PeerCapability::Relay], 0));
661        // Peer advertised at t=5000, now is t=2000 → still fresh.
662        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    // ── 16. purge_expired — nothing to purge ─────────────────────────────────
672
673    #[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    // ── 17. purge_expired — all peers expired ────────────────────────────────
683
684    #[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    // ── 18. max_peers limit ───────────────────────────────────────────────────
699
700    #[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        // Third peer should be rejected.
710        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    // ── 19. Re-registering the same peer_id replaces entry ───────────────────
716
717    #[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        // Re-register with updated capabilities.
722        reg.register(make_peer(
723            "p1",
724            [PeerCapability::BitswapV2, PeerCapability::DHTKademlia],
725            200,
726        ));
727        // Peer count should still be 1.
728        assert_eq!(reg.peer_count(), 1);
729        // The stored entry should reflect the update.
730        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    // ── 20. capability distribution stats ────────────────────────────────────
736
737    #[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 = &reg.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    // ── 21. Distribution decrements on remove ────────────────────────────────
753
754    #[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    // ── 22. Custom capability ─────────────────────────────────────────────────
780
781    #[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    // ── 23. capability_name static labels ────────────────────────────────────
793
794    #[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    // ── 24. local_capabilities accessor ──────────────────────────────────────
815
816    #[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    // ── 25. get_peer returns None for unknown peer ────────────────────────────
830
831    #[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    // ── 26. require_all + require_any combined ────────────────────────────────
838
839    #[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        // Has BitswapV1 + Relay → should be accepted.
849        let ok = make_peer("ok", [PeerCapability::BitswapV1, PeerCapability::Relay], 0);
850        assert!(reg.register(ok));
851
852        // Has BitswapV1 but neither Relay nor NatTraversal → rejected.
853        let no_any = make_peer(
854            "no_any",
855            [PeerCapability::BitswapV1, PeerCapability::GossipSub],
856            0,
857        );
858        assert!(!reg.register(no_any));
859
860        // Has Relay but not BitswapV1 → rejected by require_all.
861        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    // ── 27. meets_requirements without registration ───────────────────────────
868
869    #[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    // ── 28. TTL boundary — exactly at expiry ──────────────────────────────────
886
887    #[test]
888    fn test_ttl_boundary_exact() {
889        // ttl_ms = 1000, advertised_at = 0, now = 1000 → expired (>=).
890        let set = make_peer("p", [PeerCapability::Relay], 0);
891        assert!(set.is_expired(1_000, 1_000));
892
893        // now = 999 → still fresh.
894        assert!(!set.is_expired(999, 1_000));
895    }
896
897    // ── 29. Distribution is correct after re-register ─────────────────────────
898
899    #[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        // Re-register p1 with a different capability set.
904        reg.register(make_peer("p1", [PeerCapability::GossipSub], 100));
905
906        let dist = &reg.stats().capability_distribution;
907        // relay should be 0 (old entry removed), gossipsub should be 1.
908        assert_eq!(dist.get("relay").copied().unwrap_or(0), 0);
909        assert_eq!(dist.get("gossipsub").copied().unwrap_or(0), 1);
910    }
911
912    // ── 30. Overlap query — peers_with_all vs peers_with_any ─────────────────
913
914    #[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}