1use std::collections::HashMap;
7
8#[derive(Clone, Debug, PartialEq)]
10pub enum DiscoverySource {
11 Mdns,
13 Bootstrap,
15 DhtRandomWalk,
17 PeerExchange,
19 Manual,
21}
22
23#[derive(Clone, Debug)]
25pub struct DiscoveredPeer {
26 pub peer_id: String,
28 pub addresses: Vec<String>,
30 pub source: DiscoverySource,
32 pub discovered_at_secs: u64,
34 pub dial_attempts: u32,
36 pub last_dial_result: Option<bool>,
39}
40
41impl DiscoveredPeer {
42 pub fn should_retry(&self, max_attempts: u32) -> bool {
47 self.dial_attempts < max_attempts && self.last_dial_result != Some(true)
48 }
49}
50
51#[derive(Clone, Debug, Default)]
53pub struct DiscoveryStats {
54 pub total_discovered: u64,
56 pub from_mdns: u64,
58 pub from_bootstrap: u64,
60 pub from_dht: u64,
62 pub from_peer_exchange: u64,
64 pub from_manual: u64,
66 pub duplicates_skipped: u64,
68 pub dial_successes: u64,
70 pub dial_failures: u64,
72}
73
74pub struct PeerDiscoveryManager {
76 pub peers: HashMap<String, DiscoveredPeer>,
78 pub max_dial_attempts: u32,
80 pub stats: DiscoveryStats,
82}
83
84impl PeerDiscoveryManager {
85 pub fn new(max_dial_attempts: u32) -> Self {
87 Self {
88 peers: HashMap::new(),
89 max_dial_attempts,
90 stats: DiscoveryStats::default(),
91 }
92 }
93
94 pub fn add_peer(&mut self, peer: DiscoveredPeer) -> bool {
100 if self.peers.contains_key(&peer.peer_id) {
101 self.stats.duplicates_skipped += 1;
102 return false;
103 }
104
105 match &peer.source {
107 DiscoverySource::Mdns => self.stats.from_mdns += 1,
108 DiscoverySource::Bootstrap => self.stats.from_bootstrap += 1,
109 DiscoverySource::DhtRandomWalk => self.stats.from_dht += 1,
110 DiscoverySource::PeerExchange => self.stats.from_peer_exchange += 1,
111 DiscoverySource::Manual => self.stats.from_manual += 1,
112 }
113
114 self.stats.total_discovered += 1;
115 self.peers.insert(peer.peer_id.clone(), peer);
116 true
117 }
118
119 pub fn record_dial_result(&mut self, peer_id: &str, success: bool) {
124 if let Some(peer) = self.peers.get_mut(peer_id) {
125 peer.dial_attempts += 1;
126 peer.last_dial_result = Some(success);
127 if success {
128 self.stats.dial_successes += 1;
129 } else {
130 self.stats.dial_failures += 1;
131 }
132 }
133 }
134
135 pub fn candidates_to_dial(&self) -> Vec<&DiscoveredPeer> {
139 let mut candidates: Vec<&DiscoveredPeer> = self
140 .peers
141 .values()
142 .filter(|p| p.should_retry(self.max_dial_attempts))
143 .collect();
144 candidates.sort_by_key(|p| p.dial_attempts);
145 candidates
146 }
147
148 pub fn connected_peers(&self) -> Vec<&DiscoveredPeer> {
150 self.peers
151 .values()
152 .filter(|p| p.last_dial_result == Some(true))
153 .collect()
154 }
155
156 pub fn failed_peers(&self) -> Vec<&DiscoveredPeer> {
158 self.peers
159 .values()
160 .filter(|p| {
161 p.dial_attempts >= self.max_dial_attempts && p.last_dial_result != Some(true)
162 })
163 .collect()
164 }
165
166 pub fn peers_by_source(&self, source: DiscoverySource) -> Vec<&DiscoveredPeer> {
168 self.peers.values().filter(|p| p.source == source).collect()
169 }
170
171 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
175 self.peers.remove(peer_id).is_some()
176 }
177
178 pub fn stats(&self) -> &DiscoveryStats {
180 &self.stats
181 }
182
183 pub fn merge_addresses(&mut self, peer_id: &str, new_addrs: &[String]) {
188 if let Some(peer) = self.peers.get_mut(peer_id) {
189 for addr in new_addrs {
190 if !peer.addresses.contains(addr) {
191 peer.addresses.push(addr.clone());
192 }
193 }
194 }
195 }
196}
197
198#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn make_peer(id: &str, source: DiscoverySource) -> DiscoveredPeer {
206 DiscoveredPeer {
207 peer_id: id.to_string(),
208 addresses: vec![format!("/ip4/127.0.0.1/tcp/{}", id.len())],
209 source,
210 discovered_at_secs: 1_000_000,
211 dial_attempts: 0,
212 last_dial_result: None,
213 }
214 }
215
216 #[test]
219 fn test_new_empty_state() {
220 let mgr = PeerDiscoveryManager::new(3);
221 assert_eq!(mgr.max_dial_attempts, 3);
222 assert!(mgr.peers.is_empty());
223 let s = mgr.stats();
224 assert_eq!(s.total_discovered, 0);
225 assert_eq!(s.duplicates_skipped, 0);
226 assert_eq!(s.dial_successes, 0);
227 assert_eq!(s.dial_failures, 0);
228 }
229
230 #[test]
233 fn test_add_new_peer_returns_true() {
234 let mut mgr = PeerDiscoveryManager::new(3);
235 let result = mgr.add_peer(make_peer("peer1", DiscoverySource::Mdns));
236 assert!(result);
237 assert_eq!(mgr.stats().total_discovered, 1);
238 assert_eq!(mgr.peers.len(), 1);
239 }
240
241 #[test]
244 fn test_add_duplicate_peer_returns_false() {
245 let mut mgr = PeerDiscoveryManager::new(3);
246 mgr.add_peer(make_peer("peer1", DiscoverySource::Mdns));
247 let result = mgr.add_peer(make_peer("peer1", DiscoverySource::Bootstrap));
248 assert!(!result);
249 assert_eq!(mgr.stats().duplicates_skipped, 1);
250 assert_eq!(mgr.stats().total_discovered, 1);
251 assert_eq!(mgr.peers.len(), 1);
252 }
253
254 #[test]
257 fn test_per_source_stats_mdns() {
258 let mut mgr = PeerDiscoveryManager::new(3);
259 mgr.add_peer(make_peer("p1", DiscoverySource::Mdns));
260 assert_eq!(mgr.stats().from_mdns, 1);
261 }
262
263 #[test]
266 fn test_per_source_stats_bootstrap() {
267 let mut mgr = PeerDiscoveryManager::new(3);
268 mgr.add_peer(make_peer("p2", DiscoverySource::Bootstrap));
269 assert_eq!(mgr.stats().from_bootstrap, 1);
270 }
271
272 #[test]
275 fn test_per_source_stats_dht() {
276 let mut mgr = PeerDiscoveryManager::new(3);
277 mgr.add_peer(make_peer("p3", DiscoverySource::DhtRandomWalk));
278 assert_eq!(mgr.stats().from_dht, 1);
279 }
280
281 #[test]
284 fn test_per_source_stats_peer_exchange() {
285 let mut mgr = PeerDiscoveryManager::new(3);
286 mgr.add_peer(make_peer("p4", DiscoverySource::PeerExchange));
287 assert_eq!(mgr.stats().from_peer_exchange, 1);
288 }
289
290 #[test]
293 fn test_per_source_stats_manual() {
294 let mut mgr = PeerDiscoveryManager::new(3);
295 mgr.add_peer(make_peer("p5", DiscoverySource::Manual));
296 assert_eq!(mgr.stats().from_manual, 1);
297 }
298
299 #[test]
302 fn test_record_dial_result_success() {
303 let mut mgr = PeerDiscoveryManager::new(3);
304 mgr.add_peer(make_peer("peer1", DiscoverySource::Bootstrap));
305 mgr.record_dial_result("peer1", true);
306 let peer = mgr.peers.get("peer1").expect("peer must exist");
307 assert_eq!(peer.dial_attempts, 1);
308 assert_eq!(peer.last_dial_result, Some(true));
309 assert_eq!(mgr.stats().dial_successes, 1);
310 assert_eq!(mgr.stats().dial_failures, 0);
311 }
312
313 #[test]
316 fn test_record_dial_result_failure() {
317 let mut mgr = PeerDiscoveryManager::new(3);
318 mgr.add_peer(make_peer("peer1", DiscoverySource::Bootstrap));
319 mgr.record_dial_result("peer1", false);
320 let peer = mgr.peers.get("peer1").expect("peer must exist");
321 assert_eq!(peer.dial_attempts, 1);
322 assert_eq!(peer.last_dial_result, Some(false));
323 assert_eq!(mgr.stats().dial_successes, 0);
324 assert_eq!(mgr.stats().dial_failures, 1);
325 }
326
327 #[test]
330 fn test_record_dial_result_unknown_peer_noop() {
331 let mut mgr = PeerDiscoveryManager::new(3);
332 mgr.record_dial_result("ghost", true);
333 assert_eq!(mgr.stats().dial_successes, 0);
334 assert_eq!(mgr.stats().dial_failures, 0);
335 }
336
337 #[test]
340 fn test_should_retry_under_max_attempts() {
341 let peer = DiscoveredPeer {
342 peer_id: "p".to_string(),
343 addresses: vec![],
344 source: DiscoverySource::Mdns,
345 discovered_at_secs: 0,
346 dial_attempts: 1,
347 last_dial_result: Some(false),
348 };
349 assert!(peer.should_retry(3));
350 }
351
352 #[test]
355 fn test_should_retry_at_max_attempts() {
356 let peer = DiscoveredPeer {
357 peer_id: "p".to_string(),
358 addresses: vec![],
359 source: DiscoverySource::Mdns,
360 discovered_at_secs: 0,
361 dial_attempts: 3,
362 last_dial_result: Some(false),
363 };
364 assert!(!peer.should_retry(3));
365 }
366
367 #[test]
370 fn test_should_retry_already_succeeded() {
371 let peer = DiscoveredPeer {
372 peer_id: "p".to_string(),
373 addresses: vec![],
374 source: DiscoverySource::Mdns,
375 discovered_at_secs: 0,
376 dial_attempts: 1,
377 last_dial_result: Some(true),
378 };
379 assert!(!peer.should_retry(3));
380 }
381
382 #[test]
385 fn test_candidates_to_dial_sorted_ascending() {
386 let mut mgr = PeerDiscoveryManager::new(5);
387
388 for (id, attempts) in [("pa", 2u32), ("pb", 0u32), ("pc", 1u32)] {
389 let mut p = make_peer(id, DiscoverySource::Bootstrap);
390 p.dial_attempts = attempts;
391 mgr.peers.insert(id.to_string(), p);
392 }
393
394 let candidates = mgr.candidates_to_dial();
395 assert_eq!(candidates.len(), 3);
396 let attempt_counts: Vec<u32> = candidates.iter().map(|p| p.dial_attempts).collect();
397 assert_eq!(attempt_counts, vec![0, 1, 2]);
398 }
399
400 #[test]
403 fn test_connected_peers_filtered() {
404 let mut mgr = PeerDiscoveryManager::new(3);
405 mgr.add_peer(make_peer("pa", DiscoverySource::Mdns));
406 mgr.add_peer(make_peer("pb", DiscoverySource::Mdns));
407 mgr.add_peer(make_peer("pc", DiscoverySource::Mdns));
408 mgr.record_dial_result("pa", true);
409 mgr.record_dial_result("pb", false);
410
411 let connected = mgr.connected_peers();
412 assert_eq!(connected.len(), 1);
413 assert_eq!(connected[0].peer_id, "pa");
414 }
415
416 #[test]
419 fn test_failed_peers_filtered() {
420 let mut mgr = PeerDiscoveryManager::new(2);
421 mgr.add_peer(make_peer("pa", DiscoverySource::Bootstrap));
422 mgr.add_peer(make_peer("pb", DiscoverySource::Bootstrap));
423 mgr.add_peer(make_peer("pc", DiscoverySource::Bootstrap));
424
425 mgr.record_dial_result("pa", false);
427 mgr.record_dial_result("pa", false);
428
429 mgr.record_dial_result("pb", true);
431
432 mgr.record_dial_result("pc", false);
434
435 let failed = mgr.failed_peers();
436 assert_eq!(failed.len(), 1);
437 assert_eq!(failed[0].peer_id, "pa");
438 }
439
440 #[test]
443 fn test_peers_by_source() {
444 let mut mgr = PeerDiscoveryManager::new(3);
445 mgr.add_peer(make_peer("p1", DiscoverySource::Mdns));
446 mgr.add_peer(make_peer("p2", DiscoverySource::Bootstrap));
447 mgr.add_peer(make_peer("p3", DiscoverySource::Mdns));
448 mgr.add_peer(make_peer("p4", DiscoverySource::DhtRandomWalk));
449
450 let mdns_peers = mgr.peers_by_source(DiscoverySource::Mdns);
451 assert_eq!(mdns_peers.len(), 2);
452
453 let bootstrap_peers = mgr.peers_by_source(DiscoverySource::Bootstrap);
454 assert_eq!(bootstrap_peers.len(), 1);
455
456 let dht_peers = mgr.peers_by_source(DiscoverySource::DhtRandomWalk);
457 assert_eq!(dht_peers.len(), 1);
458
459 let manual_peers = mgr.peers_by_source(DiscoverySource::Manual);
460 assert_eq!(manual_peers.len(), 0);
461 }
462
463 #[test]
466 fn test_remove_peer_returns_correct_bool() {
467 let mut mgr = PeerDiscoveryManager::new(3);
468 mgr.add_peer(make_peer("p1", DiscoverySource::Manual));
469
470 assert!(mgr.remove_peer("p1"));
471 assert!(!mgr.remove_peer("p1")); assert!(!mgr.remove_peer("ghost")); assert!(mgr.peers.is_empty());
474 }
475
476 #[test]
479 fn test_merge_addresses_deduplicates() {
480 let mut mgr = PeerDiscoveryManager::new(3);
481 let mut peer = make_peer("p1", DiscoverySource::Mdns);
482 peer.addresses = vec!["/ip4/1.2.3.4/tcp/4001".to_string()];
483 mgr.add_peer(peer);
484
485 mgr.merge_addresses(
487 "p1",
488 &[
489 "/ip4/1.2.3.4/tcp/4001".to_string(), "/ip4/5.6.7.8/tcp/4001".to_string(), ],
492 );
493 let p = mgr.peers.get("p1").expect("peer must exist");
494 assert_eq!(p.addresses.len(), 2);
495
496 mgr.merge_addresses(
498 "p1",
499 &[
500 "/ip4/1.2.3.4/tcp/4001".to_string(),
501 "/ip4/5.6.7.8/tcp/4001".to_string(),
502 ],
503 );
504 let p = mgr.peers.get("p1").expect("peer must exist");
505 assert_eq!(p.addresses.len(), 2);
506
507 mgr.merge_addresses("ghost", &["/ip4/9.9.9.9/tcp/4001".to_string()]);
509 }
510
511 #[test]
514 fn test_stats_totals_after_multiple_operations() {
515 let mut mgr = PeerDiscoveryManager::new(3);
516
517 mgr.add_peer(make_peer("p1", DiscoverySource::Mdns));
519 mgr.add_peer(make_peer("p2", DiscoverySource::Bootstrap));
520 mgr.add_peer(make_peer("p3", DiscoverySource::DhtRandomWalk));
521 mgr.add_peer(make_peer("p4", DiscoverySource::PeerExchange));
522 mgr.add_peer(make_peer("p5", DiscoverySource::Manual));
523
524 mgr.add_peer(make_peer("p1", DiscoverySource::Mdns));
526 mgr.add_peer(make_peer("p3", DiscoverySource::Bootstrap));
527
528 mgr.record_dial_result("p1", true);
530 mgr.record_dial_result("p2", false);
531 mgr.record_dial_result("p3", true);
532
533 let s = mgr.stats();
534 assert_eq!(s.total_discovered, 5);
535 assert_eq!(s.from_mdns, 1);
536 assert_eq!(s.from_bootstrap, 1);
537 assert_eq!(s.from_dht, 1);
538 assert_eq!(s.from_peer_exchange, 1);
539 assert_eq!(s.from_manual, 1);
540 assert_eq!(s.duplicates_skipped, 2);
541 assert_eq!(s.dial_successes, 2);
542 assert_eq!(s.dial_failures, 1);
543 }
544}