1#![forbid(unsafe_code)]
14#![warn(missing_docs)]
15#![warn(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
16
17use futures::{Stream, StreamExt, future::Either, future::select};
26use std::collections::{BTreeMap, HashMap, HashSet};
27use std::error::Error;
28use std::fmt;
29use std::future::{Future, ready};
30
31pub const DEFAULT_K_VALUE: usize = 20;
33
34pub const DEFAULT_ALPHA_VALUE: usize = 3;
36
37pub const ITERATION_GRACE_TIMEOUT_SECS: u64 = 5;
40
41pub const LOOKUP_TIMEOUT_SECS: u32 = 120;
44
45pub type LookupKey = [u8; 32];
47
48pub trait LookupNode: Clone {
50 fn lookup_peer_id(&self) -> LookupKey;
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct LookupConfig {
57 pub count: usize,
59 pub alpha: usize,
61 pub max_iterations: usize,
63 pub max_candidates: usize,
65}
66
67impl LookupConfig {
68 #[must_use]
70 pub const fn saorsa(count: usize) -> Self {
71 Self {
72 count,
73 alpha: DEFAULT_ALPHA_VALUE,
74 max_iterations: 20,
75 max_candidates: 200,
76 }
77 }
78
79 fn validate(self) -> Result<Self, LookupError> {
80 if self.alpha == 0 {
81 return Err(LookupError::InvalidConfig(
82 "lookup alpha must be greater than zero",
83 ));
84 }
85 if self.max_iterations == 0 {
86 return Err(LookupError::InvalidConfig(
87 "lookup iteration limit must be greater than zero",
88 ));
89 }
90 if self.max_candidates == 0 {
91 return Err(LookupError::InvalidConfig(
92 "lookup candidate limit must be greater than zero",
93 ));
94 }
95 Ok(self)
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum LookupTermination {
102 Converged,
105 Exhausted,
107 IterationLimit,
109 TimedOut,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum LookupProgress {
116 Continue,
118 Finished(LookupTermination),
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum CandidateInsertion {
125 Inserted {
128 evicted: Option<LookupKey>,
130 },
131 Replaced,
133 AlreadyContacted,
135 TooFar,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum LookupPeerState {
142 Waiting,
144 Succeeded,
146 Failed,
148 Unresponsive,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum LookupError {
155 InvalidConfig(&'static str),
157 InvalidState(&'static str),
159 PeerNotWaiting(LookupKey),
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub enum LookupQueryOutcome<N> {
166 Succeeded {
169 responder: LookupKey,
171 candidates: Vec<N>,
173 },
174 Failed {
176 responder: LookupKey,
178 },
179 Unresponsive {
181 responder: LookupKey,
183 },
184}
185
186impl<N> LookupQueryOutcome<N> {
187 #[must_use]
189 pub const fn responder(&self) -> &LookupKey {
190 match self {
191 Self::Succeeded { responder, .. }
192 | Self::Failed { responder }
193 | Self::Unresponsive { responder } => responder,
194 }
195 }
196}
197
198pub trait LookupQuery<N: LookupNode> {
206 type Error;
208
209 fn is_candidate_eligible(
215 &mut self,
216 _candidate: &N,
217 ) -> impl Future<Output = Result<bool, Self::Error>> {
218 ready(Ok(true))
219 }
220
221 fn query_batch(
225 &mut self,
226 target: LookupKey,
227 count: usize,
228 iteration: usize,
229 batch: Vec<N>,
230 ) -> impl Future<Output = Result<Vec<LookupQueryOutcome<N>>, Self::Error>>;
231
232 fn candidate_evicted(
236 &mut self,
237 _peer: LookupKey,
238 ) -> impl Future<Output = Result<(), Self::Error>> {
239 ready(Ok(()))
240 }
241}
242
243pub async fn collect_after_first_with_grace<S, F, G>(mut stream: S, grace: F) -> Vec<S::Item>
258where
259 S: Stream + Unpin,
260 F: FnOnce() -> G,
261 G: Future<Output = ()>,
262{
263 let mut results = Vec::new();
264 let Some(first) = stream.next().await else {
265 return results;
266 };
267 results.push(first);
268
269 let mut grace = Box::pin(grace());
270 while let Either::Left((Some(item), _)) = select(stream.next(), grace.as_mut()).await {
271 results.push(item);
272 }
273 results
274}
275
276#[derive(Debug)]
278pub enum LookupRunError<E> {
279 Lookup(LookupError),
281 Query(E),
283 TimedOut,
285 UnexpectedResponder(LookupKey),
288}
289
290impl<E: fmt::Display> fmt::Display for LookupRunError<E> {
291 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
292 match self {
293 Self::Lookup(error) => write!(formatter, "lookup state error: {error}"),
294 Self::Query(error) => write!(formatter, "lookup query error: {error}"),
295 Self::TimedOut => formatter.write_str("lookup deadline elapsed"),
296 Self::UnexpectedResponder(peer) => write!(
297 formatter,
298 "lookup adapter returned unexpected responder {}",
299 encode_hex(peer)
300 ),
301 }
302 }
303}
304
305impl<E: Error + 'static> Error for LookupRunError<E> {
306 fn source(&self) -> Option<&(dyn Error + 'static)> {
307 match self {
308 Self::Lookup(error) => Some(error),
309 Self::Query(error) => Some(error),
310 Self::UnexpectedResponder(_) | Self::TimedOut => None,
311 }
312 }
313}
314
315impl<E> From<LookupError> for LookupRunError<E> {
316 fn from(error: LookupError) -> Self {
317 Self::Lookup(error)
318 }
319}
320
321impl fmt::Display for LookupError {
322 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
323 match self {
324 Self::InvalidConfig(message) | Self::InvalidState(message) => {
325 formatter.write_str(message)
326 }
327 Self::PeerNotWaiting(peer) => {
328 write!(formatter, "peer {} is not waiting", encode_hex(peer))
329 }
330 }
331 }
332}
333
334impl Error for LookupError {}
335
336#[derive(Debug)]
338pub struct IterativeLookup<N: LookupNode> {
339 target: LookupKey,
340 config: LookupConfig,
341 candidates: BTreeMap<(LookupKey, LookupKey), N>,
342 peer_states: HashMap<LookupKey, LookupPeerState>,
343 query_order: Vec<LookupKey>,
344 in_flight: HashMap<LookupKey, N>,
345 successful: HashMap<LookupKey, N>,
346 previous_top_k: Vec<LookupKey>,
347 iterations: usize,
348 round_active: bool,
349 round_queries: usize,
350 termination: Option<LookupTermination>,
351}
352
353impl<N: LookupNode> IterativeLookup<N> {
354 pub fn new(target: LookupKey, config: LookupConfig) -> Result<Self, LookupError> {
356 Ok(Self {
357 target,
358 config: config.validate()?,
359 candidates: BTreeMap::new(),
360 peer_states: HashMap::new(),
361 query_order: Vec::new(),
362 in_flight: HashMap::new(),
363 successful: HashMap::new(),
364 previous_top_k: Vec::new(),
365 iterations: 0,
366 round_active: false,
367 round_queries: 0,
368 termination: (config.count == 0).then_some(LookupTermination::Converged),
369 })
370 }
371
372 #[must_use]
374 pub const fn target(&self) -> LookupKey {
375 self.target
376 }
377
378 #[must_use]
380 pub const fn config(&self) -> LookupConfig {
381 self.config
382 }
383
384 #[must_use]
386 pub const fn iterations(&self) -> usize {
387 self.iterations
388 }
389
390 #[must_use]
392 pub const fn termination(&self) -> Option<LookupTermination> {
393 self.termination
394 }
395
396 #[must_use]
398 pub const fn round_active(&self) -> bool {
399 self.round_active
400 }
401
402 #[must_use]
404 pub fn peer_state(&self, peer: &LookupKey) -> Option<LookupPeerState> {
405 self.peer_states.get(peer).copied()
406 }
407
408 #[must_use]
410 pub fn is_contactable(&self, peer: &LookupKey) -> bool {
411 !self.peer_states.contains_key(peer)
412 }
413
414 #[must_use]
416 pub fn queried_peers(&self) -> &[LookupKey] {
417 &self.query_order
418 }
419
420 pub fn add_known_result(&mut self, node: N) {
423 let peer = node.lookup_peer_id();
424 self.candidates.retain(|(_, id), _| *id != peer);
425 self.peer_states.insert(peer, LookupPeerState::Succeeded);
426 self.successful.entry(peer).or_insert(node);
427 }
428
429 pub fn add_candidate(&mut self, node: N) -> CandidateInsertion {
431 let peer = node.lookup_peer_id();
432 if !self.is_contactable(&peer) {
433 return CandidateInsertion::AlreadyContacted;
434 }
435
436 let candidate_key = (xor_distance(&peer, &self.target), peer);
437 if let std::collections::btree_map::Entry::Occupied(mut entry) =
438 self.candidates.entry(candidate_key)
439 {
440 entry.insert(node);
441 return CandidateInsertion::Replaced;
442 }
443
444 if self.candidates.len() >= self.config.max_candidates {
445 let Some(farthest_key) = self.candidates.keys().next_back().copied() else {
446 return CandidateInsertion::TooFar;
447 };
448 if candidate_key >= farthest_key {
449 return CandidateInsertion::TooFar;
450 }
451 self.candidates.remove(&farthest_key);
452 self.candidates.insert(candidate_key, node);
453 return CandidateInsertion::Inserted {
454 evicted: Some(farthest_key.1),
455 };
456 }
457
458 self.candidates.insert(candidate_key, node);
459 CandidateInsertion::Inserted { evicted: None }
460 }
461
462 pub fn begin_round(&mut self) -> Result<LookupProgress, LookupError> {
464 if self.round_active {
465 return Err(LookupError::InvalidState(
466 "cannot begin a lookup round while another round is active",
467 ));
468 }
469 if let Some(reason) = self.termination {
470 return Ok(LookupProgress::Finished(reason));
471 }
472 if self.iterations >= self.config.max_iterations {
473 return Ok(self.finish(LookupTermination::IterationLimit));
474 }
475 self.discard_contacted_candidates();
476 if self.candidates.is_empty() {
477 return Ok(self.finish(LookupTermination::Exhausted));
478 }
479
480 self.iterations += 1;
481 self.round_queries = 0;
482 self.round_active = true;
483 Ok(LookupProgress::Continue)
484 }
485
486 pub fn take_next_candidate(&mut self) -> Result<Option<N>, LookupError> {
491 if !self.round_active {
492 return Err(LookupError::InvalidState(
493 "cannot select a candidate outside an active lookup round",
494 ));
495 }
496 if self.round_queries >= self.config.alpha {
497 return Ok(None);
498 }
499 self.discard_contacted_candidates();
500 Ok(self.candidates.pop_first().map(|(_, node)| node))
501 }
502
503 pub fn mark_waiting(&mut self, node: N) -> Result<(), LookupError> {
505 if !self.round_active {
506 return Err(LookupError::InvalidState(
507 "cannot start a query outside an active lookup round",
508 ));
509 }
510 if self.round_queries >= self.config.alpha {
511 return Err(LookupError::InvalidState(
512 "lookup round already reached its alpha limit",
513 ));
514 }
515 let peer = node.lookup_peer_id();
516 if !self.is_contactable(&peer) {
517 return Err(LookupError::InvalidState(
518 "cannot query a peer that already has lookup state",
519 ));
520 }
521 self.peer_states.insert(peer, LookupPeerState::Waiting);
522 self.query_order.push(peer);
523 self.in_flight.insert(peer, node);
524 self.round_queries += 1;
525 Ok(())
526 }
527
528 pub fn record_success(&mut self, peer: &LookupKey) -> Result<(), LookupError> {
530 let node = self.take_waiting(peer)?;
531 self.peer_states.insert(*peer, LookupPeerState::Succeeded);
532 self.successful.entry(*peer).or_insert(node);
533 Ok(())
534 }
535
536 pub fn record_failure(&mut self, peer: &LookupKey) -> Result<(), LookupError> {
538 self.take_waiting(peer)?;
539 self.peer_states.insert(*peer, LookupPeerState::Failed);
540 Ok(())
541 }
542
543 pub fn record_unresponsive(&mut self, peer: &LookupKey) -> Result<(), LookupError> {
545 self.take_waiting(peer)?;
546 self.peer_states
547 .insert(*peer, LookupPeerState::Unresponsive);
548 Ok(())
549 }
550
551 #[must_use]
553 pub fn waiting_peers(&self) -> Vec<LookupKey> {
554 let mut peers = self.in_flight.keys().copied().collect::<Vec<_>>();
555 peers.sort_by_key(|peer| (xor_distance(peer, &self.target), *peer));
556 peers
557 }
558
559 pub fn complete_round(&mut self) -> Result<LookupProgress, LookupError> {
561 if !self.round_active {
562 return Err(LookupError::InvalidState(
563 "cannot complete a lookup round when none is active",
564 ));
565 }
566 if !self.in_flight.is_empty() {
567 return Err(LookupError::InvalidState(
568 "cannot complete a lookup round with queries still waiting",
569 ));
570 }
571 self.round_active = false;
572
573 let current_top_k = self.result_peer_ids();
574 if current_top_k == self.previous_top_k {
575 if current_top_k.len() < self.config.count && !self.candidates.is_empty() {
576 self.previous_top_k = current_top_k;
577 return Ok(LookupProgress::Continue);
578 }
579 let has_promising_candidate = self.has_promising_candidate();
580 if !has_promising_candidate {
581 return Ok(self.finish(LookupTermination::Converged));
582 }
583 }
584 self.previous_top_k = current_top_k;
585
586 if self.iterations >= self.config.max_iterations {
587 return Ok(self.finish(LookupTermination::IterationLimit));
588 }
589 self.discard_contacted_candidates();
590 if self.candidates.is_empty() {
591 return Ok(self.finish(LookupTermination::Exhausted));
592 }
593 Ok(LookupProgress::Continue)
594 }
595
596 #[must_use]
598 pub fn results(&self) -> Vec<N> {
599 let mut nodes = self.successful.values().cloned().collect::<Vec<_>>();
600 nodes.sort_by_key(|node| {
601 let peer = node.lookup_peer_id();
602 (xor_distance(&peer, &self.target), peer)
603 });
604 nodes.truncate(self.config.count);
605 nodes
606 }
607
608 fn take_waiting(&mut self, peer: &LookupKey) -> Result<N, LookupError> {
609 if self.peer_states.get(peer) != Some(&LookupPeerState::Waiting) {
610 return Err(LookupError::PeerNotWaiting(*peer));
611 }
612 self.in_flight
613 .remove(peer)
614 .ok_or(LookupError::PeerNotWaiting(*peer))
615 }
616
617 fn result_peer_ids(&self) -> Vec<LookupKey> {
618 self.results()
619 .into_iter()
620 .map(|node| node.lookup_peer_id())
621 .collect()
622 }
623
624 fn has_promising_candidate(&self) -> bool {
625 let Some(worst_result) = self.result_peer_ids().last().copied() else {
626 return !self.candidates.is_empty();
627 };
628 let worst_distance = xor_distance(&worst_result, &self.target);
629 self.candidates
630 .keys()
631 .next()
632 .is_some_and(|(distance, _)| *distance < worst_distance)
633 }
634
635 fn discard_contacted_candidates(&mut self) {
636 self.candidates
637 .retain(|(_, peer), _| !self.peer_states.contains_key(peer));
638 }
639
640 fn finish(&mut self, reason: LookupTermination) -> LookupProgress {
641 self.round_active = false;
642 self.termination = Some(reason);
643 LookupProgress::Finished(reason)
644 }
645}
646
647pub async fn run_iterative_lookup<N, Q, D>(
663 lookup: &mut IterativeLookup<N>,
664 query: &mut Q,
665 deadline: D,
666) -> Result<LookupTermination, LookupRunError<Q::Error>>
667where
668 N: LookupNode,
669 Q: LookupQuery<N>,
670 D: Future<Output = ()>,
671{
672 if lookup.termination() == Some(LookupTermination::TimedOut) {
673 return Err(LookupRunError::TimedOut);
674 }
675 let result = {
676 let walk = Box::pin(run_lookup_rounds(lookup, query));
677 match select(walk, Box::pin(deadline)).await {
678 Either::Left((result, _)) => Some(result),
679 Either::Right(((), _walk)) => None,
680 }
681 };
682 match result {
683 Some(result) => result,
684 None => {
685 for (peer, _) in lookup.in_flight.drain() {
686 lookup
687 .peer_states
688 .insert(peer, LookupPeerState::Unresponsive);
689 }
690 lookup.finish(LookupTermination::TimedOut);
691 Err(LookupRunError::TimedOut)
692 }
693 }
694}
695
696async fn run_lookup_rounds<N, Q>(
697 lookup: &mut IterativeLookup<N>,
698 query: &mut Q,
699) -> Result<LookupTermination, LookupRunError<Q::Error>>
700where
701 N: LookupNode,
702 Q: LookupQuery<N>,
703{
704 loop {
705 match lookup.begin_round()? {
706 LookupProgress::Continue => {}
707 LookupProgress::Finished(reason) => return Ok(reason),
708 }
709
710 let mut batch = Vec::new();
711 while let Some(candidate) = lookup.take_next_candidate()? {
712 if query
713 .is_candidate_eligible(&candidate)
714 .await
715 .map_err(LookupRunError::Query)?
716 {
717 lookup.mark_waiting(candidate.clone())?;
718 batch.push(candidate);
719 }
720 }
721
722 if batch.is_empty() {
723 match lookup.complete_round()? {
724 LookupProgress::Continue => continue,
725 LookupProgress::Finished(reason) => return Ok(reason),
726 }
727 }
728
729 let mut awaiting = batch
730 .iter()
731 .map(LookupNode::lookup_peer_id)
732 .collect::<HashSet<_>>();
733 let outcomes = query
734 .query_batch(
735 lookup.target(),
736 lookup.config().count,
737 lookup.iterations(),
738 batch,
739 )
740 .await
741 .map_err(LookupRunError::Query)?;
742
743 for outcome in outcomes {
744 let responder = *outcome.responder();
745 if !awaiting.remove(&responder) {
746 return Err(LookupRunError::UnexpectedResponder(responder));
747 }
748 match outcome {
749 LookupQueryOutcome::Succeeded {
750 responder,
751 candidates,
752 } => {
753 lookup.record_success(&responder)?;
754 for candidate in candidates {
755 if !query
756 .is_candidate_eligible(&candidate)
757 .await
758 .map_err(LookupRunError::Query)?
759 {
760 continue;
761 }
762 if let CandidateInsertion::Inserted {
763 evicted: Some(evicted),
764 } = lookup.add_candidate(candidate)
765 {
766 query
767 .candidate_evicted(evicted)
768 .await
769 .map_err(LookupRunError::Query)?;
770 }
771 }
772 }
773 LookupQueryOutcome::Failed { responder } => {
774 lookup.record_failure(&responder)?;
775 }
776 LookupQueryOutcome::Unresponsive { responder } => {
777 lookup.record_unresponsive(&responder)?;
778 }
779 }
780 }
781
782 for responder in awaiting {
783 lookup.record_unresponsive(&responder)?;
784 }
785
786 match lookup.complete_round()? {
787 LookupProgress::Continue => {}
788 LookupProgress::Finished(reason) => return Ok(reason),
789 }
790 }
791}
792
793#[must_use]
795pub fn xor_distance(left: &LookupKey, right: &LookupKey) -> LookupKey {
796 let mut distance = [0u8; 32];
797 for (index, output) in distance.iter_mut().enumerate() {
798 *output = left[index] ^ right[index];
799 }
800 distance
801}
802
803fn encode_hex(bytes: &[u8]) -> String {
804 const HEX: &[u8; 16] = b"0123456789abcdef";
805 let mut output = String::with_capacity(bytes.len() * 2);
806 for byte in bytes {
807 output.push(char::from(HEX[usize::from(byte >> 4)]));
808 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
809 }
810 output
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816 use futures::stream::FuturesUnordered;
817 use std::convert::Infallible;
818 use std::pin::Pin;
819
820 #[derive(Debug, Clone, PartialEq, Eq)]
821 struct Node(LookupKey);
822
823 impl LookupNode for Node {
824 fn lookup_peer_id(&self) -> LookupKey {
825 self.0
826 }
827 }
828
829 fn node(last: u8) -> Node {
830 let mut peer = [0; 32];
831 peer[31] = last;
832 Node(peer)
833 }
834
835 fn peer(last: u8) -> LookupKey {
836 node(last).0
837 }
838
839 fn start_batch(lookup: &mut IterativeLookup<Node>) -> Vec<Node> {
840 assert_eq!(
841 lookup.begin_round().expect("begin round"),
842 LookupProgress::Continue
843 );
844 let mut batch = Vec::new();
845 while let Some(candidate) = lookup.take_next_candidate().expect("take candidate") {
846 lookup
847 .mark_waiting(candidate.clone())
848 .expect("mark waiting");
849 batch.push(candidate);
850 }
851 batch
852 }
853
854 #[test]
855 fn orders_batches_by_xor_distance_and_enforces_alpha() {
856 let mut lookup =
857 IterativeLookup::new([0; 32], LookupConfig::saorsa(20)).expect("valid lookup");
858 for id in [9, 1, 7, 2, 3] {
859 lookup.add_candidate(node(id));
860 }
861
862 let batch = start_batch(&mut lookup);
863 assert_eq!(batch, vec![node(1), node(2), node(3)]);
864 }
865
866 #[test]
867 fn failed_and_unresponsive_peers_cannot_be_reintroduced() {
868 let mut lookup =
869 IterativeLookup::new([0; 32], LookupConfig::saorsa(3)).expect("valid lookup");
870 lookup.add_candidate(node(1));
871 lookup.add_candidate(node(2));
872 let batch = start_batch(&mut lookup);
873 lookup.record_failure(&batch[0].0).expect("record failure");
874 lookup
875 .record_unresponsive(&batch[1].0)
876 .expect("record timeout");
877
878 assert_eq!(
879 lookup.add_candidate(node(1)),
880 CandidateInsertion::AlreadyContacted
881 );
882 assert_eq!(
883 lookup.add_candidate(node(2)),
884 CandidateInsertion::AlreadyContacted
885 );
886 assert!(!lookup.is_contactable(&peer(1)));
887 assert!(!lookup.is_contactable(&peer(2)));
888 }
889
890 #[test]
891 fn bounded_queue_evicts_only_a_farther_candidate() {
892 let config = LookupConfig {
893 max_candidates: 2,
894 ..LookupConfig::saorsa(2)
895 };
896 let mut lookup = IterativeLookup::new([0; 32], config).expect("valid lookup");
897 lookup.add_candidate(node(10));
898 lookup.add_candidate(node(20));
899 assert_eq!(
900 lookup.add_candidate(node(5)),
901 CandidateInsertion::Inserted {
902 evicted: Some(peer(20))
903 }
904 );
905 assert_eq!(lookup.add_candidate(node(30)), CandidateInsertion::TooFar);
906 }
907
908 #[test]
909 fn runs_a_multi_round_lookup_to_exhaustion() {
910 let mut lookup =
911 IterativeLookup::new([0; 32], LookupConfig::saorsa(3)).expect("valid lookup");
912 for id in [30, 40, 50] {
913 lookup.add_candidate(node(id));
914 }
915
916 let first = start_batch(&mut lookup);
917 for candidate in first {
918 lookup.record_success(&candidate.0).expect("record success");
919 }
920 lookup.add_candidate(node(10));
921 lookup.add_candidate(node(20));
922 assert_eq!(
923 lookup.complete_round().expect("complete first"),
924 LookupProgress::Continue
925 );
926
927 let second = start_batch(&mut lookup);
928 assert_eq!(second, vec![node(10), node(20)]);
929 for candidate in second {
930 lookup.record_success(&candidate.0).expect("record success");
931 }
932 assert_eq!(
933 lookup.complete_round().expect("complete second"),
934 LookupProgress::Finished(LookupTermination::Exhausted)
935 );
936 assert_eq!(lookup.results(), vec![node(10), node(20), node(30)]);
937 }
938
939 #[test]
940 fn unchanged_top_k_converges_when_only_farther_candidates_remain() {
941 let config = LookupConfig {
942 alpha: 1,
943 ..LookupConfig::saorsa(1)
944 };
945 let mut lookup = IterativeLookup::new([0; 32], config).expect("valid lookup");
946 lookup.add_candidate(node(10));
947 lookup.add_candidate(node(30));
948 lookup.add_candidate(node(40));
949
950 let first = start_batch(&mut lookup);
951 lookup.record_success(&first[0].0).expect("record success");
952 assert_eq!(
953 lookup.complete_round().expect("complete first"),
954 LookupProgress::Continue
955 );
956
957 let second = start_batch(&mut lookup);
958 lookup.record_success(&second[0].0).expect("record success");
959 assert_eq!(
960 lookup.complete_round().expect("complete second"),
961 LookupProgress::Finished(LookupTermination::Converged)
962 );
963 assert_eq!(lookup.results(), vec![node(10)]);
964 }
965
966 #[derive(Default)]
967 struct MockQuery {
968 batches: Vec<Vec<LookupKey>>,
969 }
970
971 impl LookupQuery<Node> for MockQuery {
972 type Error = Infallible;
973
974 fn query_batch(
975 &mut self,
976 _target: LookupKey,
977 _count: usize,
978 iteration: usize,
979 batch: Vec<Node>,
980 ) -> impl Future<Output = Result<Vec<LookupQueryOutcome<Node>>, Self::Error>> {
981 self.batches
982 .push(batch.iter().map(LookupNode::lookup_peer_id).collect());
983 let outcomes = if iteration == 1 {
984 vec![
985 LookupQueryOutcome::Succeeded {
986 responder: peer(1),
987 candidates: vec![node(0)],
988 },
989 LookupQueryOutcome::Failed { responder: peer(2) },
990 ]
991 } else {
992 batch
993 .into_iter()
994 .map(|candidate| LookupQueryOutcome::Succeeded {
995 responder: candidate.lookup_peer_id(),
996 candidates: Vec::new(),
997 })
998 .collect()
999 };
1000 ready(Ok(outcomes))
1001 }
1002 }
1003
1004 #[test]
1005 fn shared_runner_owns_rounds_and_drives_query_batches() {
1006 let config = LookupConfig {
1007 alpha: 2,
1008 ..LookupConfig::saorsa(2)
1009 };
1010 let mut lookup = IterativeLookup::new([0; 32], config).expect("valid lookup");
1011 for id in [3, 1, 2] {
1012 lookup.add_candidate(node(id));
1013 }
1014 let mut query = MockQuery::default();
1015
1016 let reason = futures::executor::block_on(run_iterative_lookup(
1017 &mut lookup,
1018 &mut query,
1019 std::future::pending(),
1020 ))
1021 .expect("run lookup");
1022
1023 assert_eq!(reason, LookupTermination::Exhausted);
1024 assert_eq!(
1025 query.batches,
1026 vec![vec![peer(1), peer(2)], vec![peer(0), peer(3)]]
1027 );
1028 assert_eq!(lookup.results(), vec![node(0), node(1)]);
1029 }
1030
1031 struct MissingOutcomeQuery;
1032
1033 impl LookupQuery<Node> for MissingOutcomeQuery {
1034 type Error = Infallible;
1035
1036 fn query_batch(
1037 &mut self,
1038 _target: LookupKey,
1039 _count: usize,
1040 _iteration: usize,
1041 batch: Vec<Node>,
1042 ) -> impl Future<Output = Result<Vec<LookupQueryOutcome<Node>>, Self::Error>> {
1043 ready(Ok(batch
1044 .first()
1045 .map(|candidate| LookupQueryOutcome::Succeeded {
1046 responder: candidate.lookup_peer_id(),
1047 candidates: Vec::new(),
1048 })
1049 .into_iter()
1050 .collect()))
1051 }
1052 }
1053
1054 #[test]
1055 fn shared_runner_marks_missing_batch_outcomes_unresponsive() {
1056 let config = LookupConfig {
1057 alpha: 2,
1058 ..LookupConfig::saorsa(2)
1059 };
1060 let mut lookup = IterativeLookup::new([0; 32], config).expect("valid lookup");
1061 lookup.add_candidate(node(1));
1062 lookup.add_candidate(node(2));
1063
1064 futures::executor::block_on(run_iterative_lookup(
1065 &mut lookup,
1066 &mut MissingOutcomeQuery,
1067 std::future::pending(),
1068 ))
1069 .expect("run lookup");
1070
1071 assert_eq!(
1072 lookup.peer_state(&peer(1)),
1073 Some(LookupPeerState::Succeeded)
1074 );
1075 assert_eq!(
1076 lookup.peer_state(&peer(2)),
1077 Some(LookupPeerState::Unresponsive)
1078 );
1079 }
1080
1081 #[test]
1082 fn grace_collector_cancels_pending_stragglers_after_first_result() {
1083 let queries: FuturesUnordered<Pin<Box<dyn Future<Output = u8>>>> = FuturesUnordered::new();
1084 queries.push(Box::pin(ready(7)));
1085 queries.push(Box::pin(std::future::pending()));
1086
1087 let results =
1088 futures::executor::block_on(collect_after_first_with_grace(queries, || ready(())));
1089
1090 assert_eq!(results, vec![7]);
1091 }
1092
1093 #[test]
1094 fn grace_collector_keeps_completed_batch_results() {
1095 let queries = futures::stream::iter([1_u8, 2_u8, 3_u8]);
1096
1097 let results = futures::executor::block_on(collect_after_first_with_grace(queries, || {
1098 std::future::pending::<()>()
1099 }));
1100
1101 assert_eq!(results, vec![1, 2, 3]);
1102 }
1103
1104 #[test]
1105 fn zero_count_completes_without_querying_even_with_candidates() {
1106 let mut lookup = IterativeLookup::new([0; 32], LookupConfig::saorsa(0)).unwrap();
1107 lookup.add_known_result(node(1));
1108 lookup.add_candidate(node(2));
1109 let mut query = MockQuery::default();
1110 let reason = futures::executor::block_on(run_iterative_lookup(
1111 &mut lookup,
1112 &mut query,
1113 std::future::pending(),
1114 ))
1115 .unwrap();
1116
1117 assert_eq!(reason, LookupTermination::Converged);
1118 assert!(lookup.results().is_empty());
1119 assert!(lookup.queried_peers().is_empty());
1120 assert!(query.batches.is_empty());
1121 assert_eq!(lookup.iterations(), 0);
1122 }
1123
1124 struct StalledQuery<'a> {
1125 eligibility_stalls: bool,
1126 cancelled: &'a std::cell::Cell<bool>,
1127 }
1128
1129 struct CancellationFlag<'a>(&'a std::cell::Cell<bool>);
1130
1131 impl Drop for CancellationFlag<'_> {
1132 fn drop(&mut self) {
1133 self.0.set(true);
1134 }
1135 }
1136
1137 impl LookupQuery<Node> for StalledQuery<'_> {
1138 type Error = Infallible;
1139
1140 async fn is_candidate_eligible(&mut self, _: &Node) -> Result<bool, Self::Error> {
1141 if self.eligibility_stalls {
1142 let _flag = CancellationFlag(self.cancelled);
1143 std::future::pending::<()>().await;
1144 }
1145 Ok(true)
1146 }
1147
1148 async fn query_batch(
1149 &mut self,
1150 _: LookupKey,
1151 _: usize,
1152 _: usize,
1153 _: Vec<Node>,
1154 ) -> Result<Vec<LookupQueryOutcome<Node>>, Self::Error> {
1155 let _flag = CancellationFlag(self.cancelled);
1156 let queries = futures::stream::pending::<LookupQueryOutcome<Node>>();
1158 Ok(collect_after_first_with_grace(queries, || ready(())).await)
1159 }
1160 }
1161
1162 #[test]
1163 fn deadline_cancels_stalled_eligibility_and_all_pending_batches() {
1164 for eligibility_stalls in [true, false] {
1165 let mut lookup = IterativeLookup::new([0; 32], LookupConfig::saorsa(2)).unwrap();
1166 lookup.add_known_result(node(0));
1167 lookup.add_candidate(node(1));
1168 let cancelled = std::cell::Cell::new(false);
1169 let mut query = StalledQuery {
1170 eligibility_stalls,
1171 cancelled: &cancelled,
1172 };
1173 let result = futures::executor::block_on(run_iterative_lookup(
1174 &mut lookup,
1175 &mut query,
1176 ready(()),
1177 ));
1178 assert!(matches!(result, Err(LookupRunError::TimedOut)));
1179 assert!(cancelled.get());
1180 assert_eq!(lookup.termination(), Some(LookupTermination::TimedOut));
1181 assert!(!lookup.round_active());
1182 assert!(lookup.in_flight.is_empty());
1183 assert_eq!(lookup.results(), vec![node(0)]);
1184 if !eligibility_stalls {
1185 assert_eq!(
1186 lookup.peer_state(&peer(1)),
1187 Some(LookupPeerState::Unresponsive)
1188 );
1189 }
1190 }
1191 }
1192
1193 #[test]
1194 fn rejects_zero_limits() {
1195 let invalid = LookupConfig {
1196 alpha: 0,
1197 ..LookupConfig::saorsa(1)
1198 };
1199 assert!(matches!(
1200 IterativeLookup::<Node>::new([0; 32], invalid),
1201 Err(LookupError::InvalidConfig(_))
1202 ));
1203 }
1204}