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::{FailureEvidence, NeighborSyncState};
398 use saorsa_core::identity::PeerId;
399 use tempfile::TempDir;
400
401 async fn create_test_paid_list() -> (PaidList, TempDir) {
402 let temp_dir = TempDir::new().expect("create temp dir");
403 let paid_list = PaidList::new(temp_dir.path())
404 .await
405 .expect("create paid list");
406 (paid_list, temp_dir)
407 }
408
409 #[tokio::test]
410 async fn test_insert_and_contains() {
411 let (pl, _temp) = create_test_paid_list().await;
412
413 let key: XorName = [0xAA; 32];
414 assert!(!pl.contains(&key).expect("contains before insert"));
415
416 let was_new = pl.insert(&key).await.expect("insert");
417 assert!(was_new);
418
419 assert!(pl.contains(&key).expect("contains after insert"));
420 }
421
422 #[tokio::test]
423 async fn test_insert_duplicate_returns_false() {
424 let (pl, _temp) = create_test_paid_list().await;
425
426 let key: XorName = [0xBB; 32];
427
428 let first = pl.insert(&key).await.expect("first insert");
429 assert!(first);
430
431 let second = pl.insert(&key).await.expect("second insert");
432 assert!(!second);
433 }
434
435 #[tokio::test]
436 async fn test_remove_existing() {
437 let (pl, _temp) = create_test_paid_list().await;
438
439 let key: XorName = [0xCC; 32];
440 pl.insert(&key).await.expect("insert");
441 assert!(pl.contains(&key).expect("contains"));
442
443 let removed = pl.remove(&key).await.expect("remove");
444 assert!(removed);
445 assert!(!pl.contains(&key).expect("contains after remove"));
446 }
447
448 #[tokio::test]
449 async fn test_remove_nonexistent() {
450 let (pl, _temp) = create_test_paid_list().await;
451
452 let key: XorName = [0xDD; 32];
453 let removed = pl.remove(&key).await.expect("remove nonexistent");
454 assert!(!removed);
455 }
456
457 #[tokio::test]
458 async fn test_persistence_across_reopen() {
459 let temp_dir = TempDir::new().expect("create temp dir");
460 let key: XorName = [0xEE; 32];
461
462 {
464 let pl = PaidList::new(temp_dir.path())
465 .await
466 .expect("create paid list");
467 pl.insert(&key).await.expect("insert");
468 assert_eq!(pl.count().expect("count"), 1);
469 }
470
471 {
473 let pl = PaidList::new(temp_dir.path())
474 .await
475 .expect("reopen paid list");
476 assert_eq!(pl.count().expect("count"), 1);
477 assert!(pl.contains(&key).expect("contains after reopen"));
478 }
479 }
480
481 #[tokio::test]
482 async fn test_all_keys() {
483 let (pl, _temp) = create_test_paid_list().await;
484
485 let key_a: XorName = [0x01; 32];
486 let key_b: XorName = [0x02; 32];
487 let key_c: XorName = [0x03; 32];
488
489 pl.insert(&key_a).await.expect("insert 1");
490 pl.insert(&key_b).await.expect("insert 2");
491 pl.insert(&key_c).await.expect("insert 3");
492
493 let mut keys = pl.all_keys().expect("all_keys");
494 keys.sort_unstable();
495
496 let mut expected = vec![key_a, key_b, key_c];
497 expected.sort_unstable();
498
499 assert_eq!(keys, expected);
500 }
501
502 #[tokio::test]
503 async fn test_count() {
504 let (pl, _temp) = create_test_paid_list().await;
505
506 assert_eq!(pl.count().expect("count empty"), 0);
507
508 let key1: XorName = [0x10; 32];
509 let key2: XorName = [0x20; 32];
510
511 pl.insert(&key1).await.expect("insert 1");
512 assert_eq!(pl.count().expect("count after 1"), 1);
513
514 pl.insert(&key2).await.expect("insert 2");
515 assert_eq!(pl.count().expect("count after 2"), 2);
516
517 pl.remove(&key1).await.expect("remove 1");
518 assert_eq!(pl.count().expect("count after remove"), 1);
519 }
520
521 #[tokio::test]
522 async fn test_paid_out_of_range_timestamps() {
523 let (pl, _temp) = create_test_paid_list().await;
524
525 let key: XorName = [0xF0; 32];
526
527 assert!(pl.paid_out_of_range_since(&key).is_none());
529
530 let before = Instant::now();
532 pl.set_paid_out_of_range(&key);
533 let after = Instant::now();
534
535 let ts = pl
536 .paid_out_of_range_since(&key)
537 .expect("timestamp should exist");
538 assert!(ts >= before);
539 assert!(ts <= after);
540
541 std::thread::sleep(std::time::Duration::from_millis(10));
543 pl.set_paid_out_of_range(&key);
544 let ts2 = pl
545 .paid_out_of_range_since(&key)
546 .expect("timestamp should still exist");
547 assert_eq!(ts, ts2);
548
549 pl.clear_paid_out_of_range(&key);
551 assert!(pl.paid_out_of_range_since(&key).is_none());
552 }
553
554 #[tokio::test]
555 async fn test_record_out_of_range_timestamps() {
556 let (pl, _temp) = create_test_paid_list().await;
557
558 let key: XorName = [0xF1; 32];
559
560 assert!(pl.record_out_of_range_since(&key).is_none());
561
562 let before = Instant::now();
563 pl.set_record_out_of_range(&key);
564 let after = Instant::now();
565
566 let ts = pl
567 .record_out_of_range_since(&key)
568 .expect("timestamp should exist");
569 assert!(ts >= before);
570 assert!(ts <= after);
571
572 std::thread::sleep(std::time::Duration::from_millis(10));
574 pl.set_record_out_of_range(&key);
575 let ts2 = pl
576 .record_out_of_range_since(&key)
577 .expect("timestamp should still exist");
578 assert_eq!(ts, ts2);
579
580 pl.clear_record_out_of_range(&key);
582 assert!(pl.record_out_of_range_since(&key).is_none());
583 }
584
585 #[tokio::test]
586 async fn test_remove_clears_timestamps() {
587 let (pl, _temp) = create_test_paid_list().await;
588
589 let key: XorName = [0xA0; 32];
590 pl.insert(&key).await.expect("insert");
591
592 pl.set_paid_out_of_range(&key);
593 pl.set_record_out_of_range(&key);
594 assert!(pl.paid_out_of_range_since(&key).is_some());
595 assert!(pl.record_out_of_range_since(&key).is_some());
596
597 pl.remove(&key).await.expect("remove");
598 assert!(pl.paid_out_of_range_since(&key).is_none());
599 assert!(pl.record_out_of_range_since(&key).is_none());
600 }
601
602 #[tokio::test]
603 async fn test_remove_batch() {
604 let (pl, _temp) = create_test_paid_list().await;
605
606 let key1: XorName = [0x01; 32];
607 let key2: XorName = [0x02; 32];
608 let key3: XorName = [0x03; 32];
609 let key4: XorName = [0x04; 32]; pl.insert(&key1).await.expect("insert 1");
612 pl.insert(&key2).await.expect("insert 2");
613 pl.insert(&key3).await.expect("insert 3");
614
615 pl.set_paid_out_of_range(&key1);
617 pl.set_record_out_of_range(&key2);
618
619 let removed = pl
620 .remove_batch(&[key1, key2, key4])
621 .await
622 .expect("remove_batch");
623 assert_eq!(removed, 2); assert!(!pl.contains(&key1).expect("key1 gone"));
626 assert!(!pl.contains(&key2).expect("key2 gone"));
627 assert!(pl.contains(&key3).expect("key3 still present"));
628 assert_eq!(pl.count().expect("count"), 1);
629
630 assert!(pl.paid_out_of_range_since(&key1).is_none());
632 assert!(pl.record_out_of_range_since(&key2).is_none());
633 }
634
635 #[tokio::test]
636 async fn test_remove_batch_empty() {
637 let (pl, _temp) = create_test_paid_list().await;
638
639 let removed = pl.remove_batch(&[]).await.expect("remove_batch empty");
640 assert_eq!(removed, 0);
641 }
642
643 #[tokio::test]
650 async fn scenario_50_hysteresis_prevents_premature_deletion() {
651 let (pl, _temp) = create_test_paid_list().await;
652 let key: XorName = [0x50; 32];
653
654 pl.set_record_out_of_range(&key);
656
657 let since = pl
659 .record_out_of_range_since(&key)
660 .expect("timestamp should exist after set");
661
662 let elapsed = since.elapsed();
664 assert!(
665 elapsed < PRUNE_HYSTERESIS_DURATION,
666 "elapsed ({elapsed:?}) should be far below PRUNE_HYSTERESIS_DURATION ({PRUNE_HYSTERESIS_DURATION:?})",
667 );
668 }
669
670 #[tokio::test]
673 async fn scenario_51_timestamp_reset_on_heal() {
674 let (pl, _temp) = create_test_paid_list().await;
675 let key: XorName = [0x51; 32];
676
677 pl.set_record_out_of_range(&key);
679 assert!(
680 pl.record_out_of_range_since(&key).is_some(),
681 "timestamp should exist after going out of range"
682 );
683
684 pl.clear_record_out_of_range(&key);
686 assert!(
687 pl.record_out_of_range_since(&key).is_none(),
688 "timestamp should be cleared after heal"
689 );
690
691 let before_second = Instant::now();
693 pl.set_record_out_of_range(&key);
694 let second_ts = pl
695 .record_out_of_range_since(&key)
696 .expect("timestamp should exist after second out-of-range");
697 assert!(
698 second_ts >= before_second,
699 "new timestamp should be >= the instant before second set call"
700 );
701 }
702
703 #[tokio::test]
706 async fn scenario_52_paid_and_record_timestamps_independent() {
707 let (pl, _temp) = create_test_paid_list().await;
708 let key: XorName = [0x52; 32];
709
710 pl.set_paid_out_of_range(&key);
712 pl.set_record_out_of_range(&key);
713 assert!(pl.paid_out_of_range_since(&key).is_some());
714 assert!(pl.record_out_of_range_since(&key).is_some());
715
716 pl.clear_record_out_of_range(&key);
718 assert!(
719 pl.paid_out_of_range_since(&key).is_some(),
720 "paid timestamp should survive clearing record timestamp"
721 );
722 assert!(pl.record_out_of_range_since(&key).is_none());
723
724 pl.set_record_out_of_range(&key);
726 pl.clear_paid_out_of_range(&key);
727 assert!(
728 pl.record_out_of_range_since(&key).is_some(),
729 "record timestamp should survive clearing paid timestamp"
730 );
731 assert!(pl.paid_out_of_range_since(&key).is_none());
732 }
733
734 #[tokio::test]
737 async fn scenario_23_paid_list_entry_removed() {
738 let (pl, _temp) = create_test_paid_list().await;
739 let key: XorName = [0x23; 32];
740
741 pl.insert(&key).await.expect("insert");
743 pl.set_paid_out_of_range(&key);
744 pl.set_record_out_of_range(&key);
745
746 let removed = pl.remove(&key).await.expect("remove");
748 assert!(removed, "key should have existed");
749 assert!(
750 !pl.contains(&key).expect("contains check"),
751 "key should be gone from paid list"
752 );
753 assert!(
754 pl.paid_out_of_range_since(&key).is_none(),
755 "paid timestamp should be cleaned up on remove"
756 );
757 assert!(
758 pl.record_out_of_range_since(&key).is_none(),
759 "record timestamp should be cleaned up on remove"
760 );
761 }
762
763 #[tokio::test]
768 async fn scenario_13_responsible_range_shrink() {
769 let (pl, _temp) = create_test_paid_list().await;
770
771 let out_of_range_key: XorName = [0x13; 32];
772 let in_range_key: XorName = [0x14; 32];
773
774 pl.insert(&out_of_range_key)
776 .await
777 .expect("insert out-of-range");
778 pl.insert(&in_range_key).await.expect("insert in-range");
779
780 pl.set_record_out_of_range(&out_of_range_key);
783 let first_seen = pl
784 .record_out_of_range_since(&out_of_range_key)
785 .expect("timestamp should be recorded for out-of-range key");
786
787 let elapsed = first_seen.elapsed();
789 assert!(
790 elapsed < PRUNE_HYSTERESIS_DURATION,
791 "elapsed {elapsed:?} should be below PRUNE_HYSTERESIS_DURATION \
792 ({PRUNE_HYSTERESIS_DURATION:?}) — key must not be pruned yet"
793 );
794
795 assert!(
797 pl.contains(&out_of_range_key).expect("contains"),
798 "out-of-range key should still be retained within hysteresis window"
799 );
800
801 assert!(
803 pl.record_out_of_range_since(&in_range_key).is_none(),
804 "in-range key should have no out-of-range timestamp"
805 );
806
807 let new_key: XorName = [0x15; 32];
809 let was_new = pl.insert(&new_key).await.expect("insert new key");
810 assert!(
811 was_new,
812 "new in-range keys should still be accepted while out-of-range keys await expiry"
813 );
814 assert!(
815 pl.contains(&new_key).expect("contains new"),
816 "newly inserted in-range key should be present"
817 );
818 }
819
820 #[test]
823 fn scenario_46_bootstrap_claim_first_seen_recorded() {
824 let peer = PeerId::from_bytes([0x46; 32]);
825 let mut state = NeighborSyncState::new_cycle(vec![peer]);
826
827 let first_ts = Instant::now()
829 .checked_sub(std::time::Duration::from_secs(3))
830 .unwrap_or_else(Instant::now);
831 state.bootstrap_claims.insert(peer, first_ts);
832
833 assert_eq!(
835 state.bootstrap_claims.get(&peer),
836 Some(&first_ts),
837 "first-seen timestamp should be recorded"
838 );
839
840 let later_ts = Instant::now();
842 state.bootstrap_claims.entry(peer).or_insert(later_ts);
843 assert_eq!(
844 state.bootstrap_claims.get(&peer),
845 Some(&first_ts),
846 "second insert must not overwrite the original timestamp"
847 );
848 }
849
850 #[test]
854 fn scenario_48_bootstrap_claim_abuse_after_grace_period() {
855 let peer = PeerId::from_bytes([0x48; 32]);
856 let mut state = NeighborSyncState::new_cycle(vec![peer]);
857
858 let grace_plus_margin = BOOTSTRAP_CLAIM_GRACE_PERIOD + std::time::Duration::from_secs(3600);
865 let first_seen = Instant::now()
866 .checked_sub(grace_plus_margin)
867 .unwrap_or_else(Instant::now);
868 state.bootstrap_claims.insert(peer, first_seen);
869
870 let claim_age = Instant::now().duration_since(first_seen);
872 if claim_age > std::time::Duration::from_secs(1) {
873 assert!(
874 claim_age > BOOTSTRAP_CLAIM_GRACE_PERIOD,
875 "claim age {claim_age:?} should exceed grace period {BOOTSTRAP_CLAIM_GRACE_PERIOD:?}",
876 );
877 }
878
879 let evidence = FailureEvidence::BootstrapClaimAbuse { peer, first_seen };
881
882 let FailureEvidence::BootstrapClaimAbuse {
883 peer: p,
884 first_seen: fs,
885 } = evidence
886 else {
887 unreachable!("evidence was just constructed as BootstrapClaimAbuse");
888 };
889 assert_eq!(p, peer);
890 assert_eq!(fs, first_seen);
891 }
892
893 #[test]
895 fn scenario_49_bootstrap_claim_cleared() {
896 let peer = PeerId::from_bytes([0x49; 32]);
897 let mut state = NeighborSyncState::new_cycle(vec![peer]);
898
899 state.bootstrap_claims.insert(peer, Instant::now());
901 assert!(
902 state.bootstrap_claims.contains_key(&peer),
903 "claim should exist after insert"
904 );
905
906 state.bootstrap_claims.remove(&peer);
908 assert!(
909 !state.bootstrap_claims.contains_key(&peer),
910 "claim should be gone after normal response"
911 );
912 }
913}