1use std::cmp::Ordering;
8use std::collections::{HashMap, HashSet};
9use std::time::{Duration, Instant};
10
11use serde::{Deserialize, Serialize};
12
13use crate::ant_protocol::XorName;
14use saorsa_core::identity::PeerId;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum VerificationState {
27 OfferReceived,
29 PendingVerify,
31 QuorumVerified,
34 PaidListVerified,
37 QueuedForFetch,
39 Fetching,
41 Stored,
43 FetchRetryable,
45 FetchAbandoned,
47 QuorumFailed,
50 QuorumInconclusive,
52 QuorumAbandoned,
54 Idle,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub enum HintPipeline {
66 Replica,
68 PaidOnly,
71}
72
73#[derive(Debug, Clone)]
82pub struct VerificationEntry {
83 pub state: VerificationState,
85 pub pipeline: HintPipeline,
87 pub verified_sources: Vec<PeerId>,
90 pub tried_sources: HashSet<PeerId>,
92 pub created_at: Instant,
94 pub hint_sender: PeerId,
96}
97
98#[derive(Debug, Clone)]
108pub struct FetchCandidate {
109 pub key: XorName,
111 pub distance: XorName,
113 pub sources: Vec<PeerId>,
115}
116
117impl Eq for FetchCandidate {}
118
119impl PartialEq for FetchCandidate {
120 fn eq(&self, other: &Self) -> bool {
121 self.distance == other.distance && self.key == other.key
122 }
123}
124
125impl Ord for FetchCandidate {
126 fn cmp(&self, other: &Self) -> Ordering {
127 other
130 .distance
131 .cmp(&self.distance)
132 .then_with(|| self.key.cmp(&other.key))
133 }
134}
135
136impl PartialOrd for FetchCandidate {
137 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
138 Some(self.cmp(other))
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148pub enum PresenceEvidence {
149 Present,
151 Absent,
153 Unresolved,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159pub enum PaidListEvidence {
160 Confirmed,
162 NotFound,
164 Unresolved,
166}
167
168#[derive(Debug, Clone)]
171pub struct KeyVerificationEvidence {
172 pub presence: HashMap<PeerId, PresenceEvidence>,
174 pub paid_list: HashMap<PeerId, PaidListEvidence>,
176}
177
178#[derive(Debug, Clone)]
184pub enum FailureEvidence {
185 ReplicationFailure {
187 peer: PeerId,
189 key: XorName,
191 },
192 AuditFailure {
194 challenge_id: u64,
196 challenged_peer: PeerId,
198 confirmed_failed_keys: Vec<XorName>,
200 reason: AuditFailureReason,
202 },
203 BootstrapClaimAbuse {
205 peer: PeerId,
207 first_seen: Instant,
209 },
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub enum AuditFailureReason {
215 Timeout,
217 MalformedResponse,
219 DigestMismatch,
221 KeyAbsent,
223 Rejected,
225}
226
227#[derive(Debug, Clone)]
233pub struct PeerSyncRecord {
234 pub last_sync: Option<Instant>,
236 pub cycles_since_sync: u32,
239}
240
241impl PeerSyncRecord {
242 #[must_use]
245 pub fn has_repair_opportunity(&self) -> bool {
246 self.last_sync.is_some() && self.cycles_since_sync >= 1
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub enum BootstrapClaimObservation {
257 WithinGrace {
259 first_seen: Instant,
261 },
262 PastGrace {
264 first_seen: Instant,
266 },
267 Repeated {
269 first_seen: Instant,
271 },
272}
273
274#[derive(Debug)]
280pub struct NeighborSyncState {
281 pub order: Vec<PeerId>,
283 pub cursor: usize,
285 pub last_sync_times: HashMap<PeerId, Instant>,
287 pub bootstrap_claims: HashMap<PeerId, Instant>,
293 pub bootstrap_claim_history: HashMap<PeerId, Instant>,
301 pub prune_cursor: usize,
304}
305
306impl NeighborSyncState {
307 #[must_use]
309 pub fn new_cycle(close_neighbors: Vec<PeerId>) -> Self {
310 Self {
311 order: close_neighbors,
312 cursor: 0,
313 last_sync_times: HashMap::new(),
314 bootstrap_claims: HashMap::new(),
315 bootstrap_claim_history: HashMap::new(),
316 prune_cursor: 0,
317 }
318 }
319
320 #[must_use]
327 pub fn observe_bootstrap_claim(
328 &mut self,
329 peer: PeerId,
330 now: Instant,
331 grace_period: Duration,
332 ) -> BootstrapClaimObservation {
333 if let Some(first_seen) = self.bootstrap_claims.get(&peer).copied() {
334 if now.duration_since(first_seen) > grace_period {
335 BootstrapClaimObservation::PastGrace { first_seen }
336 } else {
337 BootstrapClaimObservation::WithinGrace { first_seen }
338 }
339 } else if let Some(first_seen) = self.bootstrap_claim_history.get(&peer).copied() {
340 BootstrapClaimObservation::Repeated { first_seen }
341 } else {
342 self.bootstrap_claims.insert(peer, now);
343 self.bootstrap_claim_history.insert(peer, now);
344 BootstrapClaimObservation::WithinGrace { first_seen: now }
345 }
346 }
347
348 pub fn clear_active_bootstrap_claim(&mut self, peer: &PeerId) -> bool {
350 self.bootstrap_claims.remove(peer).is_some()
351 }
352
353 #[must_use]
355 pub fn is_cycle_complete(&self) -> bool {
356 self.cursor >= self.order.len()
357 }
358}
359
360#[derive(Debug)]
366pub struct BootstrapState {
367 pub drained: bool,
369 pub pending_peer_requests: usize,
371 pub pending_keys: HashSet<XorName>,
374 pub capacity_rejected_sources: HashSet<PeerId>,
385}
386
387impl BootstrapState {
388 #[must_use]
390 pub fn new() -> Self {
391 Self {
392 drained: false,
393 pending_peer_requests: 0,
394 pending_keys: HashSet::new(),
395 capacity_rejected_sources: HashSet::new(),
396 }
397 }
398
399 #[must_use]
406 pub fn is_drained(&self) -> bool {
407 self.drained
408 }
409
410 pub fn remove_key(&mut self, key: &XorName) {
416 self.pending_keys.remove(key);
417 }
418}
419
420impl Default for BootstrapState {
421 fn default() -> Self {
422 Self::new()
423 }
424}
425
426#[cfg(test)]
431mod tests {
432 use std::collections::BinaryHeap;
433
434 use super::*;
435
436 fn peer_id_from_byte(b: u8) -> PeerId {
438 let mut bytes = [0u8; 32];
439 bytes[0] = b;
440 PeerId::from_bytes(bytes)
441 }
442
443 #[test]
446 fn fetch_candidate_nearest_key_has_highest_priority() {
447 let near = FetchCandidate {
448 key: [1u8; 32],
449 distance: [
450 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
451 0, 0, 0, 0,
452 ],
453 sources: vec![peer_id_from_byte(1)],
454 };
455
456 let far = FetchCandidate {
457 key: [2u8; 32],
458 distance: [
459 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
460 0, 0, 0, 0, 0,
461 ],
462 sources: vec![peer_id_from_byte(2)],
463 };
464
465 assert!(near > far, "nearer candidate should compare greater");
468
469 let mut heap = BinaryHeap::new();
470 heap.push(far.clone());
471 heap.push(near.clone());
472
473 assert_eq!(heap.len(), 2, "heap should contain both candidates");
474
475 let first = heap.pop();
476 assert!(first.is_some(), "first pop should succeed");
477 assert_eq!(
478 first.map(|c| c.key),
479 Some(near.key),
480 "nearest key should pop first"
481 );
482
483 let second = heap.pop();
484 assert!(second.is_some(), "second pop should succeed");
485 assert_eq!(
486 second.map(|c| c.key),
487 Some(far.key),
488 "farthest key should pop second"
489 );
490 }
491
492 #[test]
493 fn fetch_candidate_same_distance_and_key_is_equal() {
494 let a = FetchCandidate {
495 key: [1u8; 32],
496 distance: [5u8; 32],
497 sources: vec![],
498 };
499
500 let b = FetchCandidate {
501 key: [1u8; 32],
502 distance: [5u8; 32],
503 sources: vec![],
504 };
505
506 assert_eq!(
507 a.cmp(&b),
508 Ordering::Equal,
509 "same distance + same key should yield Equal"
510 );
511 assert_eq!(a, b, "PartialEq must agree with Ord");
512 }
513
514 #[test]
515 fn fetch_candidate_same_distance_different_key_is_deterministic() {
516 let a = FetchCandidate {
517 key: [1u8; 32],
518 distance: [5u8; 32],
519 sources: vec![],
520 };
521
522 let b = FetchCandidate {
523 key: [2u8; 32],
524 distance: [5u8; 32],
525 sources: vec![],
526 };
527
528 assert_ne!(
529 a.cmp(&b),
530 Ordering::Equal,
531 "same distance + different key must not be Equal"
532 );
533 assert_ne!(a, b, "PartialEq must agree with Ord");
534 }
535
536 #[test]
539 fn peer_sync_record_no_sync_yet() {
540 let record = PeerSyncRecord {
541 last_sync: None,
542 cycles_since_sync: 0,
543 };
544 assert!(
545 !record.has_repair_opportunity(),
546 "never-synced peer has no repair opportunity"
547 );
548 }
549
550 #[test]
551 fn peer_sync_record_synced_but_no_cycle() {
552 let record = PeerSyncRecord {
553 last_sync: Some(Instant::now()),
554 cycles_since_sync: 0,
555 };
556 assert!(
557 !record.has_repair_opportunity(),
558 "synced peer with zero subsequent cycles has no repair opportunity"
559 );
560 }
561
562 #[test]
563 fn peer_sync_record_synced_with_cycle() {
564 let record = PeerSyncRecord {
565 last_sync: Some(Instant::now()),
566 cycles_since_sync: 1,
567 };
568 assert!(
569 record.has_repair_opportunity(),
570 "synced peer with >= 1 cycle should have repair opportunity"
571 );
572 }
573
574 #[test]
575 fn peer_sync_record_no_sync_many_cycles() {
576 let record = PeerSyncRecord {
577 last_sync: None,
578 cycles_since_sync: 10,
579 };
580 assert!(
581 !record.has_repair_opportunity(),
582 "never-synced peer has no repair opportunity regardless of cycle count"
583 );
584 }
585
586 #[test]
589 fn neighbor_sync_empty_cycle_is_immediately_complete() {
590 let state = NeighborSyncState::new_cycle(vec![]);
591 assert!(
592 state.is_cycle_complete(),
593 "empty neighbor list means cycle is complete"
594 );
595 }
596
597 #[test]
598 fn neighbor_sync_new_cycle_not_complete() {
599 let peers = vec![peer_id_from_byte(1), peer_id_from_byte(2)];
600 let state = NeighborSyncState::new_cycle(peers);
601 assert!(
602 !state.is_cycle_complete(),
603 "fresh cycle with peers should not be complete"
604 );
605 }
606
607 #[test]
608 fn neighbor_sync_cycle_completes_when_cursor_reaches_end() {
609 let peers = vec![
610 peer_id_from_byte(1),
611 peer_id_from_byte(2),
612 peer_id_from_byte(3),
613 ];
614 let mut state = NeighborSyncState::new_cycle(peers);
615
616 state.cursor = 2;
618 assert!(
619 !state.is_cycle_complete(),
620 "cursor at len-1 should not be complete"
621 );
622
623 state.cursor = 3;
624 assert!(
625 state.is_cycle_complete(),
626 "cursor at len should be complete"
627 );
628 }
629
630 #[test]
631 fn neighbor_sync_cursor_past_end_is_still_complete() {
632 let peers = vec![peer_id_from_byte(1)];
633 let mut state = NeighborSyncState::new_cycle(peers);
634 state.cursor = 5;
635 assert!(
636 state.is_cycle_complete(),
637 "cursor past end should still report complete"
638 );
639 }
640
641 #[test]
642 fn bootstrap_claim_history_prevents_second_grace_window() {
643 let peer = peer_id_from_byte(9);
644 let mut state = NeighborSyncState::new_cycle(vec![peer]);
645 let first_seen = Instant::now();
646 let grace = Duration::from_secs(60);
647
648 assert_eq!(
649 state.observe_bootstrap_claim(peer, first_seen, grace),
650 BootstrapClaimObservation::WithinGrace { first_seen }
651 );
652 assert!(state.clear_active_bootstrap_claim(&peer));
653 assert!(!state.bootstrap_claims.contains_key(&peer));
654 assert!(state.bootstrap_claim_history.contains_key(&peer));
655
656 assert_eq!(
657 state.observe_bootstrap_claim(peer, first_seen + Duration::from_secs(1), grace),
658 BootstrapClaimObservation::Repeated { first_seen }
659 );
660 assert!(
661 !state.bootstrap_claims.contains_key(&peer),
662 "repeated claims must not recreate an active grace window"
663 );
664 assert_eq!(
665 state.observe_bootstrap_claim(peer, first_seen + Duration::from_secs(2), grace),
666 BootstrapClaimObservation::Repeated { first_seen }
667 );
668 }
669
670 #[test]
671 fn bootstrap_claim_active_window_reports_past_grace() {
672 let peer = peer_id_from_byte(10);
673 let mut state = NeighborSyncState::new_cycle(vec![peer]);
674 let first_seen = Instant::now();
675 let grace = Duration::from_secs(60);
676
677 let _ = state.observe_bootstrap_claim(peer, first_seen, grace);
678
679 assert_eq!(
680 state.observe_bootstrap_claim(peer, first_seen + grace + Duration::from_secs(1), grace),
681 BootstrapClaimObservation::PastGrace { first_seen }
682 );
683 }
684
685 #[test]
688 fn bootstrap_state_initial_not_drained() {
689 let state = BootstrapState::new();
692 assert!(
693 !state.is_drained(),
694 "initial state must not be drained before bootstrap begins"
695 );
696 }
697
698 #[test]
699 fn bootstrap_state_pending_requests_block_drain() {
700 let mut state = BootstrapState::new();
701 state.pending_peer_requests = 3;
702 assert!(
703 !state.is_drained(),
704 "pending peer requests should block drain"
705 );
706 }
707
708 #[test]
709 fn bootstrap_state_pending_keys_block_drain() {
710 let mut state = BootstrapState::new();
711 state.pending_keys.insert([42u8; 32]);
712 assert!(!state.is_drained(), "pending keys should block drain");
713 }
714
715 #[test]
716 fn bootstrap_state_explicit_drained_overrides() {
717 let mut state = BootstrapState::new();
718 state.pending_peer_requests = 5;
719 state.pending_keys.insert([99u8; 32]);
720 state.drained = true;
721 assert!(
722 state.is_drained(),
723 "explicit drained flag should override pending counts"
724 );
725 }
726
727 #[test]
728 fn bootstrap_state_requires_explicit_drain() {
729 let mut state = BootstrapState::new();
730 state.pending_peer_requests = 2;
731 state.pending_keys.insert([1u8; 32]);
732
733 state.pending_peer_requests = 0;
735 state.pending_keys.clear();
736
737 assert!(
738 !state.is_drained(),
739 "clearing counters alone must not drain — requires check_bootstrap_drained"
740 );
741
742 state.drained = true;
744 assert!(state.is_drained(), "explicit flag should drain");
745 }
746
747 #[test]
748 fn bootstrap_state_default_matches_new() {
749 let from_new = BootstrapState::new();
750 let from_default = BootstrapState::default();
751
752 assert_eq!(from_new.drained, from_default.drained);
753 assert_eq!(
754 from_new.pending_peer_requests,
755 from_default.pending_peer_requests
756 );
757 assert_eq!(from_new.pending_keys, from_default.pending_keys);
758 }
759
760 #[test]
765 fn bootstrap_drain_requires_empty_pending_keys() {
766 let key_a: XorName = [0xA0; 32];
767 let key_b: XorName = [0xB0; 32];
768 let key_c: XorName = [0xC0; 32];
769
770 let mut state = BootstrapState::new();
771 state.pending_peer_requests = 0; state.pending_keys = std::iter::once(key_a)
773 .chain(std::iter::once(key_b))
774 .chain(std::iter::once(key_c))
775 .collect();
776
777 assert!(
778 !state.is_drained(),
779 "should NOT be drained while pending_keys still has entries"
780 );
781
782 state.pending_keys.remove(&key_a);
784 assert!(!state.is_drained(), "still not drained with 2 pending keys");
785
786 state.pending_keys.remove(&key_b);
787 assert!(!state.is_drained(), "still not drained with 1 pending key");
788
789 state.pending_keys.remove(&key_c);
790 assert!(
791 !state.is_drained(),
792 "removing all keys is necessary but not sufficient — needs explicit drain"
793 );
794
795 state.drained = true;
797 assert!(state.is_drained(), "explicit drain flag should finalize");
798 }
799
800 #[test]
803 fn verification_state_terminal_variants() {
804 let terminal_states = [
805 VerificationState::QuorumAbandoned,
806 VerificationState::FetchAbandoned,
807 VerificationState::Stored,
808 VerificationState::Idle,
809 ];
810
811 for (i, a) in terminal_states.iter().enumerate() {
813 for (j, b) in terminal_states.iter().enumerate() {
814 if i != j {
815 assert_ne!(
816 a, b,
817 "terminal states at indices {i} and {j} must be distinct"
818 );
819 }
820 }
821 }
822
823 let non_terminal_states = [
825 VerificationState::OfferReceived,
826 VerificationState::PendingVerify,
827 VerificationState::QuorumVerified,
828 VerificationState::PaidListVerified,
829 VerificationState::QueuedForFetch,
830 VerificationState::Fetching,
831 VerificationState::FetchRetryable,
832 VerificationState::QuorumFailed,
833 VerificationState::QuorumInconclusive,
834 ];
835
836 for terminal in &terminal_states {
837 for non_terminal in &non_terminal_states {
838 assert_ne!(
839 terminal, non_terminal,
840 "terminal state {terminal:?} must not equal non-terminal state {non_terminal:?}"
841 );
842 }
843 }
844 }
845
846 #[test]
849 fn repair_opportunity_requires_both_sync_and_cycle() {
850 let synced_no_cycle = PeerSyncRecord {
852 last_sync: Some(
853 Instant::now()
854 .checked_sub(std::time::Duration::from_secs(2))
855 .unwrap_or_else(Instant::now),
856 ),
857 cycles_since_sync: 0,
858 };
859 assert!(
860 !synced_no_cycle.has_repair_opportunity(),
861 "synced with zero subsequent cycles should NOT have repair opportunity"
862 );
863
864 let never_synced = PeerSyncRecord {
866 last_sync: None,
867 cycles_since_sync: 5,
868 };
869 assert!(
870 !never_synced.has_repair_opportunity(),
871 "never-synced peer should NOT have repair opportunity regardless of cycles"
872 );
873
874 let ready = PeerSyncRecord {
876 last_sync: Some(
877 Instant::now()
878 .checked_sub(std::time::Duration::from_secs(5))
879 .unwrap_or_else(Instant::now),
880 ),
881 cycles_since_sync: 1,
882 };
883 assert!(
884 ready.has_repair_opportunity(),
885 "synced peer with >= 1 cycle SHOULD have repair opportunity"
886 );
887 }
888}