Skip to main content

ipfrs_network/
peer_discovery_protocol.rs

1//! Multi-Strategy Peer Discovery Protocol
2//!
3//! [`PeerDiscoveryProtocol`] implements a production-grade, multi-strategy peer
4//! discovery engine that tracks peers discovered via Bootstrap, mDNS, DHT,
5//! PeerExchange, Static configuration, and Rendezvous namespaces.
6//!
7//! # Design overview
8//!
9//! * **Deduplication** – peers are keyed by `id`; adding an already-known peer
10//!   merges its new addresses and refreshes TTL.
11//! * **TTL-based expiry** – every peer carries a `discovered_at + ttl_us`
12//!   deadline; [`PeerDiscoveryProtocol::expire_peers`] removes stale entries.
13//! * **Event sourcing** – every state transition emits a [`PdpDiscoveryEvent`]
14//!   that callers drain with [`PeerDiscoveryProtocol::drain_events`].
15//! * **PRNG** – random peer selection uses a lock-free xorshift64 PRNG; no
16//!   `rand` crate dependency.
17//! * **Capability filtering** – peers are annotated with free-form capability
18//!   strings so callers can select specialized subsets.
19
20use std::collections::HashMap;
21
22// ---------------------------------------------------------------------------
23// PRNG (no rand crate)
24// ---------------------------------------------------------------------------
25
26/// xorshift64 PRNG — fast, non-cryptographic, no external dependencies.
27#[inline]
28pub fn xorshift64(state: &mut u64) -> u64 {
29    let mut x = *state;
30    x ^= x << 13;
31    x ^= x >> 7;
32    x ^= x << 17;
33    *state = x;
34    x
35}
36
37// ---------------------------------------------------------------------------
38// Discovery method
39// ---------------------------------------------------------------------------
40
41/// The mechanism that surfaced a particular [`PdpDiscoveredPeer`].
42#[derive(Clone, Debug, PartialEq, Eq, Hash)]
43pub enum PdpDiscoveryMethod {
44    /// Peer came from a bootstrap service at the given URL.
45    Bootstrap(String),
46    /// Local-network multicast discovery via mDNS.
47    Mdns {
48        /// mDNS service name (e.g. `_ipfrs._tcp.local`).
49        service_name: String,
50    },
51    /// Discovered through Kademlia DHT lookup for the given key.
52    Dht {
53        /// The DHT lookup key used to find the peer.
54        query_key: String,
55    },
56    /// Another connected peer advertised this peer (PEX / peer exchange).
57    PeerExchange {
58        /// The peer-id of the peer that advertised this entry.
59        source_peer: String,
60    },
61    /// Statically configured in application settings.
62    Static(String),
63    /// Discovered through a rendezvous point in the given namespace.
64    Rendezvous {
65        /// Rendezvous namespace (e.g. `"/ipfrs/mainnet"`).
66        namespace: String,
67    },
68}
69
70impl PdpDiscoveryMethod {
71    /// Return the variant name as a lowercase string (used for stats bucketing
72    /// and filtering).
73    pub fn variant_name(&self) -> &'static str {
74        match self {
75            PdpDiscoveryMethod::Bootstrap(_) => "bootstrap",
76            PdpDiscoveryMethod::Mdns { .. } => "mdns",
77            PdpDiscoveryMethod::Dht { .. } => "dht",
78            PdpDiscoveryMethod::PeerExchange { .. } => "peer_exchange",
79            PdpDiscoveryMethod::Static(_) => "static",
80            PdpDiscoveryMethod::Rendezvous { .. } => "rendezvous",
81        }
82    }
83}
84
85// ---------------------------------------------------------------------------
86// Discovered peer
87// ---------------------------------------------------------------------------
88
89/// A peer that has been discovered through one of the supported discovery
90/// strategies and is being tracked by [`PeerDiscoveryProtocol`].
91#[derive(Clone, Debug)]
92pub struct PdpDiscoveredPeer {
93    /// Unique peer identifier (e.g., libp2p PeerId as string).
94    pub id: String,
95    /// Known multiaddresses for this peer.
96    pub addresses: Vec<String>,
97    /// Unix timestamp in microseconds when this peer was first recorded.
98    pub discovered_at: u64,
99    /// Which mechanism surfaced this peer.
100    pub discovery_method: PdpDiscoveryMethod,
101    /// Time-to-live in microseconds, measured from `discovered_at`.
102    pub ttl_us: u64,
103    /// Whether the peer has been verified (successfully dialled / handshaked).
104    pub verified: bool,
105    /// Free-form capability strings (e.g., `["bitswap/1.2.0", "relay/v2"]`).
106    pub capabilities: Vec<String>,
107    /// Unix timestamp in microseconds when the peer was last verified.
108    /// `0` means never verified.
109    pub(crate) verified_at: u64,
110    /// Internal: current effective expiry = `discovered_at + ttl_us`.
111    /// This is updated on refresh so we don't recompute it repeatedly.
112    pub(crate) expires_at: u64,
113}
114
115impl PdpDiscoveredPeer {
116    /// Construct a new [`PdpDiscoveredPeer`] and compute its initial expiry.
117    pub fn new(
118        id: impl Into<String>,
119        addresses: Vec<String>,
120        discovered_at: u64,
121        discovery_method: PdpDiscoveryMethod,
122        ttl_us: u64,
123        capabilities: Vec<String>,
124    ) -> Self {
125        let expires_at = discovered_at.saturating_add(ttl_us);
126        Self {
127            id: id.into(),
128            addresses,
129            discovered_at,
130            discovery_method,
131            ttl_us,
132            verified: false,
133            capabilities,
134            verified_at: 0,
135            expires_at,
136        }
137    }
138
139    /// Returns `true` if this peer has expired relative to `current_ts`.
140    #[inline]
141    pub fn is_expired(&self, current_ts: u64) -> bool {
142        // Use the precomputed `expires_at` for constant-time check.
143        self.expires_at < current_ts
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Discovery config
149// ---------------------------------------------------------------------------
150
151/// Configuration for [`PeerDiscoveryProtocol`].
152#[derive(Clone, Debug)]
153pub struct PdpDiscoveryConfig {
154    /// Maximum number of peers the table may hold simultaneously.
155    pub max_peers: usize,
156    /// Default TTL in microseconds for newly added peers.
157    /// Defaults to 3 600 000 000 µs (1 hour).
158    pub peer_ttl_us: u64,
159    /// Enable mDNS-sourced peers.
160    pub enable_mdns: bool,
161    /// Enable DHT-sourced peers.
162    pub enable_dht: bool,
163    /// Enable peer-exchange-sourced peers.
164    pub enable_peer_exchange: bool,
165    /// Enable rendezvous-sourced peers.
166    pub enable_rendezvous: bool,
167    /// Bootstrap peer addresses (used to seed the peer table on startup).
168    pub bootstrap_peers: Vec<String>,
169    /// How often (in µs) the caller is expected to call `expire_peers`.
170    /// Informational; the protocol does not schedule anything itself.
171    pub refresh_interval_us: u64,
172}
173
174impl Default for PdpDiscoveryConfig {
175    fn default() -> Self {
176        Self {
177            max_peers: 1024,
178            peer_ttl_us: 3_600_000_000, // 1 hour
179            enable_mdns: true,
180            enable_dht: true,
181            enable_peer_exchange: true,
182            enable_rendezvous: true,
183            bootstrap_peers: Vec::new(),
184            refresh_interval_us: 60_000_000, // 1 minute
185        }
186    }
187}
188
189// ---------------------------------------------------------------------------
190// Discovery events
191// ---------------------------------------------------------------------------
192
193/// Events emitted by [`PeerDiscoveryProtocol`] as state transitions occur.
194#[derive(Clone, Debug)]
195pub enum PdpDiscoveryEvent {
196    /// A new peer was added to the table, or an existing peer was updated.
197    PeerDiscovered(PdpDiscoveredPeer),
198    /// A peer's TTL elapsed and it was removed from the table.
199    PeerExpired(String),
200    /// A peer was successfully verified.
201    PeerVerified(String),
202    /// A discovery method failed to produce results.
203    DiscoveryMethodFailed {
204        /// The variant name of the failing method (e.g., `"dht"`).
205        method: String,
206        /// Human-readable reason string.
207        reason: String,
208    },
209    /// The peer table reached capacity; the number of active peers is reported.
210    PeerTableFull(usize),
211}
212
213// ---------------------------------------------------------------------------
214// Statistics
215// ---------------------------------------------------------------------------
216
217/// Aggregate statistics emitted by [`PeerDiscoveryProtocol::stats`].
218#[derive(Clone, Debug, Default)]
219pub struct PdpDiscoveryStats {
220    /// Total peers ever added (including re-additions / merges).
221    pub total_discovered: u64,
222    /// Number of currently active (non-expired) peers.
223    pub active_peers: usize,
224    /// Total peers evicted by TTL expiry.
225    pub expired_peers: u64,
226    /// Number of peer verification calls performed.
227    pub verifications_performed: u64,
228    /// Per-method counts: `(method_name, count)`.
229    pub by_method: Vec<(String, u64)>,
230}
231
232// ---------------------------------------------------------------------------
233// Errors
234// ---------------------------------------------------------------------------
235
236/// Errors returned by [`PeerDiscoveryProtocol`] operations.
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub enum PdpDiscoveryError {
239    /// No peer with the given id was found in the table.
240    PeerNotFound(String),
241    /// The peer table is at capacity and cannot accept new entries.
242    MaxPeersExceeded,
243    /// The supplied multiaddress is not valid.
244    InvalidAddress(String),
245    /// A peer with the same id was already present (used informally; normally
246    /// re-additions are merged, not rejected).
247    DuplicatePeer(String),
248    /// A configuration value is invalid or inconsistent.
249    ConfigurationError(String),
250}
251
252impl std::fmt::Display for PdpDiscoveryError {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        match self {
255            PdpDiscoveryError::PeerNotFound(id) => write!(f, "peer not found: {id}"),
256            PdpDiscoveryError::MaxPeersExceeded => write!(f, "peer table is full"),
257            PdpDiscoveryError::InvalidAddress(addr) => write!(f, "invalid address: {addr}"),
258            PdpDiscoveryError::DuplicatePeer(id) => write!(f, "duplicate peer: {id}"),
259            PdpDiscoveryError::ConfigurationError(msg) => {
260                write!(f, "configuration error: {msg}")
261            }
262        }
263    }
264}
265
266impl std::error::Error for PdpDiscoveryError {}
267
268// ---------------------------------------------------------------------------
269// Internal per-method counter helper
270// ---------------------------------------------------------------------------
271
272/// Internal per-method discovery counter used inside [`PeerDiscoveryProtocol`].
273#[derive(Debug, Default)]
274struct MethodCounts {
275    counts: HashMap<String, u64>,
276}
277
278impl MethodCounts {
279    fn increment(&mut self, name: &str) {
280        *self.counts.entry(name.to_string()).or_insert(0) += 1;
281    }
282
283    fn to_vec(&self) -> Vec<(String, u64)> {
284        let mut v: Vec<(String, u64)> = self.counts.iter().map(|(k, v)| (k.clone(), *v)).collect();
285        v.sort_by(|a, b| a.0.cmp(&b.0));
286        v
287    }
288}
289
290// ---------------------------------------------------------------------------
291// Core protocol
292// ---------------------------------------------------------------------------
293
294/// Multi-strategy peer discovery protocol.
295///
296/// Maintains a bounded peer table with TTL expiry, verification tracking,
297/// capability filtering, and per-method statistics.  All state transitions emit
298/// [`PdpDiscoveryEvent`] values buffered internally and drained by the caller.
299#[derive(Debug)]
300pub struct PeerDiscoveryProtocol {
301    /// Peer table keyed by peer id.
302    peers: HashMap<String, PdpDiscoveredPeer>,
303    /// Effective configuration.
304    config: PdpDiscoveryConfig,
305    /// Pending event buffer.
306    events: Vec<PdpDiscoveryEvent>,
307    /// Running total of peers ever added.
308    total_discovered: u64,
309    /// Running total of peers expired.
310    expired_peers: u64,
311    /// Running total of verification calls.
312    verifications_performed: u64,
313    /// Per-method add counts.
314    method_counts: MethodCounts,
315}
316
317impl PeerDiscoveryProtocol {
318    /// Create a new protocol instance with the given configuration.
319    ///
320    /// # Errors
321    ///
322    /// Returns [`PdpDiscoveryError::ConfigurationError`] if `max_peers` is 0
323    /// or `peer_ttl_us` is 0.
324    pub fn new(config: PdpDiscoveryConfig) -> Result<Self, PdpDiscoveryError> {
325        if config.max_peers == 0 {
326            return Err(PdpDiscoveryError::ConfigurationError(
327                "max_peers must be > 0".into(),
328            ));
329        }
330        if config.peer_ttl_us == 0 {
331            return Err(PdpDiscoveryError::ConfigurationError(
332                "peer_ttl_us must be > 0".into(),
333            ));
334        }
335        Ok(Self {
336            peers: HashMap::new(),
337            config,
338            events: Vec::new(),
339            total_discovered: 0,
340            expired_peers: 0,
341            verifications_performed: 0,
342            method_counts: MethodCounts::default(),
343        })
344    }
345
346    /// Create a new protocol instance with the default configuration.
347    pub fn with_defaults() -> Self {
348        // Safety: default config is always valid.
349        Self::new(PdpDiscoveryConfig::default()).expect("default config is always valid")
350    }
351
352    // -----------------------------------------------------------------------
353    // Peer management
354    // -----------------------------------------------------------------------
355
356    /// Add a peer to the table.
357    ///
358    /// * If the peer is **new** and the table has capacity, it is inserted and
359    ///   [`PdpDiscoveryEvent::PeerDiscovered`] is emitted.
360    /// * If the peer **already exists**, its addresses are merged (deduped) and
361    ///   its TTL is refreshed; [`PdpDiscoveryEvent::PeerDiscovered`] is emitted
362    ///   again to signal the update.
363    /// * If the table is **full** and the peer is unknown,
364    ///   [`PdpDiscoveryError::MaxPeersExceeded`] is returned and
365    ///   [`PdpDiscoveryEvent::PeerTableFull`] is emitted.
366    pub fn add_peer(
367        &mut self,
368        peer: PdpDiscoveredPeer,
369    ) -> Result<PdpDiscoveryEvent, PdpDiscoveryError> {
370        let method_name = peer.discovery_method.variant_name().to_string();
371
372        if let Some(existing) = self.peers.get_mut(&peer.id) {
373            // Merge addresses (deduplicate).
374            for addr in &peer.addresses {
375                if !existing.addresses.contains(addr) {
376                    existing.addresses.push(addr.clone());
377                }
378            }
379            // Refresh TTL.
380            existing.ttl_us = peer.ttl_us;
381            existing.expires_at = existing.discovered_at.saturating_add(peer.ttl_us);
382            // Merge capabilities.
383            for cap in &peer.capabilities {
384                if !existing.capabilities.contains(cap) {
385                    existing.capabilities.push(cap.clone());
386                }
387            }
388            self.total_discovered += 1;
389            self.method_counts.increment(&method_name);
390
391            let event = PdpDiscoveryEvent::PeerDiscovered(existing.clone());
392            self.events.push(event.clone());
393            return Ok(event);
394        }
395
396        // New peer — check capacity.
397        if self.peers.len() >= self.config.max_peers {
398            let active = self.peers.len();
399            let full_event = PdpDiscoveryEvent::PeerTableFull(active);
400            self.events.push(full_event);
401            return Err(PdpDiscoveryError::MaxPeersExceeded);
402        }
403
404        self.total_discovered += 1;
405        self.method_counts.increment(&method_name);
406
407        let event = PdpDiscoveryEvent::PeerDiscovered(peer.clone());
408        self.events.push(event.clone());
409        self.peers.insert(peer.id.clone(), peer);
410        Ok(event)
411    }
412
413    /// Remove a peer by id.
414    ///
415    /// # Errors
416    ///
417    /// Returns [`PdpDiscoveryError::PeerNotFound`] if the peer does not exist.
418    pub fn remove_peer(&mut self, id: &str) -> Result<(), PdpDiscoveryError> {
419        self.peers
420            .remove(id)
421            .map(|_| ())
422            .ok_or_else(|| PdpDiscoveryError::PeerNotFound(id.to_string()))
423    }
424
425    /// Mark a peer as verified and record the current timestamp.
426    ///
427    /// Emits [`PdpDiscoveryEvent::PeerVerified`] on success.
428    ///
429    /// # Errors
430    ///
431    /// Returns [`PdpDiscoveryError::PeerNotFound`] if the peer does not exist.
432    pub fn verify_peer(
433        &mut self,
434        id: &str,
435        current_ts: u64,
436    ) -> Result<PdpDiscoveryEvent, PdpDiscoveryError> {
437        let peer = self
438            .peers
439            .get_mut(id)
440            .ok_or_else(|| PdpDiscoveryError::PeerNotFound(id.to_string()))?;
441        peer.verified = true;
442        peer.verified_at = current_ts;
443        self.verifications_performed += 1;
444        let event = PdpDiscoveryEvent::PeerVerified(id.to_string());
445        self.events.push(event.clone());
446        Ok(event)
447    }
448
449    /// Remove all peers whose TTL has elapsed relative to `current_ts`.
450    ///
451    /// Returns one [`PdpDiscoveryEvent::PeerExpired`] per removed peer.
452    pub fn expire_peers(&mut self, current_ts: u64) -> Vec<PdpDiscoveryEvent> {
453        let expired_ids: Vec<String> = self
454            .peers
455            .iter()
456            .filter(|(_, p)| p.is_expired(current_ts))
457            .map(|(id, _)| id.clone())
458            .collect();
459
460        let mut events = Vec::with_capacity(expired_ids.len());
461        for id in &expired_ids {
462            self.peers.remove(id);
463            self.expired_peers += 1;
464            let ev = PdpDiscoveryEvent::PeerExpired(id.clone());
465            self.events.push(ev.clone());
466            events.push(ev);
467        }
468        events
469    }
470
471    /// Reset the TTL of a known peer to `current_ts + config.peer_ttl_us`.
472    ///
473    /// # Errors
474    ///
475    /// Returns [`PdpDiscoveryError::PeerNotFound`] if the peer does not exist.
476    pub fn refresh_peer(&mut self, id: &str, current_ts: u64) -> Result<(), PdpDiscoveryError> {
477        let peer = self
478            .peers
479            .get_mut(id)
480            .ok_or_else(|| PdpDiscoveryError::PeerNotFound(id.to_string()))?;
481        peer.discovered_at = current_ts;
482        peer.ttl_us = self.config.peer_ttl_us;
483        peer.expires_at = current_ts.saturating_add(self.config.peer_ttl_us);
484        Ok(())
485    }
486
487    // -----------------------------------------------------------------------
488    // Lookups
489    // -----------------------------------------------------------------------
490
491    /// Retrieve an immutable reference to a peer by id.
492    ///
493    /// # Errors
494    ///
495    /// Returns [`PdpDiscoveryError::PeerNotFound`] if the peer does not exist.
496    pub fn get_peer(&self, id: &str) -> Result<&PdpDiscoveredPeer, PdpDiscoveryError> {
497        self.peers
498            .get(id)
499            .ok_or_else(|| PdpDiscoveryError::PeerNotFound(id.to_string()))
500    }
501
502    /// Return all peers discovered by the given method **variant name**.
503    ///
504    /// The `method` string must match one of the values returned by
505    /// [`PdpDiscoveryMethod::variant_name`] (e.g., `"bootstrap"`, `"mdns"`).
506    pub fn peers_by_method(&self, method: &str) -> Vec<&PdpDiscoveredPeer> {
507        self.peers
508            .values()
509            .filter(|p| p.discovery_method.variant_name() == method)
510            .collect()
511    }
512
513    /// Return all peers that have the given capability string.
514    pub fn peers_with_capability(&self, capability: &str) -> Vec<&PdpDiscoveredPeer> {
515        self.peers
516            .values()
517            .filter(|p| p.capabilities.iter().any(|c| c == capability))
518            .collect()
519    }
520
521    /// Return up to `n` randomly selected peers using an xorshift64-based
522    /// Fisher-Yates shuffle seeded with `seed`.
523    ///
524    /// If the table has fewer than `n` peers, all peers are returned in
525    /// shuffled order.
526    pub fn random_peers(&self, n: usize, seed: u64) -> Vec<&PdpDiscoveredPeer> {
527        let mut indices: Vec<usize> = (0..self.peers.len()).collect();
528        let peers_vec: Vec<&PdpDiscoveredPeer> = self.peers.values().collect();
529        let mut state = if seed == 0 {
530            0xDEAD_BEEF_CAFE_1337
531        } else {
532            seed
533        };
534
535        // Partial Fisher-Yates: shuffle only the first `take` elements.
536        let take = n.min(indices.len());
537        for i in 0..take {
538            let rand_val = xorshift64(&mut state);
539            let j = i + (rand_val as usize % (indices.len() - i));
540            indices.swap(i, j);
541        }
542
543        indices
544            .into_iter()
545            .take(take)
546            .map(|i| peers_vec[i])
547            .collect()
548    }
549
550    // -----------------------------------------------------------------------
551    // Bulk import (PeerExchange)
552    // -----------------------------------------------------------------------
553
554    /// Bulk-import peers from a `PeerExchange` handshake.
555    ///
556    /// For each peer in `other`:
557    /// * Skip if already expired (`discovered_at + ttl_us < current_ts`).
558    /// * If the peer is already known, merge addresses + capabilities and
559    ///   refresh TTL.
560    /// * If the peer is new, insert it (subject to capacity).
561    ///
562    /// Returns the list of events generated.
563    pub fn merge_peer_table(
564        &mut self,
565        other: Vec<PdpDiscoveredPeer>,
566        current_ts: u64,
567    ) -> Vec<PdpDiscoveryEvent> {
568        let mut events = Vec::new();
569        for peer in other {
570            // Skip already-expired peers.
571            if peer.is_expired(current_ts) {
572                continue;
573            }
574            match self.add_peer(peer) {
575                Ok(ev) => events.push(ev),
576                Err(PdpDiscoveryError::MaxPeersExceeded) => {
577                    // PeerTableFull event was already pushed to self.events.
578                    // Include it in the returned slice so the caller sees it.
579                    if let Some(ev) = self.events.last().cloned() {
580                        events.push(ev);
581                    }
582                    // Stop importing once the table is full.
583                    break;
584                }
585                Err(_) => {
586                    // Other errors during bulk import are silently skipped.
587                }
588            }
589        }
590        events
591    }
592
593    // -----------------------------------------------------------------------
594    // Statistics and event drain
595    // -----------------------------------------------------------------------
596
597    /// Return a snapshot of current statistics.
598    pub fn stats(&self) -> PdpDiscoveryStats {
599        PdpDiscoveryStats {
600            total_discovered: self.total_discovered,
601            active_peers: self.peers.len(),
602            expired_peers: self.expired_peers,
603            verifications_performed: self.verifications_performed,
604            by_method: self.method_counts.to_vec(),
605        }
606    }
607
608    /// Drain and return all buffered events.
609    ///
610    /// After this call the internal event buffer is empty.
611    pub fn drain_events(&mut self) -> Vec<PdpDiscoveryEvent> {
612        std::mem::take(&mut self.events)
613    }
614
615    /// Return the current number of peers in the table.
616    #[inline]
617    pub fn peer_count(&self) -> usize {
618        self.peers.len()
619    }
620
621    /// Return an iterator over all peers currently in the table.
622    pub fn all_peers(&self) -> impl Iterator<Item = &PdpDiscoveredPeer> {
623        self.peers.values()
624    }
625
626    /// Record a discovery method failure, emitting
627    /// [`PdpDiscoveryEvent::DiscoveryMethodFailed`].
628    pub fn record_method_failure(&mut self, method: impl Into<String>, reason: impl Into<String>) {
629        let ev = PdpDiscoveryEvent::DiscoveryMethodFailed {
630            method: method.into(),
631            reason: reason.into(),
632        };
633        self.events.push(ev);
634    }
635}
636
637// ---------------------------------------------------------------------------
638// Type aliases for the task spec  (PdpXxx → Xxx)
639// ---------------------------------------------------------------------------
640
641/// Type alias: `DiscoveredPeer` in the peer_discovery_protocol namespace.
642pub type DiscoveredPeer = PdpDiscoveredPeer;
643
644/// Type alias: `DiscoveryConfig` in the peer_discovery_protocol namespace.
645pub type DiscoveryConfig = PdpDiscoveryConfig;
646
647/// Type alias: `DiscoveryStats` in the peer_discovery_protocol namespace.
648pub type DiscoveryStats = PdpDiscoveryStats;
649
650/// Type alias: `DiscoveryEvent` in the peer_discovery_protocol namespace.
651pub type DiscoveryEvent = PdpDiscoveryEvent;
652
653/// Type alias: `DiscoveryError` in the peer_discovery_protocol namespace.
654pub type DiscoveryError = PdpDiscoveryError;
655
656// ---------------------------------------------------------------------------
657// Tests
658// ---------------------------------------------------------------------------
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    // -----------------------------------------------------------------------
665    // Helpers
666    // -----------------------------------------------------------------------
667
668    fn make_ts(offset_us: u64) -> u64 {
669        1_000_000_000_000 + offset_us
670    }
671
672    fn default_config() -> PdpDiscoveryConfig {
673        PdpDiscoveryConfig {
674            max_peers: 16,
675            peer_ttl_us: 3_600_000_000,
676            ..PdpDiscoveryConfig::default()
677        }
678    }
679
680    fn make_peer(id: &str, method: PdpDiscoveryMethod) -> PdpDiscoveredPeer {
681        PdpDiscoveredPeer::new(
682            id,
683            vec![format!("/ip4/127.0.0.1/tcp/{}", id.len() * 1000)],
684            make_ts(0),
685            method,
686            3_600_000_000,
687            vec!["bitswap/1.2.0".into()],
688        )
689    }
690
691    fn make_peer_with_caps(id: &str, caps: Vec<&str>) -> PdpDiscoveredPeer {
692        PdpDiscoveredPeer::new(
693            id,
694            vec!["/ip4/10.0.0.1/tcp/4001".into()],
695            make_ts(0),
696            PdpDiscoveryMethod::Bootstrap("http://boot.example.com".into()),
697            3_600_000_000,
698            caps.into_iter().map(String::from).collect(),
699        )
700    }
701
702    fn make_protocol() -> PeerDiscoveryProtocol {
703        PeerDiscoveryProtocol::new(default_config()).expect("valid config")
704    }
705
706    // -----------------------------------------------------------------------
707    // Construction
708    // -----------------------------------------------------------------------
709
710    #[test]
711    fn test_new_with_defaults() {
712        let p = PeerDiscoveryProtocol::with_defaults();
713        assert_eq!(p.peer_count(), 0);
714    }
715
716    #[test]
717    fn test_new_with_valid_config() {
718        let cfg = PdpDiscoveryConfig {
719            max_peers: 100,
720            peer_ttl_us: 60_000_000,
721            ..PdpDiscoveryConfig::default()
722        };
723        let p = PeerDiscoveryProtocol::new(cfg);
724        assert!(p.is_ok());
725    }
726
727    #[test]
728    fn test_new_zero_max_peers_fails() {
729        let cfg = PdpDiscoveryConfig {
730            max_peers: 0,
731            ..PdpDiscoveryConfig::default()
732        };
733        let err =
734            PeerDiscoveryProtocol::new(cfg).expect_err("test: zero max_peers config should fail");
735        assert!(matches!(err, PdpDiscoveryError::ConfigurationError(_)));
736    }
737
738    #[test]
739    fn test_new_zero_ttl_fails() {
740        let cfg = PdpDiscoveryConfig {
741            peer_ttl_us: 0,
742            ..PdpDiscoveryConfig::default()
743        };
744        let err = PeerDiscoveryProtocol::new(cfg).expect_err("test: zero ttl config should fail");
745        assert!(matches!(err, PdpDiscoveryError::ConfigurationError(_)));
746    }
747
748    // -----------------------------------------------------------------------
749    // Add peer
750    // -----------------------------------------------------------------------
751
752    #[test]
753    fn test_add_peer_basic() {
754        let mut p = make_protocol();
755        let peer = make_peer("peer-1", PdpDiscoveryMethod::Bootstrap("url".into()));
756        let result = p.add_peer(peer);
757        assert!(result.is_ok());
758        assert_eq!(p.peer_count(), 1);
759    }
760
761    #[test]
762    fn test_add_peer_emits_discovered_event() {
763        let mut p = make_protocol();
764        let peer = make_peer("peer-1", PdpDiscoveryMethod::Bootstrap("url".into()));
765        let ev = p
766            .add_peer(peer)
767            .expect("test: add_peer should succeed for new peer");
768        assert!(matches!(ev, PdpDiscoveryEvent::PeerDiscovered(_)));
769    }
770
771    #[test]
772    fn test_add_peer_duplicate_merges_addresses() {
773        let mut p = make_protocol();
774        let peer1 = PdpDiscoveredPeer::new(
775            "dup-peer",
776            vec!["/ip4/1.2.3.4/tcp/4001".into()],
777            make_ts(0),
778            PdpDiscoveryMethod::Bootstrap("url".into()),
779            3_600_000_000,
780            vec![],
781        );
782        let peer2 = PdpDiscoveredPeer::new(
783            "dup-peer",
784            vec!["/ip4/5.6.7.8/tcp/4001".into()],
785            make_ts(10),
786            PdpDiscoveryMethod::Bootstrap("url".into()),
787            3_600_000_000,
788            vec![],
789        );
790        p.add_peer(peer1)
791            .expect("test: add first peer should succeed");
792        p.add_peer(peer2)
793            .expect("test: add duplicate peer should succeed (merge)");
794        let stored = p
795            .get_peer("dup-peer")
796            .expect("test: get_peer should find dup-peer");
797        assert_eq!(stored.addresses.len(), 2);
798        assert_eq!(p.peer_count(), 1);
799    }
800
801    #[test]
802    fn test_add_peer_duplicate_deduplicates_addresses() {
803        let mut p = make_protocol();
804        let addr = "/ip4/1.2.3.4/tcp/4001";
805        let peer1 = PdpDiscoveredPeer::new(
806            "dup2",
807            vec![addr.into()],
808            make_ts(0),
809            PdpDiscoveryMethod::Mdns {
810                service_name: "svc".into(),
811            },
812            3_600_000_000,
813            vec![],
814        );
815        let peer2 = PdpDiscoveredPeer::new(
816            "dup2",
817            vec![addr.into()], // same address
818            make_ts(1),
819            PdpDiscoveryMethod::Mdns {
820                service_name: "svc".into(),
821            },
822            3_600_000_000,
823            vec![],
824        );
825        p.add_peer(peer1)
826            .expect("test: add first dup2 peer should succeed");
827        p.add_peer(peer2)
828            .expect("test: add duplicate dup2 peer should succeed (merge)");
829        let stored = p.get_peer("dup2").expect("test: get_peer should find dup2");
830        assert_eq!(stored.addresses.len(), 1);
831    }
832
833    #[test]
834    fn test_add_peer_table_full_error() {
835        let cfg = PdpDiscoveryConfig {
836            max_peers: 2,
837            peer_ttl_us: 3_600_000_000,
838            ..PdpDiscoveryConfig::default()
839        };
840        let mut p = PeerDiscoveryProtocol::new(cfg)
841            .expect("test: valid config with max_peers=2 should succeed");
842        p.add_peer(make_peer("a", PdpDiscoveryMethod::Static("a".into())))
843            .expect("test: add peer a should succeed");
844        p.add_peer(make_peer("b", PdpDiscoveryMethod::Static("b".into())))
845            .expect("test: add peer b should succeed");
846        let err = p
847            .add_peer(make_peer("c", PdpDiscoveryMethod::Static("c".into())))
848            .expect_err("test: add peer c beyond max_peers should fail");
849        assert!(matches!(err, PdpDiscoveryError::MaxPeersExceeded));
850    }
851
852    #[test]
853    fn test_add_peer_table_full_emits_event() {
854        let cfg = PdpDiscoveryConfig {
855            max_peers: 1,
856            peer_ttl_us: 3_600_000_000,
857            ..PdpDiscoveryConfig::default()
858        };
859        let mut p = PeerDiscoveryProtocol::new(cfg)
860            .expect("test: valid config with max_peers=1 should succeed");
861        p.add_peer(make_peer("a", PdpDiscoveryMethod::Static("a".into())))
862            .expect("test: add peer a should succeed");
863        let _ = p.add_peer(make_peer("b", PdpDiscoveryMethod::Static("b".into())));
864        let events = p.drain_events();
865        // Should contain PeerDiscovered(a) + PeerTableFull
866        assert!(events
867            .iter()
868            .any(|e| matches!(e, PdpDiscoveryEvent::PeerTableFull(_))));
869    }
870
871    #[test]
872    fn test_add_peer_multiple_methods() {
873        let mut p = make_protocol();
874        p.add_peer(make_peer("p1", PdpDiscoveryMethod::Bootstrap("b".into())))
875            .expect("test: add p1 bootstrap peer should succeed");
876        p.add_peer(make_peer(
877            "p2",
878            PdpDiscoveryMethod::Mdns {
879                service_name: "s".into(),
880            },
881        ))
882        .expect("test: add p2 mdns peer should succeed");
883        p.add_peer(make_peer(
884            "p3",
885            PdpDiscoveryMethod::Dht {
886                query_key: "k".into(),
887            },
888        ))
889        .expect("test: add p3 dht peer should succeed");
890        p.add_peer(make_peer(
891            "p4",
892            PdpDiscoveryMethod::PeerExchange {
893                source_peer: "src".into(),
894            },
895        ))
896        .expect("test: add p4 peer_exchange peer should succeed");
897        p.add_peer(make_peer("p5", PdpDiscoveryMethod::Static("addr".into())))
898            .expect("test: add p5 static peer should succeed");
899        p.add_peer(make_peer(
900            "p6",
901            PdpDiscoveryMethod::Rendezvous {
902                namespace: "ns".into(),
903            },
904        ))
905        .expect("test: add p6 rendezvous peer should succeed");
906        assert_eq!(p.peer_count(), 6);
907    }
908
909    // -----------------------------------------------------------------------
910    // Remove peer
911    // -----------------------------------------------------------------------
912
913    #[test]
914    fn test_remove_peer_success() {
915        let mut p = make_protocol();
916        p.add_peer(make_peer("r1", PdpDiscoveryMethod::Static("x".into())))
917            .expect("test: add r1 peer should succeed");
918        assert!(p.remove_peer("r1").is_ok());
919        assert_eq!(p.peer_count(), 0);
920    }
921
922    #[test]
923    fn test_remove_peer_not_found() {
924        let mut p = make_protocol();
925        let err = p
926            .remove_peer("ghost")
927            .expect_err("test: remove nonexistent peer should fail");
928        assert!(matches!(err, PdpDiscoveryError::PeerNotFound(_)));
929    }
930
931    #[test]
932    fn test_remove_then_add_same_id() {
933        let mut p = make_protocol();
934        p.add_peer(make_peer("x", PdpDiscoveryMethod::Static("a".into())))
935            .expect("test: add peer x should succeed");
936        p.remove_peer("x")
937            .expect("test: remove peer x should succeed");
938        // Should be addable again.
939        p.add_peer(make_peer("x", PdpDiscoveryMethod::Static("a".into())))
940            .expect("test: re-add peer x after removal should succeed");
941        assert_eq!(p.peer_count(), 1);
942    }
943
944    // -----------------------------------------------------------------------
945    // Verify peer
946    // -----------------------------------------------------------------------
947
948    #[test]
949    fn test_verify_peer_success() {
950        let mut p = make_protocol();
951        p.add_peer(make_peer("v1", PdpDiscoveryMethod::Bootstrap("u".into())))
952            .expect("test: add v1 peer should succeed");
953        let ev = p
954            .verify_peer("v1", make_ts(1000))
955            .expect("test: verify_peer v1 should succeed");
956        assert!(matches!(ev, PdpDiscoveryEvent::PeerVerified(_)));
957        assert!(
958            p.get_peer("v1")
959                .expect("test: get_peer v1 should succeed after verify")
960                .verified
961        );
962    }
963
964    #[test]
965    fn test_verify_peer_not_found() {
966        let mut p = make_protocol();
967        let err = p
968            .verify_peer("ghost", make_ts(0))
969            .expect_err("test: verify nonexistent peer should fail");
970        assert!(matches!(err, PdpDiscoveryError::PeerNotFound(_)));
971    }
972
973    #[test]
974    fn test_verify_peer_updates_verified_at() {
975        let mut p = make_protocol();
976        p.add_peer(make_peer("v2", PdpDiscoveryMethod::Bootstrap("u".into())))
977            .expect("test: add v2 peer should succeed");
978        let ts = make_ts(500_000);
979        p.verify_peer("v2", ts)
980            .expect("test: verify_peer v2 should succeed");
981        assert_eq!(
982            p.get_peer("v2")
983                .expect("test: get_peer v2 should succeed after verify")
984                .verified_at,
985            ts
986        );
987    }
988
989    #[test]
990    fn test_verify_peer_increments_stats() {
991        let mut p = make_protocol();
992        p.add_peer(make_peer("v3", PdpDiscoveryMethod::Bootstrap("u".into())))
993            .expect("test: add v3 peer should succeed");
994        p.verify_peer("v3", make_ts(0))
995            .expect("test: first verify_peer v3 should succeed");
996        p.verify_peer("v3", make_ts(1))
997            .expect("test: second verify_peer v3 should succeed");
998        assert_eq!(p.stats().verifications_performed, 2);
999    }
1000
1001    // -----------------------------------------------------------------------
1002    // TTL expiration
1003    // -----------------------------------------------------------------------
1004
1005    #[test]
1006    fn test_expire_peers_removes_stale() {
1007        let mut p = make_protocol();
1008        let ttl = 1_000_000; // 1 second in µs
1009        let added_at = make_ts(0);
1010        let peer = PdpDiscoveredPeer::new(
1011            "exp-1",
1012            vec![],
1013            added_at,
1014            PdpDiscoveryMethod::Bootstrap("b".into()),
1015            ttl,
1016            vec![],
1017        );
1018        p.add_peer(peer)
1019            .expect("test: add exp-1 peer should succeed");
1020        // Current time is well past expiry.
1021        let evs = p.expire_peers(added_at + ttl + 1);
1022        assert_eq!(evs.len(), 1);
1023        assert!(matches!(&evs[0], PdpDiscoveryEvent::PeerExpired(id) if id == "exp-1"));
1024        assert_eq!(p.peer_count(), 0);
1025    }
1026
1027    #[test]
1028    fn test_expire_peers_keeps_live() {
1029        let mut p = make_protocol();
1030        let ttl = 1_000_000;
1031        let added_at = make_ts(0);
1032        let peer = PdpDiscoveredPeer::new(
1033            "live-1",
1034            vec![],
1035            added_at,
1036            PdpDiscoveryMethod::Static("x".into()),
1037            ttl,
1038            vec![],
1039        );
1040        p.add_peer(peer)
1041            .expect("test: add live-1 peer should succeed");
1042        // Current time is before expiry.
1043        let evs = p.expire_peers(added_at + ttl - 1);
1044        assert_eq!(evs.len(), 0);
1045        assert_eq!(p.peer_count(), 1);
1046    }
1047
1048    #[test]
1049    fn test_expire_peers_increments_expired_count() {
1050        let mut p = make_protocol();
1051        let ttl = 500;
1052        let t0 = make_ts(0);
1053        for i in 0..3u64 {
1054            let peer = PdpDiscoveredPeer::new(
1055                format!("ep-{i}"),
1056                vec![],
1057                t0,
1058                PdpDiscoveryMethod::Dht {
1059                    query_key: "k".into(),
1060                },
1061                ttl,
1062                vec![],
1063            );
1064            p.add_peer(peer)
1065                .expect("test: add ep-{i} peer should succeed");
1066        }
1067        p.expire_peers(t0 + ttl + 1);
1068        assert_eq!(p.stats().expired_peers, 3);
1069    }
1070
1071    #[test]
1072    fn test_expire_peers_emits_events_in_buffer() {
1073        let mut p = make_protocol();
1074        let ttl = 100;
1075        let t0 = make_ts(0);
1076        let peer = PdpDiscoveredPeer::new(
1077            "buf-exp",
1078            vec![],
1079            t0,
1080            PdpDiscoveryMethod::Static("x".into()),
1081            ttl,
1082            vec![],
1083        );
1084        p.add_peer(peer)
1085            .expect("test: add buf-exp peer should succeed");
1086        p.expire_peers(t0 + ttl + 1);
1087        let events = p.drain_events();
1088        assert!(events
1089            .iter()
1090            .any(|e| matches!(e, PdpDiscoveryEvent::PeerExpired(_))));
1091    }
1092
1093    #[test]
1094    fn test_expire_only_expired_not_live() {
1095        let mut p = make_protocol();
1096        let t0 = make_ts(0);
1097        let short_ttl = 1_000;
1098        let long_ttl = 3_600_000_000;
1099        let short_peer = PdpDiscoveredPeer::new(
1100            "short",
1101            vec![],
1102            t0,
1103            PdpDiscoveryMethod::Static("x".into()),
1104            short_ttl,
1105            vec![],
1106        );
1107        let long_peer = PdpDiscoveredPeer::new(
1108            "long",
1109            vec![],
1110            t0,
1111            PdpDiscoveryMethod::Static("y".into()),
1112            long_ttl,
1113            vec![],
1114        );
1115        p.add_peer(short_peer)
1116            .expect("test: add short-ttl peer should succeed");
1117        p.add_peer(long_peer)
1118            .expect("test: add long-ttl peer should succeed");
1119        let evs = p.expire_peers(t0 + short_ttl + 1);
1120        assert_eq!(evs.len(), 1);
1121        assert!(matches!(&evs[0], PdpDiscoveryEvent::PeerExpired(id) if id == "short"));
1122        assert_eq!(p.peer_count(), 1);
1123        assert!(p.get_peer("long").is_ok());
1124    }
1125
1126    // -----------------------------------------------------------------------
1127    // Refresh peer
1128    // -----------------------------------------------------------------------
1129
1130    #[test]
1131    fn test_refresh_peer_extends_ttl() {
1132        let mut p = make_protocol();
1133        let short_ttl = 100;
1134        let t0 = make_ts(0);
1135        let peer = PdpDiscoveredPeer::new(
1136            "ref-1",
1137            vec![],
1138            t0,
1139            PdpDiscoveryMethod::Bootstrap("b".into()),
1140            short_ttl,
1141            vec![],
1142        );
1143        p.add_peer(peer)
1144            .expect("test: add ref-1 peer should succeed");
1145        // Refresh before expiry.
1146        let t1 = t0 + 50;
1147        p.refresh_peer("ref-1", t1)
1148            .expect("test: refresh_peer ref-1 should succeed");
1149        // Expire at old deadline — should NOT expire because TTL was refreshed.
1150        let evs = p.expire_peers(t0 + short_ttl + 1);
1151        assert_eq!(evs.len(), 0);
1152    }
1153
1154    #[test]
1155    fn test_refresh_peer_not_found() {
1156        let mut p = make_protocol();
1157        let err = p
1158            .refresh_peer("ghost", make_ts(0))
1159            .expect_err("test: refresh nonexistent peer should fail");
1160        assert!(matches!(err, PdpDiscoveryError::PeerNotFound(_)));
1161    }
1162
1163    #[test]
1164    fn test_refresh_then_expire() {
1165        let mut p = make_protocol();
1166        let t0 = make_ts(0);
1167        let peer = PdpDiscoveredPeer::new(
1168            "re-exp",
1169            vec![],
1170            t0,
1171            PdpDiscoveryMethod::Static("x".into()),
1172            3_600_000_000,
1173            vec![],
1174        );
1175        p.add_peer(peer)
1176            .expect("test: add re-exp peer should succeed");
1177        // Refresh with tiny TTL (config default), then advance past it.
1178        p.refresh_peer("re-exp", t0)
1179            .expect("test: refresh_peer re-exp should succeed");
1180        // Default TTL is 3_600_000_000 us — advance past that.
1181        let evs = p.expire_peers(t0 + 3_600_000_001);
1182        assert_eq!(evs.len(), 1);
1183    }
1184
1185    // -----------------------------------------------------------------------
1186    // Get peer
1187    // -----------------------------------------------------------------------
1188
1189    #[test]
1190    fn test_get_peer_success() {
1191        let mut p = make_protocol();
1192        p.add_peer(make_peer("g1", PdpDiscoveryMethod::Static("a".into())))
1193            .expect("test: add g1 peer should succeed");
1194        assert!(p.get_peer("g1").is_ok());
1195    }
1196
1197    #[test]
1198    fn test_get_peer_not_found() {
1199        let p = make_protocol();
1200        assert!(matches!(
1201            p.get_peer("none"),
1202            Err(PdpDiscoveryError::PeerNotFound(_))
1203        ));
1204    }
1205
1206    // -----------------------------------------------------------------------
1207    // peers_by_method
1208    // -----------------------------------------------------------------------
1209
1210    #[test]
1211    fn test_peers_by_method_bootstrap() {
1212        let mut p = make_protocol();
1213        p.add_peer(make_peer("b1", PdpDiscoveryMethod::Bootstrap("url".into())))
1214            .expect("test: add b1 bootstrap peer should succeed");
1215        p.add_peer(make_peer(
1216            "m1",
1217            PdpDiscoveryMethod::Mdns {
1218                service_name: "s".into(),
1219            },
1220        ))
1221        .expect("test: add m1 mdns peer should succeed");
1222        let boots = p.peers_by_method("bootstrap");
1223        assert_eq!(boots.len(), 1);
1224        assert_eq!(boots[0].id, "b1");
1225    }
1226
1227    #[test]
1228    fn test_peers_by_method_mdns() {
1229        let mut p = make_protocol();
1230        p.add_peer(make_peer(
1231            "m2",
1232            PdpDiscoveryMethod::Mdns {
1233                service_name: "svc".into(),
1234            },
1235        ))
1236        .expect("test: add m2 mdns peer should succeed");
1237        p.add_peer(make_peer(
1238            "m3",
1239            PdpDiscoveryMethod::Mdns {
1240                service_name: "svc".into(),
1241            },
1242        ))
1243        .expect("test: add m3 mdns peer should succeed");
1244        assert_eq!(p.peers_by_method("mdns").len(), 2);
1245    }
1246
1247    #[test]
1248    fn test_peers_by_method_dht() {
1249        let mut p = make_protocol();
1250        p.add_peer(make_peer(
1251            "d1",
1252            PdpDiscoveryMethod::Dht {
1253                query_key: "k".into(),
1254            },
1255        ))
1256        .expect("test: add d1 dht peer should succeed");
1257        assert_eq!(p.peers_by_method("dht").len(), 1);
1258    }
1259
1260    #[test]
1261    fn test_peers_by_method_peer_exchange() {
1262        let mut p = make_protocol();
1263        p.add_peer(make_peer(
1264            "pe1",
1265            PdpDiscoveryMethod::PeerExchange {
1266                source_peer: "src".into(),
1267            },
1268        ))
1269        .expect("test: add pe1 peer_exchange peer should succeed");
1270        assert_eq!(p.peers_by_method("peer_exchange").len(), 1);
1271    }
1272
1273    #[test]
1274    fn test_peers_by_method_rendezvous() {
1275        let mut p = make_protocol();
1276        p.add_peer(make_peer(
1277            "rv1",
1278            PdpDiscoveryMethod::Rendezvous {
1279                namespace: "ns".into(),
1280            },
1281        ))
1282        .expect("test: add rv1 rendezvous peer should succeed");
1283        assert_eq!(p.peers_by_method("rendezvous").len(), 1);
1284    }
1285
1286    #[test]
1287    fn test_peers_by_method_no_match() {
1288        let mut p = make_protocol();
1289        p.add_peer(make_peer("x", PdpDiscoveryMethod::Static("a".into())))
1290            .expect("test: add x peer for method filter test should succeed");
1291        assert_eq!(p.peers_by_method("dht").len(), 0);
1292    }
1293
1294    // -----------------------------------------------------------------------
1295    // peers_with_capability
1296    // -----------------------------------------------------------------------
1297
1298    #[test]
1299    fn test_peers_with_capability_found() {
1300        let mut p = make_protocol();
1301        p.add_peer(make_peer_with_caps("c1", vec!["relay/v2", "bitswap"]))
1302            .expect("test: add c1 peer with caps should succeed");
1303        p.add_peer(make_peer_with_caps("c2", vec!["bitswap"]))
1304            .expect("test: add c2 peer with caps should succeed");
1305        let relay_peers = p.peers_with_capability("relay/v2");
1306        assert_eq!(relay_peers.len(), 1);
1307        assert_eq!(relay_peers[0].id, "c1");
1308    }
1309
1310    #[test]
1311    fn test_peers_with_capability_not_found() {
1312        let mut p = make_protocol();
1313        p.add_peer(make_peer_with_caps("c3", vec!["bitswap"]))
1314            .expect("test: add c3 peer with caps should succeed");
1315        assert_eq!(p.peers_with_capability("relay/v2").len(), 0);
1316    }
1317
1318    #[test]
1319    fn test_peers_with_capability_multiple() {
1320        let mut p = make_protocol();
1321        for i in 0..5 {
1322            p.add_peer(make_peer_with_caps(
1323                &format!("peer-cap-{i}"),
1324                vec!["dht/1.0"],
1325            ))
1326            .expect("test: add peer with dht/1.0 capability should succeed");
1327        }
1328        assert_eq!(p.peers_with_capability("dht/1.0").len(), 5);
1329    }
1330
1331    #[test]
1332    fn test_peers_with_capability_empty_table() {
1333        let p = make_protocol();
1334        assert_eq!(p.peers_with_capability("anything").len(), 0);
1335    }
1336
1337    // -----------------------------------------------------------------------
1338    // random_peers
1339    // -----------------------------------------------------------------------
1340
1341    #[test]
1342    fn test_random_peers_count() {
1343        let mut p = make_protocol();
1344        for i in 0..10u32 {
1345            p.add_peer(make_peer(
1346                &format!("rp-{i}"),
1347                PdpDiscoveryMethod::Static("a".into()),
1348            ))
1349            .expect("test: add random peer should succeed");
1350        }
1351        let sample = p.random_peers(5, 0xDEAD);
1352        assert_eq!(sample.len(), 5);
1353    }
1354
1355    #[test]
1356    fn test_random_peers_fewer_than_requested() {
1357        let mut p = make_protocol();
1358        for i in 0..3u32 {
1359            p.add_peer(make_peer(
1360                &format!("rp2-{i}"),
1361                PdpDiscoveryMethod::Static("a".into()),
1362            ))
1363            .expect("test: add rp2 peer should succeed");
1364        }
1365        let sample = p.random_peers(10, 1234);
1366        assert_eq!(sample.len(), 3);
1367    }
1368
1369    #[test]
1370    fn test_random_peers_zero_n() {
1371        let mut p = make_protocol();
1372        p.add_peer(make_peer("z", PdpDiscoveryMethod::Static("a".into())))
1373            .expect("test: add z peer for random_peers test should succeed");
1374        let sample = p.random_peers(0, 42);
1375        assert_eq!(sample.len(), 0);
1376    }
1377
1378    #[test]
1379    fn test_random_peers_no_duplicates() {
1380        let mut p = make_protocol();
1381        for i in 0..8u32 {
1382            p.add_peer(make_peer(
1383                &format!("rp3-{i}"),
1384                PdpDiscoveryMethod::Static("a".into()),
1385            ))
1386            .expect("test: add rp3 peer should succeed");
1387        }
1388        let sample = p.random_peers(8, 0xCAFE);
1389        let mut ids: Vec<&str> = sample.iter().map(|pr| pr.id.as_str()).collect();
1390        ids.sort_unstable();
1391        ids.dedup();
1392        assert_eq!(ids.len(), 8);
1393    }
1394
1395    #[test]
1396    fn test_random_peers_different_seeds_different_order() {
1397        let mut p = make_protocol();
1398        for i in 0..10u32 {
1399            p.add_peer(make_peer(
1400                &format!("seed-{i}"),
1401                PdpDiscoveryMethod::Static("a".into()),
1402            ))
1403            .expect("test: add seed peer should succeed");
1404        }
1405        let s1: Vec<String> = p.random_peers(10, 1).iter().map(|p| p.id.clone()).collect();
1406        let s2: Vec<String> = p
1407            .random_peers(10, 99999)
1408            .iter()
1409            .map(|p| p.id.clone())
1410            .collect();
1411        // With different seeds the order should differ (not guaranteed but
1412        // extremely likely with 10 distinct peers).
1413        assert_ne!(s1, s2);
1414    }
1415
1416    // -----------------------------------------------------------------------
1417    // merge_peer_table
1418    // -----------------------------------------------------------------------
1419
1420    #[test]
1421    fn test_merge_peer_table_basic() {
1422        let mut p = make_protocol();
1423        let t0 = make_ts(0);
1424        let peers = vec![
1425            make_peer("mp1", PdpDiscoveryMethod::Static("a".into())),
1426            make_peer("mp2", PdpDiscoveryMethod::Static("b".into())),
1427        ];
1428        let events = p.merge_peer_table(peers, t0);
1429        assert_eq!(events.len(), 2);
1430        assert_eq!(p.peer_count(), 2);
1431    }
1432
1433    #[test]
1434    fn test_merge_peer_table_skips_expired() {
1435        let mut p = make_protocol();
1436        let t0 = make_ts(0);
1437        let ttl = 100;
1438        let expired = PdpDiscoveredPeer::new(
1439            "old-peer",
1440            vec![],
1441            t0,
1442            PdpDiscoveryMethod::Static("x".into()),
1443            ttl,
1444            vec![],
1445        );
1446        // current_ts is past the expiry.
1447        let events = p.merge_peer_table(vec![expired], t0 + ttl + 1);
1448        assert_eq!(events.len(), 0);
1449        assert_eq!(p.peer_count(), 0);
1450    }
1451
1452    #[test]
1453    fn test_merge_peer_table_deduplicates() {
1454        let mut p = make_protocol();
1455        let t0 = make_ts(0);
1456        p.add_peer(make_peer("dup", PdpDiscoveryMethod::Static("a".into())))
1457            .expect("test: add dup peer before merge should succeed");
1458        let import = vec![make_peer("dup", PdpDiscoveryMethod::Static("a".into()))];
1459        p.merge_peer_table(import, t0);
1460        assert_eq!(p.peer_count(), 1);
1461    }
1462
1463    #[test]
1464    fn test_merge_peer_table_stops_at_capacity() {
1465        let cfg = PdpDiscoveryConfig {
1466            max_peers: 2,
1467            peer_ttl_us: 3_600_000_000,
1468            ..PdpDiscoveryConfig::default()
1469        };
1470        let mut p = PeerDiscoveryProtocol::new(cfg)
1471            .expect("test: valid config with max_peers=2 should succeed");
1472        let t0 = make_ts(0);
1473        let peers: Vec<_> = (0..5)
1474            .map(|i| make_peer(&format!("mp-{i}"), PdpDiscoveryMethod::Static("a".into())))
1475            .collect();
1476        p.merge_peer_table(peers, t0);
1477        // Should not exceed max_peers.
1478        assert!(p.peer_count() <= 2);
1479    }
1480
1481    #[test]
1482    fn test_merge_peer_table_returns_events() {
1483        let mut p = make_protocol();
1484        let t0 = make_ts(0);
1485        let peers: Vec<_> = (0..3)
1486            .map(|i| {
1487                make_peer(
1488                    &format!("evt-{i}"),
1489                    PdpDiscoveryMethod::Bootstrap("b".into()),
1490                )
1491            })
1492            .collect();
1493        let events = p.merge_peer_table(peers, t0);
1494        assert_eq!(events.len(), 3);
1495    }
1496
1497    // -----------------------------------------------------------------------
1498    // Stats
1499    // -----------------------------------------------------------------------
1500
1501    #[test]
1502    fn test_stats_total_discovered() {
1503        let mut p = make_protocol();
1504        for i in 0..3u32 {
1505            p.add_peer(make_peer(
1506                &format!("st-{i}"),
1507                PdpDiscoveryMethod::Bootstrap("b".into()),
1508            ))
1509            .expect("test: add st peer should succeed");
1510        }
1511        assert_eq!(p.stats().total_discovered, 3);
1512    }
1513
1514    #[test]
1515    fn test_stats_active_peers() {
1516        let mut p = make_protocol();
1517        for i in 0..4u32 {
1518            p.add_peer(make_peer(
1519                &format!("ap-{i}"),
1520                PdpDiscoveryMethod::Static("a".into()),
1521            ))
1522            .expect("test: add ap peer should succeed");
1523        }
1524        assert_eq!(p.stats().active_peers, 4);
1525    }
1526
1527    #[test]
1528    fn test_stats_expired_peers() {
1529        let mut p = make_protocol();
1530        let t0 = make_ts(0);
1531        let short = PdpDiscoveredPeer::new(
1532            "s",
1533            vec![],
1534            t0,
1535            PdpDiscoveryMethod::Static("x".into()),
1536            100,
1537            vec![],
1538        );
1539        p.add_peer(short)
1540            .expect("test: add short-lived peer should succeed");
1541        p.expire_peers(t0 + 200);
1542        assert_eq!(p.stats().expired_peers, 1);
1543    }
1544
1545    #[test]
1546    fn test_stats_by_method() {
1547        let mut p = make_protocol();
1548        p.add_peer(make_peer("s1", PdpDiscoveryMethod::Bootstrap("b".into())))
1549            .expect("test: add s1 bootstrap peer should succeed");
1550        p.add_peer(make_peer("s2", PdpDiscoveryMethod::Bootstrap("b".into())))
1551            .expect("test: add s2 bootstrap peer should succeed");
1552        p.add_peer(make_peer(
1553            "s3",
1554            PdpDiscoveryMethod::Mdns {
1555                service_name: "s".into(),
1556            },
1557        ))
1558        .expect("test: add s3 mdns peer should succeed");
1559        let stats = p.stats();
1560        let boot_count = stats
1561            .by_method
1562            .iter()
1563            .find(|(m, _)| m == "bootstrap")
1564            .map(|(_, c)| *c)
1565            .unwrap_or(0);
1566        let mdns_count = stats
1567            .by_method
1568            .iter()
1569            .find(|(m, _)| m == "mdns")
1570            .map(|(_, c)| *c)
1571            .unwrap_or(0);
1572        assert_eq!(boot_count, 2);
1573        assert_eq!(mdns_count, 1);
1574    }
1575
1576    #[test]
1577    fn test_stats_verifications() {
1578        let mut p = make_protocol();
1579        p.add_peer(make_peer("vv1", PdpDiscoveryMethod::Static("a".into())))
1580            .expect("test: add vv1 peer should succeed");
1581        p.add_peer(make_peer("vv2", PdpDiscoveryMethod::Static("b".into())))
1582            .expect("test: add vv2 peer should succeed");
1583        p.verify_peer("vv1", make_ts(0))
1584            .expect("test: verify_peer vv1 should succeed");
1585        p.verify_peer("vv2", make_ts(1))
1586            .expect("test: verify_peer vv2 should succeed");
1587        assert_eq!(p.stats().verifications_performed, 2);
1588    }
1589
1590    // -----------------------------------------------------------------------
1591    // Event drain
1592    // -----------------------------------------------------------------------
1593
1594    #[test]
1595    fn test_drain_events_clears_buffer() {
1596        let mut p = make_protocol();
1597        p.add_peer(make_peer("ev1", PdpDiscoveryMethod::Static("a".into())))
1598            .expect("test: add ev1 peer should succeed");
1599        let first = p.drain_events();
1600        assert!(!first.is_empty());
1601        let second = p.drain_events();
1602        assert!(second.is_empty());
1603    }
1604
1605    #[test]
1606    fn test_drain_events_accumulates() {
1607        let mut p = make_protocol();
1608        p.add_peer(make_peer("e1", PdpDiscoveryMethod::Static("a".into())))
1609            .expect("test: add e1 peer should succeed");
1610        p.add_peer(make_peer("e2", PdpDiscoveryMethod::Static("b".into())))
1611            .expect("test: add e2 peer should succeed");
1612        let evs = p.drain_events();
1613        assert_eq!(evs.len(), 2);
1614    }
1615
1616    #[test]
1617    fn test_drain_events_after_expiry() {
1618        let mut p = make_protocol();
1619        let t0 = make_ts(0);
1620        let peer = PdpDiscoveredPeer::new(
1621            "de1",
1622            vec![],
1623            t0,
1624            PdpDiscoveryMethod::Static("x".into()),
1625            100,
1626            vec![],
1627        );
1628        p.add_peer(peer).expect("test: add de1 peer should succeed");
1629        p.drain_events(); // Clear initial events.
1630        p.expire_peers(t0 + 200);
1631        let evs = p.drain_events();
1632        assert_eq!(evs.len(), 1);
1633        assert!(matches!(&evs[0], PdpDiscoveryEvent::PeerExpired(_)));
1634    }
1635
1636    // -----------------------------------------------------------------------
1637    // Error cases
1638    // -----------------------------------------------------------------------
1639
1640    #[test]
1641    fn test_error_display_peer_not_found() {
1642        let err = PdpDiscoveryError::PeerNotFound("abc".into());
1643        assert!(err.to_string().contains("abc"));
1644    }
1645
1646    #[test]
1647    fn test_error_display_max_peers() {
1648        let err = PdpDiscoveryError::MaxPeersExceeded;
1649        assert!(!err.to_string().is_empty());
1650    }
1651
1652    #[test]
1653    fn test_error_display_invalid_address() {
1654        let err = PdpDiscoveryError::InvalidAddress("bad-addr".into());
1655        assert!(err.to_string().contains("bad-addr"));
1656    }
1657
1658    #[test]
1659    fn test_error_display_duplicate_peer() {
1660        let err = PdpDiscoveryError::DuplicatePeer("dup".into());
1661        assert!(err.to_string().contains("dup"));
1662    }
1663
1664    #[test]
1665    fn test_error_display_config_error() {
1666        let err = PdpDiscoveryError::ConfigurationError("bad".into());
1667        assert!(err.to_string().contains("bad"));
1668    }
1669
1670    // -----------------------------------------------------------------------
1671    // xorshift64 PRNG
1672    // -----------------------------------------------------------------------
1673
1674    #[test]
1675    fn test_xorshift64_not_zero() {
1676        let mut state = 12345;
1677        let val = xorshift64(&mut state);
1678        assert_ne!(val, 0);
1679    }
1680
1681    #[test]
1682    fn test_xorshift64_sequence_is_different() {
1683        let mut state = 1;
1684        let v1 = xorshift64(&mut state);
1685        let v2 = xorshift64(&mut state);
1686        assert_ne!(v1, v2);
1687    }
1688
1689    #[test]
1690    fn test_xorshift64_deterministic() {
1691        let mut s1 = 42u64;
1692        let mut s2 = 42u64;
1693        for _ in 0..100 {
1694            assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1695        }
1696    }
1697
1698    // -----------------------------------------------------------------------
1699    // Discovery method variant names
1700    // -----------------------------------------------------------------------
1701
1702    #[test]
1703    fn test_variant_names() {
1704        assert_eq!(
1705            PdpDiscoveryMethod::Bootstrap("u".into()).variant_name(),
1706            "bootstrap"
1707        );
1708        assert_eq!(
1709            PdpDiscoveryMethod::Mdns {
1710                service_name: "s".into()
1711            }
1712            .variant_name(),
1713            "mdns"
1714        );
1715        assert_eq!(
1716            PdpDiscoveryMethod::Dht {
1717                query_key: "k".into()
1718            }
1719            .variant_name(),
1720            "dht"
1721        );
1722        assert_eq!(
1723            PdpDiscoveryMethod::PeerExchange {
1724                source_peer: "p".into()
1725            }
1726            .variant_name(),
1727            "peer_exchange"
1728        );
1729        assert_eq!(
1730            PdpDiscoveryMethod::Static("a".into()).variant_name(),
1731            "static"
1732        );
1733        assert_eq!(
1734            PdpDiscoveryMethod::Rendezvous {
1735                namespace: "n".into()
1736            }
1737            .variant_name(),
1738            "rendezvous"
1739        );
1740    }
1741
1742    // -----------------------------------------------------------------------
1743    // record_method_failure
1744    // -----------------------------------------------------------------------
1745
1746    #[test]
1747    fn test_record_method_failure_emits_event() {
1748        let mut p = make_protocol();
1749        p.record_method_failure("dht", "timeout");
1750        let evs = p.drain_events();
1751        assert_eq!(evs.len(), 1);
1752        assert!(matches!(
1753            &evs[0],
1754            PdpDiscoveryEvent::DiscoveryMethodFailed { method, reason }
1755            if method == "dht" && reason == "timeout"
1756        ));
1757    }
1758
1759    // -----------------------------------------------------------------------
1760    // all_peers iterator
1761    // -----------------------------------------------------------------------
1762
1763    #[test]
1764    fn test_all_peers_iterator() {
1765        let mut p = make_protocol();
1766        for i in 0..5u32 {
1767            p.add_peer(make_peer(
1768                &format!("it-{i}"),
1769                PdpDiscoveryMethod::Static("a".into()),
1770            ))
1771            .expect("test: add it peer should succeed");
1772        }
1773        assert_eq!(p.all_peers().count(), 5);
1774    }
1775
1776    // -----------------------------------------------------------------------
1777    // Type alias sanity
1778    // -----------------------------------------------------------------------
1779
1780    #[test]
1781    fn test_type_aliases_are_usable() {
1782        let _: DiscoveredPeer = PdpDiscoveredPeer::new(
1783            "alias-test",
1784            vec![],
1785            0,
1786            PdpDiscoveryMethod::Static("x".into()),
1787            1000,
1788            vec![],
1789        );
1790        let _: DiscoveryConfig = PdpDiscoveryConfig::default();
1791        let _: DiscoveryError = PdpDiscoveryError::MaxPeersExceeded;
1792    }
1793}