1use crate::ant_protocol::XorName;
24use crate::error::{Error, Result};
25use crate::logging::{debug, trace, warn};
26use heed::types::Bytes;
27use heed::{Database, Env, EnvOpenOptions};
28use parking_lot::RwLock;
29use std::collections::HashMap;
30use std::path::Path;
31use std::time::Instant;
32use tokio::task::spawn_blocking;
33
34use crate::ant_protocol::XORNAME_LEN;
35
36const DEFAULT_MAP_SIZE: usize = 256 * 1_024 * 1_024;
41
42pub struct PaidList {
47 env: Env,
49 db: Database<Bytes, Bytes>,
51 paid_out_of_range: RwLock<HashMap<XorName, Instant>>,
54 record_out_of_range: RwLock<HashMap<XorName, Instant>>,
57}
58
59impl PaidList {
60 #[allow(unsafe_code)]
67 pub async fn new(root_dir: &Path) -> Result<Self> {
68 let env_dir = root_dir.join("paid_list.mdb");
69
70 std::fs::create_dir_all(&env_dir)
71 .map_err(|e| Error::Storage(format!("Failed to create paid-list directory: {e}")))?;
72
73 let env_dir_clone = env_dir.clone();
74 let (env, db) = spawn_blocking(move || -> Result<(Env, Database<Bytes, Bytes>)> {
75 let env = unsafe {
82 EnvOpenOptions::new()
83 .map_size(DEFAULT_MAP_SIZE)
84 .max_dbs(1)
85 .open(&env_dir_clone)
86 .map_err(|e| {
87 Error::Storage(format!("Failed to open paid-list LMDB env: {e}"))
88 })?
89 };
90
91 let mut wtxn = env
92 .write_txn()
93 .map_err(|e| Error::Storage(format!("Failed to create write txn: {e}")))?;
94 let db: Database<Bytes, Bytes> = env
95 .create_database(&mut wtxn, None)
96 .map_err(|e| Error::Storage(format!("Failed to create paid-list database: {e}")))?;
97 wtxn.commit()
98 .map_err(|e| Error::Storage(format!("Failed to commit db creation: {e}")))?;
99
100 Ok((env, db))
101 })
102 .await
103 .map_err(|e| Error::Storage(format!("Paid-list init task failed: {e}")))??;
104
105 let paid_list = Self {
106 env,
107 db,
108 paid_out_of_range: RwLock::new(HashMap::new()),
109 record_out_of_range: RwLock::new(HashMap::new()),
110 };
111
112 let count = paid_list.count()?;
113 debug!("Initialized paid-list at {env_dir:?} ({count} existing keys)");
114
115 Ok(paid_list)
116 }
117
118 pub async fn insert(&self, key: &XorName) -> Result<bool> {
126 if self.contains(key)? {
128 trace!("Paid-list key {} already present", hex::encode(key));
129 return Ok(false);
130 }
131
132 let key_owned = *key;
133 let env = self.env.clone();
134 let db = self.db;
135
136 let was_new = spawn_blocking(move || -> Result<bool> {
137 let mut wtxn = env
138 .write_txn()
139 .map_err(|e| Error::Storage(format!("Failed to create write txn: {e}")))?;
140
141 if db
143 .get(&wtxn, &key_owned)
144 .map_err(|e| Error::Storage(format!("Failed to check paid-list existence: {e}")))?
145 .is_some()
146 {
147 return Ok(false);
148 }
149
150 db.put(&mut wtxn, &key_owned, &[])
151 .map_err(|e| Error::Storage(format!("Failed to insert into paid-list: {e}")))?;
152 wtxn.commit()
153 .map_err(|e| Error::Storage(format!("Failed to commit paid-list insert: {e}")))?;
154
155 Ok(true)
156 })
157 .await
158 .map_err(|e| Error::Storage(format!("Paid-list insert task failed: {e}")))??;
159
160 if was_new {
161 debug!("Added key {} to paid-list", hex::encode(key));
162 }
163
164 Ok(was_new)
165 }
166
167 pub async fn remove(&self, key: &XorName) -> Result<bool> {
177 let key_owned = *key;
178 let env = self.env.clone();
179 let db = self.db;
180
181 let existed = spawn_blocking(move || -> Result<bool> {
182 let mut wtxn = env
183 .write_txn()
184 .map_err(|e| Error::Storage(format!("Failed to create write txn: {e}")))?;
185 let deleted = db
186 .delete(&mut wtxn, &key_owned)
187 .map_err(|e| Error::Storage(format!("Failed to delete from paid-list: {e}")))?;
188 wtxn.commit()
189 .map_err(|e| Error::Storage(format!("Failed to commit paid-list delete: {e}")))?;
190 Ok(deleted)
191 })
192 .await
193 .map_err(|e| Error::Storage(format!("Paid-list remove task failed: {e}")))??;
194
195 if existed {
196 self.paid_out_of_range.write().remove(key);
197 self.record_out_of_range.write().remove(key);
198 debug!("Removed key {} from paid-list", hex::encode(key));
199 }
200
201 Ok(existed)
202 }
203
204 pub fn contains(&self, key: &XorName) -> Result<bool> {
212 let rtxn = self
213 .env
214 .read_txn()
215 .map_err(|e| Error::Storage(format!("Failed to create read txn: {e}")))?;
216 let found = self
217 .db
218 .get(&rtxn, key.as_ref())
219 .map_err(|e| Error::Storage(format!("Failed to check paid-list membership: {e}")))?
220 .is_some();
221 Ok(found)
222 }
223
224 pub fn count(&self) -> Result<u64> {
232 let rtxn = self
233 .env
234 .read_txn()
235 .map_err(|e| Error::Storage(format!("Failed to create read txn: {e}")))?;
236 let entries = self
237 .db
238 .stat(&rtxn)
239 .map_err(|e| Error::Storage(format!("Failed to read paid-list stats: {e}")))?
240 .entries;
241 Ok(entries as u64)
242 }
243
244 pub fn all_keys(&self) -> Result<Vec<XorName>> {
252 let rtxn = self
253 .env
254 .read_txn()
255 .map_err(|e| Error::Storage(format!("Failed to create read txn: {e}")))?;
256 let mut keys = Vec::new();
257 let iter = self
258 .db
259 .iter(&rtxn)
260 .map_err(|e| Error::Storage(format!("Failed to iterate paid-list: {e}")))?;
261 for result in iter {
262 let (key_bytes, _) = result
263 .map_err(|e| Error::Storage(format!("Failed to read paid-list entry: {e}")))?;
264 if key_bytes.len() == XORNAME_LEN {
265 let mut key = [0u8; XORNAME_LEN];
266 key.copy_from_slice(key_bytes);
267 keys.push(key);
268 } else {
269 warn!(
270 "PaidList: skipping entry with unexpected key length {} (expected {XORNAME_LEN})",
271 key_bytes.len()
272 );
273 }
274 }
275 Ok(keys)
276 }
277
278 pub fn set_paid_out_of_range(&self, key: &XorName) {
283 self.paid_out_of_range
284 .write()
285 .entry(*key)
286 .or_insert_with(Instant::now);
287 }
288
289 pub fn clear_paid_out_of_range(&self, key: &XorName) {
293 self.paid_out_of_range.write().remove(key);
294 }
295
296 pub fn paid_out_of_range_since(&self, key: &XorName) -> Option<Instant> {
300 self.paid_out_of_range.read().get(key).copied()
301 }
302
303 pub fn set_record_out_of_range(&self, key: &XorName) {
308 self.record_out_of_range
309 .write()
310 .entry(*key)
311 .or_insert_with(Instant::now);
312 }
313
314 pub fn clear_record_out_of_range(&self, key: &XorName) {
318 self.record_out_of_range.write().remove(key);
319 }
320
321 pub fn record_out_of_range_since(&self, key: &XorName) -> Option<Instant> {
325 self.record_out_of_range.read().get(key).copied()
326 }
327
328 pub async fn remove_batch(&self, keys: &[XorName]) -> Result<usize> {
338 if keys.is_empty() {
339 return Ok(0);
340 }
341
342 let keys_owned: Vec<XorName> = keys.to_vec();
343 let env = self.env.clone();
344 let db = self.db;
345
346 let removed_keys = spawn_blocking(move || -> Result<Vec<XorName>> {
347 let mut wtxn = env
348 .write_txn()
349 .map_err(|e| Error::Storage(format!("Failed to create write txn: {e}")))?;
350
351 let mut removed = Vec::new();
352 for key in &keys_owned {
353 let deleted = db
354 .delete(&mut wtxn, key.as_ref())
355 .map_err(|e| Error::Storage(format!("Failed to delete from paid-list: {e}")))?;
356 if deleted {
357 removed.push(*key);
358 }
359 }
360
361 wtxn.commit()
362 .map_err(|e| Error::Storage(format!("Failed to commit batch remove: {e}")))?;
363
364 Ok(removed)
365 })
366 .await
367 .map_err(|e| Error::Storage(format!("Paid-list batch remove task failed: {e}")))??;
368
369 if !removed_keys.is_empty() {
372 {
373 let mut paid_oor = self.paid_out_of_range.write();
374 for key in &removed_keys {
375 paid_oor.remove(key);
376 }
377 }
378 {
379 let mut record_oor = self.record_out_of_range.write();
380 for key in &removed_keys {
381 record_oor.remove(key);
382 }
383 }
384 }
385
386 let count = removed_keys.len();
387 debug!("Batch-removed {count} keys from paid-list");
388 Ok(count)
389 }
390}
391
392#[cfg(test)]
393#[allow(clippy::unwrap_used, clippy::expect_used)]
394mod tests {
395 use super::*;
396 use crate::replication::config::{BOOTSTRAP_CLAIM_GRACE_PERIOD, PRUNE_HYSTERESIS_DURATION};
397 use crate::replication::types::{
398 BootstrapClaimObservation, FailureEvidence, NeighborSyncState,
399 };
400 use saorsa_core::identity::PeerId;
401 use tempfile::TempDir;
402
403 async fn create_test_paid_list() -> (PaidList, TempDir) {
404 let temp_dir = TempDir::new().expect("create temp dir");
405 let paid_list = PaidList::new(temp_dir.path())
406 .await
407 .expect("create paid list");
408 (paid_list, temp_dir)
409 }
410
411 #[tokio::test]
412 async fn test_insert_and_contains() {
413 let (pl, _temp) = create_test_paid_list().await;
414
415 let key: XorName = [0xAA; 32];
416 assert!(!pl.contains(&key).expect("contains before insert"));
417
418 let was_new = pl.insert(&key).await.expect("insert");
419 assert!(was_new);
420
421 assert!(pl.contains(&key).expect("contains after insert"));
422 }
423
424 #[tokio::test]
425 async fn test_insert_duplicate_returns_false() {
426 let (pl, _temp) = create_test_paid_list().await;
427
428 let key: XorName = [0xBB; 32];
429
430 let first = pl.insert(&key).await.expect("first insert");
431 assert!(first);
432
433 let second = pl.insert(&key).await.expect("second insert");
434 assert!(!second);
435 }
436
437 #[tokio::test]
438 async fn test_remove_existing() {
439 let (pl, _temp) = create_test_paid_list().await;
440
441 let key: XorName = [0xCC; 32];
442 pl.insert(&key).await.expect("insert");
443 assert!(pl.contains(&key).expect("contains"));
444
445 let removed = pl.remove(&key).await.expect("remove");
446 assert!(removed);
447 assert!(!pl.contains(&key).expect("contains after remove"));
448 }
449
450 #[tokio::test]
451 async fn test_remove_nonexistent() {
452 let (pl, _temp) = create_test_paid_list().await;
453
454 let key: XorName = [0xDD; 32];
455 let removed = pl.remove(&key).await.expect("remove nonexistent");
456 assert!(!removed);
457 }
458
459 #[tokio::test]
460 async fn test_persistence_across_reopen() {
461 let temp_dir = TempDir::new().expect("create temp dir");
462 let key: XorName = [0xEE; 32];
463
464 {
466 let pl = PaidList::new(temp_dir.path())
467 .await
468 .expect("create paid list");
469 pl.insert(&key).await.expect("insert");
470 assert_eq!(pl.count().expect("count"), 1);
471 }
472
473 {
475 let pl = PaidList::new(temp_dir.path())
476 .await
477 .expect("reopen paid list");
478 assert_eq!(pl.count().expect("count"), 1);
479 assert!(pl.contains(&key).expect("contains after reopen"));
480 }
481 }
482
483 #[tokio::test]
484 async fn test_all_keys() {
485 let (pl, _temp) = create_test_paid_list().await;
486
487 let key_a: XorName = [0x01; 32];
488 let key_b: XorName = [0x02; 32];
489 let key_c: XorName = [0x03; 32];
490
491 pl.insert(&key_a).await.expect("insert 1");
492 pl.insert(&key_b).await.expect("insert 2");
493 pl.insert(&key_c).await.expect("insert 3");
494
495 let mut keys = pl.all_keys().expect("all_keys");
496 keys.sort_unstable();
497
498 let mut expected = vec![key_a, key_b, key_c];
499 expected.sort_unstable();
500
501 assert_eq!(keys, expected);
502 }
503
504 #[tokio::test]
505 async fn test_count() {
506 let (pl, _temp) = create_test_paid_list().await;
507
508 assert_eq!(pl.count().expect("count empty"), 0);
509
510 let key1: XorName = [0x10; 32];
511 let key2: XorName = [0x20; 32];
512
513 pl.insert(&key1).await.expect("insert 1");
514 assert_eq!(pl.count().expect("count after 1"), 1);
515
516 pl.insert(&key2).await.expect("insert 2");
517 assert_eq!(pl.count().expect("count after 2"), 2);
518
519 pl.remove(&key1).await.expect("remove 1");
520 assert_eq!(pl.count().expect("count after remove"), 1);
521 }
522
523 #[tokio::test]
524 async fn test_paid_out_of_range_timestamps() {
525 let (pl, _temp) = create_test_paid_list().await;
526
527 let key: XorName = [0xF0; 32];
528
529 assert!(pl.paid_out_of_range_since(&key).is_none());
531
532 let before = Instant::now();
534 pl.set_paid_out_of_range(&key);
535 let after = Instant::now();
536
537 let ts = pl
538 .paid_out_of_range_since(&key)
539 .expect("timestamp should exist");
540 assert!(ts >= before);
541 assert!(ts <= after);
542
543 std::thread::sleep(std::time::Duration::from_millis(10));
545 pl.set_paid_out_of_range(&key);
546 let ts2 = pl
547 .paid_out_of_range_since(&key)
548 .expect("timestamp should still exist");
549 assert_eq!(ts, ts2);
550
551 pl.clear_paid_out_of_range(&key);
553 assert!(pl.paid_out_of_range_since(&key).is_none());
554 }
555
556 #[tokio::test]
557 async fn test_record_out_of_range_timestamps() {
558 let (pl, _temp) = create_test_paid_list().await;
559
560 let key: XorName = [0xF1; 32];
561
562 assert!(pl.record_out_of_range_since(&key).is_none());
563
564 let before = Instant::now();
565 pl.set_record_out_of_range(&key);
566 let after = Instant::now();
567
568 let ts = pl
569 .record_out_of_range_since(&key)
570 .expect("timestamp should exist");
571 assert!(ts >= before);
572 assert!(ts <= after);
573
574 std::thread::sleep(std::time::Duration::from_millis(10));
576 pl.set_record_out_of_range(&key);
577 let ts2 = pl
578 .record_out_of_range_since(&key)
579 .expect("timestamp should still exist");
580 assert_eq!(ts, ts2);
581
582 pl.clear_record_out_of_range(&key);
584 assert!(pl.record_out_of_range_since(&key).is_none());
585 }
586
587 #[tokio::test]
588 async fn test_remove_clears_timestamps() {
589 let (pl, _temp) = create_test_paid_list().await;
590
591 let key: XorName = [0xA0; 32];
592 pl.insert(&key).await.expect("insert");
593
594 pl.set_paid_out_of_range(&key);
595 pl.set_record_out_of_range(&key);
596 assert!(pl.paid_out_of_range_since(&key).is_some());
597 assert!(pl.record_out_of_range_since(&key).is_some());
598
599 pl.remove(&key).await.expect("remove");
600 assert!(pl.paid_out_of_range_since(&key).is_none());
601 assert!(pl.record_out_of_range_since(&key).is_none());
602 }
603
604 #[tokio::test]
605 async fn test_remove_batch() {
606 let (pl, _temp) = create_test_paid_list().await;
607
608 let key1: XorName = [0x01; 32];
609 let key2: XorName = [0x02; 32];
610 let key3: XorName = [0x03; 32];
611 let key4: XorName = [0x04; 32]; pl.insert(&key1).await.expect("insert 1");
614 pl.insert(&key2).await.expect("insert 2");
615 pl.insert(&key3).await.expect("insert 3");
616
617 pl.set_paid_out_of_range(&key1);
619 pl.set_record_out_of_range(&key2);
620
621 let removed = pl
622 .remove_batch(&[key1, key2, key4])
623 .await
624 .expect("remove_batch");
625 assert_eq!(removed, 2); assert!(!pl.contains(&key1).expect("key1 gone"));
628 assert!(!pl.contains(&key2).expect("key2 gone"));
629 assert!(pl.contains(&key3).expect("key3 still present"));
630 assert_eq!(pl.count().expect("count"), 1);
631
632 assert!(pl.paid_out_of_range_since(&key1).is_none());
634 assert!(pl.record_out_of_range_since(&key2).is_none());
635 }
636
637 #[tokio::test]
638 async fn test_remove_batch_empty() {
639 let (pl, _temp) = create_test_paid_list().await;
640
641 let removed = pl.remove_batch(&[]).await.expect("remove_batch empty");
642 assert_eq!(removed, 0);
643 }
644
645 #[tokio::test]
652 async fn scenario_50_hysteresis_prevents_premature_deletion() {
653 let (pl, _temp) = create_test_paid_list().await;
654 let key: XorName = [0x50; 32];
655
656 pl.set_record_out_of_range(&key);
658
659 let since = pl
661 .record_out_of_range_since(&key)
662 .expect("timestamp should exist after set");
663
664 let elapsed = since.elapsed();
666 assert!(
667 elapsed < PRUNE_HYSTERESIS_DURATION,
668 "elapsed ({elapsed:?}) should be far below PRUNE_HYSTERESIS_DURATION ({PRUNE_HYSTERESIS_DURATION:?})",
669 );
670 }
671
672 #[tokio::test]
675 async fn scenario_51_timestamp_reset_on_heal() {
676 let (pl, _temp) = create_test_paid_list().await;
677 let key: XorName = [0x51; 32];
678
679 pl.set_record_out_of_range(&key);
681 assert!(
682 pl.record_out_of_range_since(&key).is_some(),
683 "timestamp should exist after going out of range"
684 );
685
686 pl.clear_record_out_of_range(&key);
688 assert!(
689 pl.record_out_of_range_since(&key).is_none(),
690 "timestamp should be cleared after heal"
691 );
692
693 let before_second = Instant::now();
695 pl.set_record_out_of_range(&key);
696 let second_ts = pl
697 .record_out_of_range_since(&key)
698 .expect("timestamp should exist after second out-of-range");
699 assert!(
700 second_ts >= before_second,
701 "new timestamp should be >= the instant before second set call"
702 );
703 }
704
705 #[tokio::test]
708 async fn scenario_52_paid_and_record_timestamps_independent() {
709 let (pl, _temp) = create_test_paid_list().await;
710 let key: XorName = [0x52; 32];
711
712 pl.set_paid_out_of_range(&key);
714 pl.set_record_out_of_range(&key);
715 assert!(pl.paid_out_of_range_since(&key).is_some());
716 assert!(pl.record_out_of_range_since(&key).is_some());
717
718 pl.clear_record_out_of_range(&key);
720 assert!(
721 pl.paid_out_of_range_since(&key).is_some(),
722 "paid timestamp should survive clearing record timestamp"
723 );
724 assert!(pl.record_out_of_range_since(&key).is_none());
725
726 pl.set_record_out_of_range(&key);
728 pl.clear_paid_out_of_range(&key);
729 assert!(
730 pl.record_out_of_range_since(&key).is_some(),
731 "record timestamp should survive clearing paid timestamp"
732 );
733 assert!(pl.paid_out_of_range_since(&key).is_none());
734 }
735
736 #[tokio::test]
739 async fn scenario_23_paid_list_entry_removed() {
740 let (pl, _temp) = create_test_paid_list().await;
741 let key: XorName = [0x23; 32];
742
743 pl.insert(&key).await.expect("insert");
745 pl.set_paid_out_of_range(&key);
746 pl.set_record_out_of_range(&key);
747
748 let removed = pl.remove(&key).await.expect("remove");
750 assert!(removed, "key should have existed");
751 assert!(
752 !pl.contains(&key).expect("contains check"),
753 "key should be gone from paid list"
754 );
755 assert!(
756 pl.paid_out_of_range_since(&key).is_none(),
757 "paid timestamp should be cleaned up on remove"
758 );
759 assert!(
760 pl.record_out_of_range_since(&key).is_none(),
761 "record timestamp should be cleaned up on remove"
762 );
763 }
764
765 #[tokio::test]
770 async fn scenario_13_responsible_range_shrink() {
771 let (pl, _temp) = create_test_paid_list().await;
772
773 let out_of_range_key: XorName = [0x13; 32];
774 let in_range_key: XorName = [0x14; 32];
775
776 pl.insert(&out_of_range_key)
778 .await
779 .expect("insert out-of-range");
780 pl.insert(&in_range_key).await.expect("insert in-range");
781
782 pl.set_record_out_of_range(&out_of_range_key);
785 let first_seen = pl
786 .record_out_of_range_since(&out_of_range_key)
787 .expect("timestamp should be recorded for out-of-range key");
788
789 let elapsed = first_seen.elapsed();
791 assert!(
792 elapsed < PRUNE_HYSTERESIS_DURATION,
793 "elapsed {elapsed:?} should be below PRUNE_HYSTERESIS_DURATION \
794 ({PRUNE_HYSTERESIS_DURATION:?}) — key must not be pruned yet"
795 );
796
797 assert!(
799 pl.contains(&out_of_range_key).expect("contains"),
800 "out-of-range key should still be retained within hysteresis window"
801 );
802
803 assert!(
805 pl.record_out_of_range_since(&in_range_key).is_none(),
806 "in-range key should have no out-of-range timestamp"
807 );
808
809 let new_key: XorName = [0x15; 32];
811 let was_new = pl.insert(&new_key).await.expect("insert new key");
812 assert!(
813 was_new,
814 "new in-range keys should still be accepted while out-of-range keys await expiry"
815 );
816 assert!(
817 pl.contains(&new_key).expect("contains new"),
818 "newly inserted in-range key should be present"
819 );
820 }
821
822 #[test]
825 fn scenario_46_bootstrap_claim_first_seen_recorded() {
826 let peer = PeerId::from_bytes([0x46; 32]);
827 let mut state = NeighborSyncState::new_cycle(vec![peer]);
828
829 let first_ts = Instant::now()
830 .checked_sub(std::time::Duration::from_secs(3))
831 .unwrap_or_else(Instant::now);
832 let observed = state.observe_bootstrap_claim(peer, first_ts, BOOTSTRAP_CLAIM_GRACE_PERIOD);
833 assert_eq!(
834 observed,
835 BootstrapClaimObservation::WithinGrace {
836 first_seen: first_ts
837 }
838 );
839
840 assert_eq!(
842 state.bootstrap_claims.get(&peer),
843 Some(&first_ts),
844 "first-seen timestamp should be recorded"
845 );
846 assert_eq!(
847 state.bootstrap_claim_history.get(&peer),
848 Some(&first_ts),
849 "first-ever timestamp should be retained"
850 );
851
852 let later_ts = Instant::now();
855 let observed = state.observe_bootstrap_claim(peer, later_ts, BOOTSTRAP_CLAIM_GRACE_PERIOD);
856 assert_eq!(
857 observed,
858 BootstrapClaimObservation::WithinGrace {
859 first_seen: first_ts
860 }
861 );
862 assert_eq!(
863 state.bootstrap_claims.get(&peer),
864 Some(&first_ts),
865 "second insert must not overwrite the original timestamp"
866 );
867 }
868
869 #[test]
873 fn scenario_48_bootstrap_claim_abuse_after_grace_period() {
874 let peer = PeerId::from_bytes([0x48; 32]);
875 let mut state = NeighborSyncState::new_cycle(vec![peer]);
876
877 let grace_plus_margin = BOOTSTRAP_CLAIM_GRACE_PERIOD + std::time::Duration::from_secs(3600);
884 let first_seen = Instant::now()
885 .checked_sub(grace_plus_margin)
886 .unwrap_or_else(Instant::now);
887 state.bootstrap_claims.insert(peer, first_seen);
888 state.bootstrap_claim_history.insert(peer, first_seen);
889
890 let claim_age = Instant::now().duration_since(first_seen);
892 if claim_age > std::time::Duration::from_secs(1) {
893 assert!(
894 claim_age > BOOTSTRAP_CLAIM_GRACE_PERIOD,
895 "claim age {claim_age:?} should exceed grace period {BOOTSTRAP_CLAIM_GRACE_PERIOD:?}",
896 );
897 }
898
899 let evidence = FailureEvidence::BootstrapClaimAbuse { peer, first_seen };
901
902 let FailureEvidence::BootstrapClaimAbuse {
903 peer: p,
904 first_seen: fs,
905 } = evidence
906 else {
907 unreachable!("evidence was just constructed as BootstrapClaimAbuse");
908 };
909 assert_eq!(p, peer);
910 assert_eq!(fs, first_seen);
911 }
912
913 #[test]
915 fn scenario_49_bootstrap_claim_cleared() {
916 let peer = PeerId::from_bytes([0x49; 32]);
917 let mut state = NeighborSyncState::new_cycle(vec![peer]);
918
919 let first_seen = Instant::now();
921 let _ = state.observe_bootstrap_claim(peer, first_seen, BOOTSTRAP_CLAIM_GRACE_PERIOD);
922 assert!(
923 state.bootstrap_claims.contains_key(&peer),
924 "claim should exist after insert"
925 );
926
927 state.clear_active_bootstrap_claim(&peer);
929 assert!(
930 !state.bootstrap_claims.contains_key(&peer),
931 "claim should be gone after normal response"
932 );
933 assert!(
934 state.bootstrap_claim_history.contains_key(&peer),
935 "claim history should remain so the peer cannot claim bootstrapping again"
936 );
937
938 let repeated =
939 state.observe_bootstrap_claim(peer, Instant::now(), BOOTSTRAP_CLAIM_GRACE_PERIOD);
940 assert_eq!(
941 repeated,
942 BootstrapClaimObservation::Repeated { first_seen },
943 "a second bootstrap claim should be classified as repeated abuse"
944 );
945 }
946}