1use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct PexConfig {
12 pub max_peers_per_exchange: usize,
14 pub exchange_interval_ticks: u64,
16 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#[derive(Debug, Clone)]
32pub struct PexPeerRecord {
33 pub peer_id: String,
35 pub addresses: Vec<String>,
37 pub last_seen_tick: u64,
39 pub source: PeerSource,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum PeerSource {
46 Direct,
48 Exchange,
50 Bootstrap,
52}
53
54impl PeerSource {
55 fn priority(self) -> u8 {
57 match self {
58 PeerSource::Direct => 0,
59 PeerSource::Exchange => 1,
60 PeerSource::Bootstrap => 2,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct PexStats {
68 pub total_peers: usize,
70 pub direct_peers: usize,
72 pub exchange_peers: usize,
74 pub bootstrap_peers: usize,
76 pub exchanges_completed: u64,
78}
79
80pub 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 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 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 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 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
134 self.known_peers.remove(peer_id).is_some()
135 }
136
137 pub fn select_for_exchange(&self) -> Vec<&PexPeerRecord> {
143 let mut peers: Vec<&PexPeerRecord> = self.known_peers.values().collect();
144
145 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 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 self.enforce_max_peers();
173 }
174
175 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 pub fn tick(&mut self) {
184 self.current_tick += 1;
185 }
186
187 pub fn get_peer(&self, peer_id: &str) -> Option<&PexPeerRecord> {
189 self.known_peers.get(peer_id)
190 }
191
192 pub fn peer_count(&self) -> usize {
194 self.known_peers.len()
195 }
196
197 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 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 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 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 #[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); 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 #[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 #[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 #[test]
445 fn test_max_known_peers_eviction() {
446 let mut pex = small_protocol(); 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 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 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 #[test]
493 fn test_should_exchange_initially_true() {
494 let pex = default_protocol();
495 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(); 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 #[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 #[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 #[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 #[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 #[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(); 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 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 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}