1use std::collections::HashMap;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub enum DiscoverySource {
11 Dht,
13 Mdns,
15 Bootstrap,
17 Relay,
19 Manual,
21}
22
23#[derive(Clone, Debug)]
25pub struct PeerRecord {
26 pub peer_id: String,
28 pub addresses: Vec<String>,
30 pub source: DiscoverySource,
32 pub discovered_at_secs: u64,
34 pub last_seen_secs: u64,
36 pub successful_connections: u32,
38 pub failed_connections: u32,
40}
41
42impl PeerRecord {
43 pub fn reliability_score(&self) -> f64 {
48 let total = self.successful_connections + self.failed_connections;
49 if total == 0 {
50 0.5
51 } else {
52 self.successful_connections as f64 / total as f64
53 }
54 }
55}
56
57#[derive(Clone, Debug)]
59pub struct DiscoveryCacheStats {
60 pub total_peers: usize,
62 pub by_source: HashMap<DiscoverySource, usize>,
64 pub avg_reliability: f64,
66}
67
68pub struct PeerDiscoveryCache {
70 pub records: HashMap<String, PeerRecord>,
72 pub max_size: usize,
74 pub ttl_secs: u64,
76}
77
78impl PeerDiscoveryCache {
79 pub fn new(max_size: usize, ttl_secs: u64) -> Self {
81 Self {
82 records: HashMap::new(),
83 max_size,
84 ttl_secs,
85 }
86 }
87
88 pub fn upsert(&mut self, record: PeerRecord) {
96 if let Some(existing) = self.records.get_mut(&record.peer_id) {
97 for addr in &record.addresses {
99 if !existing.addresses.contains(addr) {
100 existing.addresses.push(addr.clone());
101 }
102 }
103 if record.last_seen_secs > existing.last_seen_secs {
105 existing.last_seen_secs = record.last_seen_secs;
106 }
107 existing.source = record.source;
109 return;
110 }
111
112 if self.records.len() >= self.max_size {
114 self.evict_lowest_reliability();
115 }
116
117 self.records.insert(record.peer_id.clone(), record);
118 }
119
120 fn evict_lowest_reliability(&mut self) {
123 let victim = self
124 .records
125 .iter()
126 .min_by(|a, b| {
127 let score_a = a.1.reliability_score();
128 let score_b = b.1.reliability_score();
129 score_a
130 .partial_cmp(&score_b)
131 .unwrap_or(std::cmp::Ordering::Equal)
132 .then_with(|| a.1.last_seen_secs.cmp(&b.1.last_seen_secs))
133 })
134 .map(|(k, _)| k.clone());
135
136 if let Some(key) = victim {
137 self.records.remove(&key);
138 }
139 }
140
141 pub fn record_success(&mut self, peer_id: &str, now_secs: u64) -> bool {
146 match self.records.get_mut(peer_id) {
147 Some(record) => {
148 record.successful_connections += 1;
149 record.last_seen_secs = now_secs;
150 true
151 }
152 None => false,
153 }
154 }
155
156 pub fn record_failure(&mut self, peer_id: &str) -> bool {
161 match self.records.get_mut(peer_id) {
162 Some(record) => {
163 record.failed_connections += 1;
164 true
165 }
166 None => false,
167 }
168 }
169
170 pub fn evict_stale(&mut self, now_secs: u64) {
172 let ttl = self.ttl_secs;
173 self.records
174 .retain(|_, record| record.last_seen_secs + ttl >= now_secs);
175 }
176
177 pub fn get(&self, peer_id: &str) -> Option<&PeerRecord> {
179 self.records.get(peer_id)
180 }
181
182 pub fn peers_by_reliability(&self) -> Vec<&PeerRecord> {
184 let mut peers: Vec<&PeerRecord> = self.records.values().collect();
185 peers.sort_by(|a, b| {
186 b.reliability_score()
187 .partial_cmp(&a.reliability_score())
188 .unwrap_or(std::cmp::Ordering::Equal)
189 });
190 peers
191 }
192
193 pub fn peers_from_source(&self, source: DiscoverySource) -> Vec<&PeerRecord> {
195 let mut peers: Vec<&PeerRecord> = self
196 .records
197 .values()
198 .filter(|r| r.source == source)
199 .collect();
200 peers.sort_by_key(|b| std::cmp::Reverse(b.last_seen_secs));
201 peers
202 }
203
204 pub fn remove(&mut self, peer_id: &str) -> bool {
208 self.records.remove(peer_id).is_some()
209 }
210
211 pub fn stats(&self) -> DiscoveryCacheStats {
213 let total_peers = self.records.len();
214
215 let mut by_source: HashMap<DiscoverySource, usize> = HashMap::new();
216 let mut reliability_sum = 0.0_f64;
217
218 for record in self.records.values() {
219 *by_source.entry(record.source).or_insert(0) += 1;
220 reliability_sum += record.reliability_score();
221 }
222
223 let avg_reliability = if total_peers == 0 {
224 0.0
225 } else {
226 reliability_sum / total_peers as f64
227 };
228
229 DiscoveryCacheStats {
230 total_peers,
231 by_source,
232 avg_reliability,
233 }
234 }
235}
236
237#[cfg(test)]
242mod tests {
243 use super::*;
244
245 fn make_record(
246 peer_id: &str,
247 source: DiscoverySource,
248 last_seen_secs: u64,
249 successful: u32,
250 failed: u32,
251 ) -> PeerRecord {
252 PeerRecord {
253 peer_id: peer_id.to_string(),
254 addresses: vec![format!("/ip4/127.0.0.1/tcp/{}", peer_id.len())],
255 source,
256 discovered_at_secs: 1000,
257 last_seen_secs,
258 successful_connections: successful,
259 failed_connections: failed,
260 }
261 }
262
263 #[test]
265 fn test_new_starts_empty() {
266 let cache = PeerDiscoveryCache::new(10, 300);
267 assert_eq!(cache.records.len(), 0);
268 assert_eq!(cache.max_size, 10);
269 assert_eq!(cache.ttl_secs, 300);
270 }
271
272 #[test]
274 fn test_upsert_adds_new_record() {
275 let mut cache = PeerDiscoveryCache::new(10, 300);
276 let rec = make_record("peer1", DiscoverySource::Dht, 2000, 0, 0);
277 cache.upsert(rec);
278 assert!(cache.records.contains_key("peer1"));
279 }
280
281 #[test]
283 fn test_upsert_updates_existing_merge_addresses() {
284 let mut cache = PeerDiscoveryCache::new(10, 300);
285 let rec1 = PeerRecord {
286 peer_id: "peer1".to_string(),
287 addresses: vec!["/ip4/1.2.3.4/tcp/4001".to_string()],
288 source: DiscoverySource::Dht,
289 discovered_at_secs: 1000,
290 last_seen_secs: 2000,
291 successful_connections: 0,
292 failed_connections: 0,
293 };
294 cache.upsert(rec1);
295
296 let rec2 = PeerRecord {
297 peer_id: "peer1".to_string(),
298 addresses: vec![
299 "/ip4/1.2.3.4/tcp/4001".to_string(),
300 "/ip4/5.6.7.8/tcp/4002".to_string(),
301 ],
302 source: DiscoverySource::Mdns,
303 discovered_at_secs: 1500,
304 last_seen_secs: 3000,
305 successful_connections: 0,
306 failed_connections: 0,
307 };
308 cache.upsert(rec2);
309
310 let record = cache
311 .records
312 .get("peer1")
313 .expect("test: peer1 should exist in cache after upsert");
314 assert_eq!(record.addresses.len(), 2);
315 assert_eq!(record.last_seen_secs, 3000);
316 assert_eq!(record.source, DiscoverySource::Mdns);
317 }
318
319 #[test]
321 fn test_upsert_at_capacity_evicts_lowest_reliability() {
322 let mut cache = PeerDiscoveryCache::new(2, 300);
323 cache.upsert(make_record("peer_a", DiscoverySource::Dht, 2000, 1, 0));
325 cache.upsert(make_record("peer_b", DiscoverySource::Dht, 2000, 0, 1));
327 cache.upsert(make_record("peer_c", DiscoverySource::Dht, 2000, 1, 1));
329
330 assert!(cache.records.contains_key("peer_a"));
331 assert!(!cache.records.contains_key("peer_b"));
332 assert!(cache.records.contains_key("peer_c"));
333 }
334
335 #[test]
337 fn test_upsert_at_capacity_tie_broken_by_oldest_last_seen() {
338 let mut cache = PeerDiscoveryCache::new(2, 300);
339 cache.upsert(make_record("peer_a", DiscoverySource::Dht, 1000, 0, 0));
342 cache.upsert(make_record("peer_b", DiscoverySource::Dht, 2000, 0, 0));
344 cache.upsert(make_record("peer_c", DiscoverySource::Dht, 3000, 0, 0));
346
347 assert!(
348 !cache.records.contains_key("peer_a"),
349 "peer_a should have been evicted"
350 );
351 assert!(cache.records.contains_key("peer_b"));
352 assert!(cache.records.contains_key("peer_c"));
353 }
354
355 #[test]
357 fn test_record_success_increments_counter() {
358 let mut cache = PeerDiscoveryCache::new(10, 300);
359 cache.upsert(make_record("peer1", DiscoverySource::Dht, 2000, 0, 0));
360 assert!(cache.record_success("peer1", 3000));
361 assert_eq!(cache.records["peer1"].successful_connections, 1);
362 }
363
364 #[test]
366 fn test_record_success_updates_last_seen() {
367 let mut cache = PeerDiscoveryCache::new(10, 300);
368 cache.upsert(make_record("peer1", DiscoverySource::Dht, 2000, 0, 0));
369 cache.record_success("peer1", 5000);
370 assert_eq!(cache.records["peer1"].last_seen_secs, 5000);
371 }
372
373 #[test]
375 fn test_record_success_false_if_unknown() {
376 let mut cache = PeerDiscoveryCache::new(10, 300);
377 assert!(!cache.record_success("unknown_peer", 5000));
378 }
379
380 #[test]
382 fn test_record_failure_increments_counter() {
383 let mut cache = PeerDiscoveryCache::new(10, 300);
384 cache.upsert(make_record("peer1", DiscoverySource::Dht, 2000, 0, 0));
385 assert!(cache.record_failure("peer1"));
386 assert_eq!(cache.records["peer1"].failed_connections, 1);
387 }
388
389 #[test]
391 fn test_record_failure_false_if_unknown() {
392 let mut cache = PeerDiscoveryCache::new(10, 300);
393 assert!(!cache.record_failure("unknown_peer"));
394 }
395
396 #[test]
398 fn test_reliability_score_no_connections() {
399 let rec = make_record("peer1", DiscoverySource::Dht, 2000, 0, 0);
400 assert!((rec.reliability_score() - 0.5).abs() < f64::EPSILON);
401 }
402
403 #[test]
405 fn test_reliability_score_computed_correctly() {
406 let rec = make_record("peer1", DiscoverySource::Dht, 2000, 3, 1);
407 assert!((rec.reliability_score() - 0.75).abs() < f64::EPSILON);
408
409 let rec2 = make_record("peer2", DiscoverySource::Dht, 2000, 0, 4);
410 assert!(rec2.reliability_score().abs() < f64::EPSILON);
411
412 let rec3 = make_record("peer3", DiscoverySource::Dht, 2000, 5, 0);
413 assert!((rec3.reliability_score() - 1.0).abs() < f64::EPSILON);
414 }
415
416 #[test]
418 fn test_evict_stale_removes_old() {
419 let mut cache = PeerDiscoveryCache::new(10, 100);
420 cache.upsert(make_record("stale_peer", DiscoverySource::Dht, 500, 0, 0));
422 cache.evict_stale(700);
423 assert!(!cache.records.contains_key("stale_peer"));
424 }
425
426 #[test]
428 fn test_evict_stale_keeps_fresh() {
429 let mut cache = PeerDiscoveryCache::new(10, 100);
430 cache.upsert(make_record("fresh_peer", DiscoverySource::Dht, 650, 0, 0));
432 cache.evict_stale(700);
433 assert!(cache.records.contains_key("fresh_peer"));
434 }
435
436 #[test]
438 fn test_peers_by_reliability_sorted_desc() {
439 let mut cache = PeerDiscoveryCache::new(10, 300);
440 cache.upsert(make_record("low", DiscoverySource::Dht, 2000, 1, 9)); cache.upsert(make_record("mid", DiscoverySource::Dht, 2000, 5, 5)); cache.upsert(make_record("high", DiscoverySource::Dht, 2000, 9, 1)); let sorted = cache.peers_by_reliability();
445 assert_eq!(sorted.len(), 3);
446 assert!(sorted[0].reliability_score() >= sorted[1].reliability_score());
447 assert!(sorted[1].reliability_score() >= sorted[2].reliability_score());
448 assert_eq!(sorted[0].peer_id, "high");
449 assert_eq!(sorted[2].peer_id, "low");
450 }
451
452 #[test]
454 fn test_peers_from_source_filters_correctly() {
455 let mut cache = PeerDiscoveryCache::new(10, 300);
456 cache.upsert(make_record("dht1", DiscoverySource::Dht, 2000, 0, 0));
457 cache.upsert(make_record("dht2", DiscoverySource::Dht, 2001, 0, 0));
458 cache.upsert(make_record("mdns1", DiscoverySource::Mdns, 2002, 0, 0));
459
460 let dht_peers = cache.peers_from_source(DiscoverySource::Dht);
461 assert_eq!(dht_peers.len(), 2);
462 for p in &dht_peers {
463 assert_eq!(p.source, DiscoverySource::Dht);
464 }
465
466 let mdns_peers = cache.peers_from_source(DiscoverySource::Mdns);
467 assert_eq!(mdns_peers.len(), 1);
468 assert_eq!(mdns_peers[0].peer_id, "mdns1");
469 }
470
471 #[test]
473 fn test_peers_from_source_sorted_by_last_seen_desc() {
474 let mut cache = PeerDiscoveryCache::new(10, 300);
475 cache.upsert(make_record("a", DiscoverySource::Bootstrap, 1000, 0, 0));
476 cache.upsert(make_record("b", DiscoverySource::Bootstrap, 3000, 0, 0));
477 cache.upsert(make_record("c", DiscoverySource::Bootstrap, 2000, 0, 0));
478
479 let peers = cache.peers_from_source(DiscoverySource::Bootstrap);
480 assert_eq!(peers.len(), 3);
481 assert_eq!(peers[0].last_seen_secs, 3000);
482 assert_eq!(peers[1].last_seen_secs, 2000);
483 assert_eq!(peers[2].last_seen_secs, 1000);
484 }
485
486 #[test]
488 fn test_remove_returns_true_when_present() {
489 let mut cache = PeerDiscoveryCache::new(10, 300);
490 cache.upsert(make_record("peer1", DiscoverySource::Dht, 2000, 0, 0));
491 assert!(cache.remove("peer1"));
492 assert!(!cache.records.contains_key("peer1"));
493 }
494
495 #[test]
497 fn test_remove_returns_false_when_absent() {
498 let mut cache = PeerDiscoveryCache::new(10, 300);
499 assert!(!cache.remove("nonexistent"));
500 }
501
502 #[test]
504 fn test_get_returns_some() {
505 let mut cache = PeerDiscoveryCache::new(10, 300);
506 cache.upsert(make_record("peer1", DiscoverySource::Relay, 2000, 2, 1));
507 let rec = cache.get("peer1");
508 assert!(rec.is_some());
509 assert_eq!(
510 rec.expect("test: peer1 record should be found by get()")
511 .peer_id,
512 "peer1"
513 );
514 }
515
516 #[test]
518 fn test_get_returns_none() {
519 let cache = PeerDiscoveryCache::new(10, 300);
520 assert!(cache.get("nobody").is_none());
521 }
522
523 #[test]
525 fn test_stats_total_peers() {
526 let mut cache = PeerDiscoveryCache::new(10, 300);
527 assert_eq!(cache.stats().total_peers, 0);
528 cache.upsert(make_record("p1", DiscoverySource::Dht, 2000, 0, 0));
529 cache.upsert(make_record("p2", DiscoverySource::Dht, 2000, 0, 0));
530 assert_eq!(cache.stats().total_peers, 2);
531 }
532
533 #[test]
535 fn test_stats_by_source_counts() {
536 let mut cache = PeerDiscoveryCache::new(10, 300);
537 cache.upsert(make_record("d1", DiscoverySource::Dht, 2000, 0, 0));
538 cache.upsert(make_record("d2", DiscoverySource::Dht, 2000, 0, 0));
539 cache.upsert(make_record("m1", DiscoverySource::Mdns, 2000, 0, 0));
540 cache.upsert(make_record("b1", DiscoverySource::Bootstrap, 2000, 0, 0));
541
542 let stats = cache.stats();
543 assert_eq!(*stats.by_source.get(&DiscoverySource::Dht).unwrap_or(&0), 2);
544 assert_eq!(
545 *stats.by_source.get(&DiscoverySource::Mdns).unwrap_or(&0),
546 1
547 );
548 assert_eq!(
549 *stats
550 .by_source
551 .get(&DiscoverySource::Bootstrap)
552 .unwrap_or(&0),
553 1
554 );
555 assert_eq!(
556 *stats.by_source.get(&DiscoverySource::Relay).unwrap_or(&0),
557 0
558 );
559 }
560
561 #[test]
563 fn test_stats_avg_reliability() {
564 let mut cache = PeerDiscoveryCache::new(10, 300);
565 assert!((cache.stats().avg_reliability - 0.0).abs() < f64::EPSILON);
567
568 cache.upsert(make_record("good", DiscoverySource::Dht, 2000, 4, 0));
570 cache.upsert(make_record("bad", DiscoverySource::Dht, 2000, 0, 4));
571 let avg = cache.stats().avg_reliability;
572 assert!((avg - 0.5).abs() < f64::EPSILON);
573 }
574
575 #[test]
577 fn test_upsert_does_not_update_last_seen_if_older() {
578 let mut cache = PeerDiscoveryCache::new(10, 300);
579 cache.upsert(make_record("peer1", DiscoverySource::Dht, 5000, 0, 0));
580 let older = PeerRecord {
581 peer_id: "peer1".to_string(),
582 addresses: vec![],
583 source: DiscoverySource::Mdns,
584 discovered_at_secs: 1000,
585 last_seen_secs: 3000,
586 successful_connections: 0,
587 failed_connections: 0,
588 };
589 cache.upsert(older);
590 assert_eq!(cache.records["peer1"].last_seen_secs, 5000);
591 }
592
593 #[test]
595 fn test_manual_source() {
596 let mut cache = PeerDiscoveryCache::new(10, 300);
597 cache.upsert(make_record("manual1", DiscoverySource::Manual, 9000, 10, 0));
598 let peers = cache.peers_from_source(DiscoverySource::Manual);
599 assert_eq!(peers.len(), 1);
600 assert!((peers[0].reliability_score() - 1.0).abs() < f64::EPSILON);
601 }
602}