Skip to main content

ipfrs_network/
peer_exchange.rs

1//! Peer Exchange Protocol (PEX)
2//!
3//! Periodic peer list exchange for peer discovery without DHT.
4//! Nodes periodically share subsets of their known peers with
5//! connected peers, enabling organic network growth and resilience.
6
7use std::collections::HashMap;
8
9/// Configuration for the Peer Exchange Protocol.
10#[derive(Debug, Clone)]
11pub struct PexConfig {
12    /// Maximum number of peers to include in a single exchange.
13    pub max_peers_per_exchange: usize,
14    /// Number of ticks between exchanges.
15    pub exchange_interval_ticks: u64,
16    /// Maximum number of known peers to store.
17    pub max_known_peers: usize,
18}
19
20impl Default for PexConfig {
21    fn default() -> Self {
22        Self {
23            max_peers_per_exchange: 20,
24            exchange_interval_ticks: 50,
25            max_known_peers: 500,
26        }
27    }
28}
29
30/// Record of a known peer.
31#[derive(Debug, Clone)]
32pub struct PexPeerRecord {
33    /// The peer's identifier.
34    pub peer_id: String,
35    /// Known multiaddresses for this peer.
36    pub addresses: Vec<String>,
37    /// Tick at which this peer was last seen or updated.
38    pub last_seen_tick: u64,
39    /// How this peer was discovered.
40    pub source: PeerSource,
41}
42
43/// How a peer was discovered.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum PeerSource {
46    /// Connected directly.
47    Direct,
48    /// Received via peer exchange.
49    Exchange,
50    /// From the bootstrap list.
51    Bootstrap,
52}
53
54impl PeerSource {
55    /// Priority for selection ordering (lower = higher priority).
56    fn priority(self) -> u8 {
57        match self {
58            PeerSource::Direct => 0,
59            PeerSource::Exchange => 1,
60            PeerSource::Bootstrap => 2,
61        }
62    }
63}
64
65/// Statistics for the Peer Exchange Protocol.
66#[derive(Debug, Clone)]
67pub struct PexStats {
68    /// Total number of known peers.
69    pub total_peers: usize,
70    /// Number of directly connected peers.
71    pub direct_peers: usize,
72    /// Number of peers learned via exchange.
73    pub exchange_peers: usize,
74    /// Number of bootstrap peers.
75    pub bootstrap_peers: usize,
76    /// Number of exchanges completed.
77    pub exchanges_completed: u64,
78}
79
80/// Peer Exchange Protocol implementation.
81///
82/// Manages a set of known peers and supports periodic exchange of peer
83/// lists with connected peers for decentralised peer discovery.
84pub struct PeerExchangeProtocol {
85    config: PexConfig,
86    known_peers: HashMap<String, PexPeerRecord>,
87    current_tick: u64,
88    exchanges_completed: u64,
89    last_exchange_tick: u64,
90}
91
92impl PeerExchangeProtocol {
93    /// Create a new `PeerExchangeProtocol` with the given configuration.
94    pub fn new(config: PexConfig) -> Self {
95        Self {
96            config,
97            known_peers: HashMap::new(),
98            current_tick: 0,
99            exchanges_completed: 0,
100            last_exchange_tick: 0,
101        }
102    }
103
104    /// Add or update a known peer.
105    ///
106    /// If the peer already exists, its addresses and last-seen tick are
107    /// updated. The source is upgraded if the new source has higher
108    /// priority (Direct > Exchange > Bootstrap).
109    pub fn add_peer(&mut self, peer_id: &str, addresses: Vec<String>, source: PeerSource) {
110        if let Some(existing) = self.known_peers.get_mut(peer_id) {
111            existing.addresses = addresses;
112            existing.last_seen_tick = self.current_tick;
113            // Upgrade source if new source is higher priority
114            if source.priority() < existing.source.priority() {
115                existing.source = source;
116            }
117        } else {
118            self.known_peers.insert(
119                peer_id.to_string(),
120                PexPeerRecord {
121                    peer_id: peer_id.to_string(),
122                    addresses,
123                    last_seen_tick: self.current_tick,
124                    source,
125                },
126            );
127        }
128    }
129
130    /// Remove a peer from the known peer set.
131    ///
132    /// Returns `true` if the peer existed and was removed.
133    pub fn remove_peer(&mut self, peer_id: &str) -> bool {
134        self.known_peers.remove(peer_id).is_some()
135    }
136
137    /// Select peers for exchange.
138    ///
139    /// Returns up to `max_peers_per_exchange` peers, preferring
140    /// Direct > Exchange > Bootstrap, and within each source category
141    /// preferring the most recently seen peers.
142    pub fn select_for_exchange(&self) -> Vec<&PexPeerRecord> {
143        let mut peers: Vec<&PexPeerRecord> = self.known_peers.values().collect();
144
145        // Sort by source priority (ascending = higher priority first),
146        // then by last_seen_tick descending (most recent first).
147        peers.sort_by(|a, b| {
148            a.source
149                .priority()
150                .cmp(&b.source.priority())
151                .then_with(|| b.last_seen_tick.cmp(&a.last_seen_tick))
152        });
153
154        peers.truncate(self.config.max_peers_per_exchange);
155        peers
156    }
157
158    /// Receive a set of peers from a peer exchange.
159    ///
160    /// All received peers are added with `PeerSource::Exchange`. If the
161    /// total number of known peers exceeds `max_known_peers`, the oldest
162    /// peers (by `last_seen_tick`) are evicted.
163    pub fn receive_exchange(&mut self, peers: Vec<(String, Vec<String>)>) {
164        for (peer_id, addresses) in peers {
165            self.add_peer(&peer_id, addresses, PeerSource::Exchange);
166        }
167
168        self.exchanges_completed += 1;
169        self.last_exchange_tick = self.current_tick;
170
171        // Evict oldest peers if over capacity
172        self.enforce_max_peers();
173    }
174
175    /// Returns `true` if enough ticks have elapsed since the last
176    /// exchange to warrant a new one.
177    pub fn should_exchange(&self) -> bool {
178        self.current_tick.saturating_sub(self.last_exchange_tick)
179            >= self.config.exchange_interval_ticks
180    }
181
182    /// Advance the internal clock by one tick.
183    pub fn tick(&mut self) {
184        self.current_tick += 1;
185    }
186
187    /// Look up a peer by ID.
188    pub fn get_peer(&self, peer_id: &str) -> Option<&PexPeerRecord> {
189        self.known_peers.get(peer_id)
190    }
191
192    /// Return the number of known peers.
193    pub fn peer_count(&self) -> usize {
194        self.known_peers.len()
195    }
196
197    /// Return all peers discovered via the given source.
198    pub fn peers_by_source(&self, source: PeerSource) -> Vec<&PexPeerRecord> {
199        self.known_peers
200            .values()
201            .filter(|p| p.source == source)
202            .collect()
203    }
204
205    /// Return protocol statistics.
206    pub fn stats(&self) -> PexStats {
207        let mut direct = 0usize;
208        let mut exchange = 0usize;
209        let mut bootstrap = 0usize;
210
211        for peer in self.known_peers.values() {
212            match peer.source {
213                PeerSource::Direct => direct += 1,
214                PeerSource::Exchange => exchange += 1,
215                PeerSource::Bootstrap => bootstrap += 1,
216            }
217        }
218
219        PexStats {
220            total_peers: self.known_peers.len(),
221            direct_peers: direct,
222            exchange_peers: exchange,
223            bootstrap_peers: bootstrap,
224            exchanges_completed: self.exchanges_completed,
225        }
226    }
227
228    /// Evict oldest peers when over `max_known_peers`.
229    fn enforce_max_peers(&mut self) {
230        if self.known_peers.len() <= self.config.max_known_peers {
231            return;
232        }
233
234        let to_remove = self.known_peers.len() - self.config.max_known_peers;
235
236        // Collect (peer_id, last_seen_tick) and sort oldest first
237        let mut peers_by_age: Vec<(String, u64)> = self
238            .known_peers
239            .iter()
240            .map(|(id, rec)| (id.clone(), rec.last_seen_tick))
241            .collect();
242
243        peers_by_age.sort_by_key(|&(_, tick)| tick);
244
245        for (peer_id, _) in peers_by_age.into_iter().take(to_remove) {
246            self.known_peers.remove(&peer_id);
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    fn default_protocol() -> PeerExchangeProtocol {
256        PeerExchangeProtocol::new(PexConfig::default())
257    }
258
259    fn small_protocol() -> PeerExchangeProtocol {
260        PeerExchangeProtocol::new(PexConfig {
261            max_peers_per_exchange: 3,
262            exchange_interval_ticks: 5,
263            max_known_peers: 5,
264        })
265    }
266
267    // --- Basic add/remove ---
268
269    #[test]
270    fn test_add_peer() {
271        let mut pex = default_protocol();
272        pex.add_peer(
273            "peer1",
274            vec!["/ip4/1.2.3.4/tcp/4001".to_string()],
275            PeerSource::Direct,
276        );
277        assert_eq!(pex.peer_count(), 1);
278        let peer = pex.get_peer("peer1").expect("peer should exist");
279        assert_eq!(peer.peer_id, "peer1");
280        assert_eq!(peer.source, PeerSource::Direct);
281    }
282
283    #[test]
284    fn test_add_peer_updates_existing() {
285        let mut pex = default_protocol();
286        pex.add_peer(
287            "peer1",
288            vec!["/ip4/1.2.3.4/tcp/4001".to_string()],
289            PeerSource::Bootstrap,
290        );
291        pex.tick();
292        pex.add_peer(
293            "peer1",
294            vec!["/ip4/5.6.7.8/tcp/4001".to_string()],
295            PeerSource::Direct,
296        );
297        assert_eq!(pex.peer_count(), 1);
298        let peer = pex.get_peer("peer1").expect("peer should exist");
299        assert_eq!(peer.addresses[0], "/ip4/5.6.7.8/tcp/4001");
300        assert_eq!(peer.source, PeerSource::Direct); // upgraded
301        assert_eq!(peer.last_seen_tick, 1);
302    }
303
304    #[test]
305    fn test_add_peer_does_not_downgrade_source() {
306        let mut pex = default_protocol();
307        pex.add_peer("peer1", vec![], PeerSource::Direct);
308        pex.add_peer("peer1", vec![], PeerSource::Bootstrap);
309        let peer = pex.get_peer("peer1").expect("peer should exist");
310        assert_eq!(peer.source, PeerSource::Direct);
311    }
312
313    #[test]
314    fn test_remove_peer_exists() {
315        let mut pex = default_protocol();
316        pex.add_peer("peer1", vec![], PeerSource::Direct);
317        assert!(pex.remove_peer("peer1"));
318        assert_eq!(pex.peer_count(), 0);
319    }
320
321    #[test]
322    fn test_remove_peer_not_exists() {
323        let mut pex = default_protocol();
324        assert!(!pex.remove_peer("nonexistent"));
325    }
326
327    #[test]
328    fn test_remove_peer_idempotent() {
329        let mut pex = default_protocol();
330        pex.add_peer("peer1", vec![], PeerSource::Direct);
331        assert!(pex.remove_peer("peer1"));
332        assert!(!pex.remove_peer("peer1"));
333    }
334
335    // --- select_for_exchange ---
336
337    #[test]
338    fn test_select_for_exchange_empty() {
339        let pex = default_protocol();
340        assert!(pex.select_for_exchange().is_empty());
341    }
342
343    #[test]
344    fn test_select_for_exchange_respects_max() {
345        let mut pex = small_protocol();
346        for i in 0..10 {
347            pex.add_peer(&format!("peer{i}"), vec![], PeerSource::Direct);
348        }
349        assert_eq!(pex.select_for_exchange().len(), 3);
350    }
351
352    #[test]
353    fn test_select_for_exchange_priority_ordering() {
354        let mut pex = small_protocol();
355        pex.add_peer("bootstrap1", vec![], PeerSource::Bootstrap);
356        pex.add_peer("exchange1", vec![], PeerSource::Exchange);
357        pex.add_peer("direct1", vec![], PeerSource::Direct);
358
359        let selected = pex.select_for_exchange();
360        assert_eq!(selected.len(), 3);
361        assert_eq!(selected[0].source, PeerSource::Direct);
362        assert_eq!(selected[1].source, PeerSource::Exchange);
363        assert_eq!(selected[2].source, PeerSource::Bootstrap);
364    }
365
366    #[test]
367    fn test_select_for_exchange_recency_within_source() {
368        let mut pex = PeerExchangeProtocol::new(PexConfig {
369            max_peers_per_exchange: 5,
370            exchange_interval_ticks: 50,
371            max_known_peers: 500,
372        });
373
374        pex.add_peer("direct_old", vec![], PeerSource::Direct);
375        pex.tick();
376        pex.tick();
377        pex.add_peer("direct_new", vec![], PeerSource::Direct);
378
379        let selected = pex.select_for_exchange();
380        assert_eq!(selected[0].peer_id, "direct_new");
381        assert_eq!(selected[1].peer_id, "direct_old");
382    }
383
384    #[test]
385    fn test_select_for_exchange_direct_before_exchange_before_bootstrap() {
386        let mut pex = PeerExchangeProtocol::new(PexConfig {
387            max_peers_per_exchange: 2,
388            exchange_interval_ticks: 50,
389            max_known_peers: 500,
390        });
391
392        pex.add_peer("bootstrap1", vec![], PeerSource::Bootstrap);
393        pex.add_peer("exchange1", vec![], PeerSource::Exchange);
394        pex.add_peer("direct1", vec![], PeerSource::Direct);
395
396        let selected = pex.select_for_exchange();
397        assert_eq!(selected.len(), 2);
398        assert_eq!(selected[0].source, PeerSource::Direct);
399        assert_eq!(selected[1].source, PeerSource::Exchange);
400    }
401
402    // --- receive_exchange ---
403
404    #[test]
405    fn test_receive_exchange_adds_peers() {
406        let mut pex = default_protocol();
407        pex.receive_exchange(vec![
408            (
409                "peer1".to_string(),
410                vec!["/ip4/1.2.3.4/tcp/4001".to_string()],
411            ),
412            (
413                "peer2".to_string(),
414                vec!["/ip4/5.6.7.8/tcp/4001".to_string()],
415            ),
416        ]);
417        assert_eq!(pex.peer_count(), 2);
418        let peer = pex.get_peer("peer1").expect("peer should exist");
419        assert_eq!(peer.source, PeerSource::Exchange);
420    }
421
422    #[test]
423    fn test_receive_exchange_increments_counter() {
424        let mut pex = default_protocol();
425        pex.receive_exchange(vec![]);
426        pex.receive_exchange(vec![]);
427        assert_eq!(pex.stats().exchanges_completed, 2);
428    }
429
430    #[test]
431    fn test_receive_exchange_does_not_downgrade_direct() {
432        let mut pex = default_protocol();
433        pex.add_peer("peer1", vec![], PeerSource::Direct);
434        pex.receive_exchange(vec![(
435            "peer1".to_string(),
436            vec!["/ip4/9.9.9.9/tcp/4001".to_string()],
437        )]);
438        let peer = pex.get_peer("peer1").expect("peer should exist");
439        assert_eq!(peer.source, PeerSource::Direct);
440    }
441
442    // --- max_known_peers eviction ---
443
444    #[test]
445    fn test_max_known_peers_eviction() {
446        let mut pex = small_protocol(); // max 5
447        for i in 0..5 {
448            pex.add_peer(&format!("peer{i}"), vec![], PeerSource::Direct);
449            pex.tick();
450        }
451        assert_eq!(pex.peer_count(), 5);
452
453        // Receive 3 more peers, should evict oldest to stay at 5
454        pex.receive_exchange(vec![
455            ("new1".to_string(), vec![]),
456            ("new2".to_string(), vec![]),
457            ("new3".to_string(), vec![]),
458        ]);
459        assert_eq!(pex.peer_count(), 5);
460        // Oldest peers (peer0, peer1, peer2) should be evicted
461        assert!(pex.get_peer("peer0").is_none());
462        assert!(pex.get_peer("peer1").is_none());
463        assert!(pex.get_peer("peer2").is_none());
464    }
465
466    #[test]
467    fn test_eviction_preserves_newest() {
468        let mut pex = PeerExchangeProtocol::new(PexConfig {
469            max_peers_per_exchange: 20,
470            exchange_interval_ticks: 50,
471            max_known_peers: 3,
472        });
473
474        pex.add_peer("old", vec![], PeerSource::Direct);
475        pex.tick();
476        pex.add_peer("mid", vec![], PeerSource::Direct);
477        pex.tick();
478        pex.add_peer("new", vec![], PeerSource::Direct);
479        pex.tick();
480
481        pex.receive_exchange(vec![("newest".to_string(), vec![])]);
482
483        assert_eq!(pex.peer_count(), 3);
484        assert!(pex.get_peer("old").is_none());
485        assert!(pex.get_peer("mid").is_some());
486        assert!(pex.get_peer("new").is_some());
487        assert!(pex.get_peer("newest").is_some());
488    }
489
490    // --- should_exchange / tick ---
491
492    #[test]
493    fn test_should_exchange_initially_true() {
494        let pex = default_protocol();
495        // At tick 0, last_exchange_tick 0, interval 50 => 0 >= 50 is false
496        // Actually 0 - 0 = 0 < 50, so false
497        assert!(!pex.should_exchange());
498    }
499
500    #[test]
501    fn test_should_exchange_after_interval() {
502        let mut pex = PeerExchangeProtocol::new(PexConfig {
503            max_peers_per_exchange: 20,
504            exchange_interval_ticks: 5,
505            max_known_peers: 500,
506        });
507
508        for _ in 0..4 {
509            pex.tick();
510            assert!(!pex.should_exchange());
511        }
512        pex.tick(); // tick 5
513        assert!(pex.should_exchange());
514    }
515
516    #[test]
517    fn test_should_exchange_resets_after_receive() {
518        let mut pex = PeerExchangeProtocol::new(PexConfig {
519            max_peers_per_exchange: 20,
520            exchange_interval_ticks: 3,
521            max_known_peers: 500,
522        });
523
524        for _ in 0..3 {
525            pex.tick();
526        }
527        assert!(pex.should_exchange());
528
529        pex.receive_exchange(vec![]);
530        assert!(!pex.should_exchange());
531    }
532
533    #[test]
534    fn test_tick_advances_clock() {
535        let mut pex = default_protocol();
536        pex.tick();
537        pex.tick();
538        pex.add_peer("peer1", vec![], PeerSource::Direct);
539        let peer = pex.get_peer("peer1").expect("peer should exist");
540        assert_eq!(peer.last_seen_tick, 2);
541    }
542
543    // --- peers_by_source ---
544
545    #[test]
546    fn test_peers_by_source_direct() {
547        let mut pex = default_protocol();
548        pex.add_peer("d1", vec![], PeerSource::Direct);
549        pex.add_peer("d2", vec![], PeerSource::Direct);
550        pex.add_peer("e1", vec![], PeerSource::Exchange);
551        pex.add_peer("b1", vec![], PeerSource::Bootstrap);
552
553        let direct = pex.peers_by_source(PeerSource::Direct);
554        assert_eq!(direct.len(), 2);
555        for p in &direct {
556            assert_eq!(p.source, PeerSource::Direct);
557        }
558    }
559
560    #[test]
561    fn test_peers_by_source_exchange() {
562        let mut pex = default_protocol();
563        pex.add_peer("e1", vec![], PeerSource::Exchange);
564        pex.add_peer("d1", vec![], PeerSource::Direct);
565        let exchange = pex.peers_by_source(PeerSource::Exchange);
566        assert_eq!(exchange.len(), 1);
567    }
568
569    #[test]
570    fn test_peers_by_source_bootstrap() {
571        let mut pex = default_protocol();
572        pex.add_peer("b1", vec![], PeerSource::Bootstrap);
573        pex.add_peer("b2", vec![], PeerSource::Bootstrap);
574        pex.add_peer("b3", vec![], PeerSource::Bootstrap);
575        let bootstrap = pex.peers_by_source(PeerSource::Bootstrap);
576        assert_eq!(bootstrap.len(), 3);
577    }
578
579    #[test]
580    fn test_peers_by_source_empty() {
581        let pex = default_protocol();
582        assert!(pex.peers_by_source(PeerSource::Direct).is_empty());
583    }
584
585    // --- stats ---
586
587    #[test]
588    fn test_stats_empty() {
589        let pex = default_protocol();
590        let s = pex.stats();
591        assert_eq!(s.total_peers, 0);
592        assert_eq!(s.direct_peers, 0);
593        assert_eq!(s.exchange_peers, 0);
594        assert_eq!(s.bootstrap_peers, 0);
595        assert_eq!(s.exchanges_completed, 0);
596    }
597
598    #[test]
599    fn test_stats_accuracy() {
600        let mut pex = default_protocol();
601        pex.add_peer("d1", vec![], PeerSource::Direct);
602        pex.add_peer("d2", vec![], PeerSource::Direct);
603        pex.add_peer("e1", vec![], PeerSource::Exchange);
604        pex.add_peer("b1", vec![], PeerSource::Bootstrap);
605        pex.add_peer("b2", vec![], PeerSource::Bootstrap);
606        pex.add_peer("b3", vec![], PeerSource::Bootstrap);
607        pex.receive_exchange(vec![("e2".to_string(), vec![])]);
608
609        let s = pex.stats();
610        assert_eq!(s.total_peers, 7);
611        assert_eq!(s.direct_peers, 2);
612        assert_eq!(s.exchange_peers, 2);
613        assert_eq!(s.bootstrap_peers, 3);
614        assert_eq!(s.exchanges_completed, 1);
615    }
616
617    // --- get_peer ---
618
619    #[test]
620    fn test_get_peer_not_found() {
621        let pex = default_protocol();
622        assert!(pex.get_peer("nonexistent").is_none());
623    }
624
625    #[test]
626    fn test_get_peer_after_remove() {
627        let mut pex = default_protocol();
628        pex.add_peer("peer1", vec![], PeerSource::Direct);
629        pex.remove_peer("peer1");
630        assert!(pex.get_peer("peer1").is_none());
631    }
632
633    // --- peer_count ---
634
635    #[test]
636    fn test_peer_count_empty() {
637        let pex = default_protocol();
638        assert_eq!(pex.peer_count(), 0);
639    }
640
641    #[test]
642    fn test_peer_count_after_adds_and_removes() {
643        let mut pex = default_protocol();
644        pex.add_peer("p1", vec![], PeerSource::Direct);
645        pex.add_peer("p2", vec![], PeerSource::Exchange);
646        pex.add_peer("p3", vec![], PeerSource::Bootstrap);
647        assert_eq!(pex.peer_count(), 3);
648        pex.remove_peer("p2");
649        assert_eq!(pex.peer_count(), 2);
650    }
651
652    // --- Edge cases ---
653
654    #[test]
655    fn test_add_peer_with_multiple_addresses() {
656        let mut pex = default_protocol();
657        pex.add_peer(
658            "peer1",
659            vec![
660                "/ip4/1.2.3.4/tcp/4001".to_string(),
661                "/ip6/::1/tcp/4001".to_string(),
662                "/ip4/10.0.0.1/udp/4001/quic-v1".to_string(),
663            ],
664            PeerSource::Direct,
665        );
666        let peer = pex.get_peer("peer1").expect("peer should exist");
667        assert_eq!(peer.addresses.len(), 3);
668    }
669
670    #[test]
671    fn test_add_peer_empty_addresses() {
672        let mut pex = default_protocol();
673        pex.add_peer("peer1", vec![], PeerSource::Direct);
674        let peer = pex.get_peer("peer1").expect("peer should exist");
675        assert!(peer.addresses.is_empty());
676    }
677
678    #[test]
679    fn test_large_exchange() {
680        let mut pex = default_protocol();
681        let peers: Vec<(String, Vec<String>)> = (0..100)
682            .map(|i| {
683                (
684                    format!("peer{i}"),
685                    vec![format!("/ip4/10.0.0.{}/tcp/4001", i % 256)],
686                )
687            })
688            .collect();
689        pex.receive_exchange(peers);
690        assert_eq!(pex.peer_count(), 100);
691    }
692
693    #[test]
694    fn test_config_default_values() {
695        let config = PexConfig::default();
696        assert_eq!(config.max_peers_per_exchange, 20);
697        assert_eq!(config.exchange_interval_ticks, 50);
698        assert_eq!(config.max_known_peers, 500);
699    }
700
701    #[test]
702    fn test_select_all_when_fewer_than_max() {
703        let mut pex = default_protocol(); // max 20
704        pex.add_peer("p1", vec![], PeerSource::Direct);
705        pex.add_peer("p2", vec![], PeerSource::Exchange);
706        let selected = pex.select_for_exchange();
707        assert_eq!(selected.len(), 2);
708    }
709
710    #[test]
711    fn test_multiple_ticks_and_exchanges() {
712        let mut pex = PeerExchangeProtocol::new(PexConfig {
713            max_peers_per_exchange: 5,
714            exchange_interval_ticks: 3,
715            max_known_peers: 100,
716        });
717
718        // First exchange cycle
719        for _ in 0..3 {
720            pex.tick();
721        }
722        assert!(pex.should_exchange());
723        pex.receive_exchange(vec![("a".to_string(), vec![])]);
724        assert!(!pex.should_exchange());
725
726        // Second exchange cycle
727        for _ in 0..3 {
728            pex.tick();
729        }
730        assert!(pex.should_exchange());
731        pex.receive_exchange(vec![("b".to_string(), vec![])]);
732
733        assert_eq!(pex.stats().exchanges_completed, 2);
734        assert_eq!(pex.peer_count(), 2);
735    }
736}