1use std::cmp::Ordering;
8use std::collections::{HashMap, HashSet};
9use std::time::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)]
259pub struct NeighborSyncState {
260 pub order: Vec<PeerId>,
262 pub cursor: usize,
264 pub last_sync_times: HashMap<PeerId, Instant>,
266 pub bootstrap_claims: HashMap<PeerId, Instant>,
273}
274
275impl NeighborSyncState {
276 #[must_use]
278 pub fn new_cycle(close_neighbors: Vec<PeerId>) -> Self {
279 Self {
280 order: close_neighbors,
281 cursor: 0,
282 last_sync_times: HashMap::new(),
283 bootstrap_claims: HashMap::new(),
284 }
285 }
286
287 #[must_use]
289 pub fn is_cycle_complete(&self) -> bool {
290 self.cursor >= self.order.len()
291 }
292}
293
294#[derive(Debug)]
300pub struct BootstrapState {
301 pub drained: bool,
303 pub pending_peer_requests: usize,
305 pub pending_keys: HashSet<XorName>,
308}
309
310impl BootstrapState {
311 #[must_use]
313 pub fn new() -> Self {
314 Self {
315 drained: false,
316 pending_peer_requests: 0,
317 pending_keys: HashSet::new(),
318 }
319 }
320
321 #[must_use]
328 pub fn is_drained(&self) -> bool {
329 self.drained
330 }
331
332 pub fn remove_key(&mut self, key: &XorName) {
338 self.pending_keys.remove(key);
339 }
340}
341
342impl Default for BootstrapState {
343 fn default() -> Self {
344 Self::new()
345 }
346}
347
348#[cfg(test)]
353mod tests {
354 use std::collections::BinaryHeap;
355
356 use super::*;
357
358 fn peer_id_from_byte(b: u8) -> PeerId {
360 let mut bytes = [0u8; 32];
361 bytes[0] = b;
362 PeerId::from_bytes(bytes)
363 }
364
365 #[test]
368 fn fetch_candidate_nearest_key_has_highest_priority() {
369 let near = FetchCandidate {
370 key: [1u8; 32],
371 distance: [
372 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,
373 0, 0, 0, 0,
374 ],
375 sources: vec![peer_id_from_byte(1)],
376 };
377
378 let far = FetchCandidate {
379 key: [2u8; 32],
380 distance: [
381 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,
382 0, 0, 0, 0, 0,
383 ],
384 sources: vec![peer_id_from_byte(2)],
385 };
386
387 assert!(near > far, "nearer candidate should compare greater");
390
391 let mut heap = BinaryHeap::new();
392 heap.push(far.clone());
393 heap.push(near.clone());
394
395 assert_eq!(heap.len(), 2, "heap should contain both candidates");
396
397 let first = heap.pop();
398 assert!(first.is_some(), "first pop should succeed");
399 assert_eq!(
400 first.map(|c| c.key),
401 Some(near.key),
402 "nearest key should pop first"
403 );
404
405 let second = heap.pop();
406 assert!(second.is_some(), "second pop should succeed");
407 assert_eq!(
408 second.map(|c| c.key),
409 Some(far.key),
410 "farthest key should pop second"
411 );
412 }
413
414 #[test]
415 fn fetch_candidate_same_distance_and_key_is_equal() {
416 let a = FetchCandidate {
417 key: [1u8; 32],
418 distance: [5u8; 32],
419 sources: vec![],
420 };
421
422 let b = FetchCandidate {
423 key: [1u8; 32],
424 distance: [5u8; 32],
425 sources: vec![],
426 };
427
428 assert_eq!(
429 a.cmp(&b),
430 Ordering::Equal,
431 "same distance + same key should yield Equal"
432 );
433 assert_eq!(a, b, "PartialEq must agree with Ord");
434 }
435
436 #[test]
437 fn fetch_candidate_same_distance_different_key_is_deterministic() {
438 let a = FetchCandidate {
439 key: [1u8; 32],
440 distance: [5u8; 32],
441 sources: vec![],
442 };
443
444 let b = FetchCandidate {
445 key: [2u8; 32],
446 distance: [5u8; 32],
447 sources: vec![],
448 };
449
450 assert_ne!(
451 a.cmp(&b),
452 Ordering::Equal,
453 "same distance + different key must not be Equal"
454 );
455 assert_ne!(a, b, "PartialEq must agree with Ord");
456 }
457
458 #[test]
461 fn peer_sync_record_no_sync_yet() {
462 let record = PeerSyncRecord {
463 last_sync: None,
464 cycles_since_sync: 0,
465 };
466 assert!(
467 !record.has_repair_opportunity(),
468 "never-synced peer has no repair opportunity"
469 );
470 }
471
472 #[test]
473 fn peer_sync_record_synced_but_no_cycle() {
474 let record = PeerSyncRecord {
475 last_sync: Some(Instant::now()),
476 cycles_since_sync: 0,
477 };
478 assert!(
479 !record.has_repair_opportunity(),
480 "synced peer with zero subsequent cycles has no repair opportunity"
481 );
482 }
483
484 #[test]
485 fn peer_sync_record_synced_with_cycle() {
486 let record = PeerSyncRecord {
487 last_sync: Some(Instant::now()),
488 cycles_since_sync: 1,
489 };
490 assert!(
491 record.has_repair_opportunity(),
492 "synced peer with >= 1 cycle should have repair opportunity"
493 );
494 }
495
496 #[test]
497 fn peer_sync_record_no_sync_many_cycles() {
498 let record = PeerSyncRecord {
499 last_sync: None,
500 cycles_since_sync: 10,
501 };
502 assert!(
503 !record.has_repair_opportunity(),
504 "never-synced peer has no repair opportunity regardless of cycle count"
505 );
506 }
507
508 #[test]
511 fn neighbor_sync_empty_cycle_is_immediately_complete() {
512 let state = NeighborSyncState::new_cycle(vec![]);
513 assert!(
514 state.is_cycle_complete(),
515 "empty neighbor list means cycle is complete"
516 );
517 }
518
519 #[test]
520 fn neighbor_sync_new_cycle_not_complete() {
521 let peers = vec![peer_id_from_byte(1), peer_id_from_byte(2)];
522 let state = NeighborSyncState::new_cycle(peers);
523 assert!(
524 !state.is_cycle_complete(),
525 "fresh cycle with peers should not be complete"
526 );
527 }
528
529 #[test]
530 fn neighbor_sync_cycle_completes_when_cursor_reaches_end() {
531 let peers = vec![
532 peer_id_from_byte(1),
533 peer_id_from_byte(2),
534 peer_id_from_byte(3),
535 ];
536 let mut state = NeighborSyncState::new_cycle(peers);
537
538 state.cursor = 2;
540 assert!(
541 !state.is_cycle_complete(),
542 "cursor at len-1 should not be complete"
543 );
544
545 state.cursor = 3;
546 assert!(
547 state.is_cycle_complete(),
548 "cursor at len should be complete"
549 );
550 }
551
552 #[test]
553 fn neighbor_sync_cursor_past_end_is_still_complete() {
554 let peers = vec![peer_id_from_byte(1)];
555 let mut state = NeighborSyncState::new_cycle(peers);
556 state.cursor = 5;
557 assert!(
558 state.is_cycle_complete(),
559 "cursor past end should still report complete"
560 );
561 }
562
563 #[test]
566 fn bootstrap_state_initial_not_drained() {
567 let state = BootstrapState::new();
570 assert!(
571 !state.is_drained(),
572 "initial state must not be drained before bootstrap begins"
573 );
574 }
575
576 #[test]
577 fn bootstrap_state_pending_requests_block_drain() {
578 let mut state = BootstrapState::new();
579 state.pending_peer_requests = 3;
580 assert!(
581 !state.is_drained(),
582 "pending peer requests should block drain"
583 );
584 }
585
586 #[test]
587 fn bootstrap_state_pending_keys_block_drain() {
588 let mut state = BootstrapState::new();
589 state.pending_keys.insert([42u8; 32]);
590 assert!(!state.is_drained(), "pending keys should block drain");
591 }
592
593 #[test]
594 fn bootstrap_state_explicit_drained_overrides() {
595 let mut state = BootstrapState::new();
596 state.pending_peer_requests = 5;
597 state.pending_keys.insert([99u8; 32]);
598 state.drained = true;
599 assert!(
600 state.is_drained(),
601 "explicit drained flag should override pending counts"
602 );
603 }
604
605 #[test]
606 fn bootstrap_state_requires_explicit_drain() {
607 let mut state = BootstrapState::new();
608 state.pending_peer_requests = 2;
609 state.pending_keys.insert([1u8; 32]);
610
611 state.pending_peer_requests = 0;
613 state.pending_keys.clear();
614
615 assert!(
616 !state.is_drained(),
617 "clearing counters alone must not drain — requires check_bootstrap_drained"
618 );
619
620 state.drained = true;
622 assert!(state.is_drained(), "explicit flag should drain");
623 }
624
625 #[test]
626 fn bootstrap_state_default_matches_new() {
627 let from_new = BootstrapState::new();
628 let from_default = BootstrapState::default();
629
630 assert_eq!(from_new.drained, from_default.drained);
631 assert_eq!(
632 from_new.pending_peer_requests,
633 from_default.pending_peer_requests
634 );
635 assert_eq!(from_new.pending_keys, from_default.pending_keys);
636 }
637
638 #[test]
643 fn bootstrap_drain_requires_empty_pending_keys() {
644 let key_a: XorName = [0xA0; 32];
645 let key_b: XorName = [0xB0; 32];
646 let key_c: XorName = [0xC0; 32];
647
648 let mut state = BootstrapState::new();
649 state.pending_peer_requests = 0; state.pending_keys = std::iter::once(key_a)
651 .chain(std::iter::once(key_b))
652 .chain(std::iter::once(key_c))
653 .collect();
654
655 assert!(
656 !state.is_drained(),
657 "should NOT be drained while pending_keys still has entries"
658 );
659
660 state.pending_keys.remove(&key_a);
662 assert!(!state.is_drained(), "still not drained with 2 pending keys");
663
664 state.pending_keys.remove(&key_b);
665 assert!(!state.is_drained(), "still not drained with 1 pending key");
666
667 state.pending_keys.remove(&key_c);
668 assert!(
669 !state.is_drained(),
670 "removing all keys is necessary but not sufficient — needs explicit drain"
671 );
672
673 state.drained = true;
675 assert!(state.is_drained(), "explicit drain flag should finalize");
676 }
677
678 #[test]
681 fn verification_state_terminal_variants() {
682 let terminal_states = [
683 VerificationState::QuorumAbandoned,
684 VerificationState::FetchAbandoned,
685 VerificationState::Stored,
686 VerificationState::Idle,
687 ];
688
689 for (i, a) in terminal_states.iter().enumerate() {
691 for (j, b) in terminal_states.iter().enumerate() {
692 if i != j {
693 assert_ne!(
694 a, b,
695 "terminal states at indices {i} and {j} must be distinct"
696 );
697 }
698 }
699 }
700
701 let non_terminal_states = [
703 VerificationState::OfferReceived,
704 VerificationState::PendingVerify,
705 VerificationState::QuorumVerified,
706 VerificationState::PaidListVerified,
707 VerificationState::QueuedForFetch,
708 VerificationState::Fetching,
709 VerificationState::FetchRetryable,
710 VerificationState::QuorumFailed,
711 VerificationState::QuorumInconclusive,
712 ];
713
714 for terminal in &terminal_states {
715 for non_terminal in &non_terminal_states {
716 assert_ne!(
717 terminal, non_terminal,
718 "terminal state {terminal:?} must not equal non-terminal state {non_terminal:?}"
719 );
720 }
721 }
722 }
723
724 #[test]
727 fn repair_opportunity_requires_both_sync_and_cycle() {
728 let synced_no_cycle = PeerSyncRecord {
730 last_sync: Some(
731 Instant::now()
732 .checked_sub(std::time::Duration::from_secs(2))
733 .unwrap_or_else(Instant::now),
734 ),
735 cycles_since_sync: 0,
736 };
737 assert!(
738 !synced_no_cycle.has_repair_opportunity(),
739 "synced with zero subsequent cycles should NOT have repair opportunity"
740 );
741
742 let never_synced = PeerSyncRecord {
744 last_sync: None,
745 cycles_since_sync: 5,
746 };
747 assert!(
748 !never_synced.has_repair_opportunity(),
749 "never-synced peer should NOT have repair opportunity regardless of cycles"
750 );
751
752 let ready = PeerSyncRecord {
754 last_sync: Some(
755 Instant::now()
756 .checked_sub(std::time::Duration::from_secs(5))
757 .unwrap_or_else(Instant::now),
758 ),
759 cycles_since_sync: 1,
760 };
761 assert!(
762 ready.has_repair_opportunity(),
763 "synced peer with >= 1 cycle SHOULD have repair opportunity"
764 );
765 }
766}