2026-02-12 09:13:50 +00:00
|
|
|
|
//! Kademlia iterative FIND_NODE lookup.
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! A state machine that drives the iterative lookup process:
|
|
|
|
|
|
//! 1. Start with the α closest nodes from the local routing table.
|
|
|
|
|
|
//! 2. Query them in parallel (caller dispatches the actual I/O).
|
|
|
|
|
|
//! 3. Incorporate responses (closer nodes discovered).
|
|
|
|
|
|
//! 4. Repeat until the k closest nodes have all been queried or max rounds exceeded.
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! The lookup does NOT do I/O — it produces `LookupAction`s that the caller
|
|
|
|
|
|
//! translates into real network requests.
|
|
|
|
|
|
|
|
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
|
|
|
|
|
|
|
|
use crate::types::NodeId;
|
|
|
|
|
|
use super::routing_table::{RoutingTable, K};
|
|
|
|
|
|
|
|
|
|
|
|
/// Concurrency parameter — how many queries to issue in parallel per round.
|
|
|
|
|
|
pub const ALPHA: usize = 3;
|
|
|
|
|
|
|
|
|
|
|
|
/// Maximum lookup rounds before termination.
|
|
|
|
|
|
const MAX_ROUNDS: usize = 20;
|
|
|
|
|
|
|
|
|
|
|
|
/// Actions produced by the lookup state machine.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub enum LookupAction {
|
|
|
|
|
|
/// Send a FIND_NODE query to this node.
|
2026-02-15 09:47:05 +00:00
|
|
|
|
Query { node_id: NodeId },
|
2026-02-12 09:13:50 +00:00
|
|
|
|
/// The lookup is complete — here are the k closest nodes found.
|
2026-02-15 09:47:05 +00:00
|
|
|
|
Done { closest: Vec<NodeId> },
|
2026-02-12 09:13:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// State of a single iterative FIND_NODE lookup.
|
|
|
|
|
|
pub struct NodeLookup {
|
|
|
|
|
|
target: NodeId,
|
|
|
|
|
|
k: usize,
|
|
|
|
|
|
alpha: usize,
|
|
|
|
|
|
/// All nodes discovered during the lookup, with their distances.
|
2026-02-15 09:47:05 +00:00
|
|
|
|
known: HashMap<NodeId, [u8; 32]>,
|
2026-02-12 09:13:50 +00:00
|
|
|
|
/// Nodes we've already queried.
|
|
|
|
|
|
queried: HashSet<NodeId>,
|
|
|
|
|
|
/// Nodes we've sent queries to but haven't received responses yet.
|
|
|
|
|
|
pending: HashSet<NodeId>,
|
|
|
|
|
|
round: usize,
|
|
|
|
|
|
done: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl NodeLookup {
|
|
|
|
|
|
/// Start a new lookup for `target` using the local routing table as seeds.
|
|
|
|
|
|
pub fn start(target: NodeId, routing_table: &RoutingTable) -> (Self, Vec<LookupAction>) {
|
|
|
|
|
|
Self::start_with_params(target, routing_table, K, ALPHA)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Start with custom k and alpha parameters.
|
|
|
|
|
|
pub fn start_with_params(
|
|
|
|
|
|
target: NodeId,
|
|
|
|
|
|
routing_table: &RoutingTable,
|
|
|
|
|
|
k: usize,
|
|
|
|
|
|
alpha: usize,
|
|
|
|
|
|
) -> (Self, Vec<LookupAction>) {
|
|
|
|
|
|
let seeds = routing_table.closest(&target, k);
|
|
|
|
|
|
|
|
|
|
|
|
let mut known = HashMap::new();
|
|
|
|
|
|
for entry in &seeds {
|
|
|
|
|
|
let dist = entry.node_id.xor_distance(&target);
|
2026-02-15 09:47:05 +00:00
|
|
|
|
known.insert(entry.node_id, dist);
|
2026-02-12 09:13:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut lookup = Self {
|
|
|
|
|
|
target,
|
|
|
|
|
|
k,
|
|
|
|
|
|
alpha,
|
|
|
|
|
|
known,
|
|
|
|
|
|
queried: HashSet::new(),
|
|
|
|
|
|
pending: HashSet::new(),
|
|
|
|
|
|
round: 0,
|
|
|
|
|
|
done: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let actions = lookup.next_round();
|
|
|
|
|
|
(lookup, actions)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Feed a response from a queried node. Returns new actions (more queries, or done).
|
|
|
|
|
|
pub fn handle_response(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
from: NodeId,
|
2026-02-15 09:47:05 +00:00
|
|
|
|
closer_nodes: Vec<NodeId>,
|
2026-02-12 09:13:50 +00:00
|
|
|
|
) -> Vec<LookupAction> {
|
|
|
|
|
|
if self.done {
|
|
|
|
|
|
return vec![self.done_action()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.pending.remove(&from);
|
|
|
|
|
|
|
|
|
|
|
|
// Incorporate newly discovered nodes
|
2026-02-15 09:47:05 +00:00
|
|
|
|
for node_id in closer_nodes {
|
2026-02-12 09:13:50 +00:00
|
|
|
|
if node_id == self.target {
|
|
|
|
|
|
// Skip the target itself (it's what we're looking for)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.known.entry(node_id).or_insert_with(|| {
|
2026-02-15 09:47:05 +00:00
|
|
|
|
node_id.xor_distance(&self.target)
|
2026-02-12 09:13:50 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If no more pending queries, start the next round
|
|
|
|
|
|
if self.pending.is_empty() {
|
|
|
|
|
|
return self.next_round();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vec::new()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Handle a timeout or failure for a queried node.
|
|
|
|
|
|
pub fn handle_failure(&mut self, node_id: NodeId) -> Vec<LookupAction> {
|
|
|
|
|
|
self.pending.remove(&node_id);
|
|
|
|
|
|
if self.pending.is_empty() && !self.done {
|
|
|
|
|
|
return self.next_round();
|
|
|
|
|
|
}
|
|
|
|
|
|
Vec::new()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Is the lookup complete?
|
|
|
|
|
|
pub fn is_done(&self) -> bool {
|
|
|
|
|
|
self.done
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn next_round(&mut self) -> Vec<LookupAction> {
|
|
|
|
|
|
self.round += 1;
|
|
|
|
|
|
|
|
|
|
|
|
if self.round > MAX_ROUNDS {
|
|
|
|
|
|
self.done = true;
|
|
|
|
|
|
return vec![self.done_action()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find the closest unqueried nodes
|
|
|
|
|
|
let mut candidates: Vec<_> = self
|
|
|
|
|
|
.known
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|(id, _)| !self.queried.contains(id))
|
2026-02-15 09:47:05 +00:00
|
|
|
|
.map(|(id, dist)| (*id, *dist))
|
2026-02-12 09:13:50 +00:00
|
|
|
|
.collect();
|
|
|
|
|
|
|
2026-02-15 09:47:05 +00:00
|
|
|
|
candidates.sort_by(|a, b| a.1.cmp(&b.1));
|
2026-02-12 09:13:50 +00:00
|
|
|
|
candidates.truncate(self.alpha);
|
|
|
|
|
|
|
|
|
|
|
|
if candidates.is_empty() {
|
|
|
|
|
|
// No more nodes to query — we're done
|
|
|
|
|
|
self.done = true;
|
|
|
|
|
|
return vec![self.done_action()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check termination: if all k closest nodes have been queried
|
|
|
|
|
|
let all_known_sorted = self.k_closest();
|
|
|
|
|
|
let all_k_queried = all_known_sorted
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.take(self.k)
|
2026-02-15 09:47:05 +00:00
|
|
|
|
.all(|id| self.queried.contains(id));
|
2026-02-12 09:13:50 +00:00
|
|
|
|
|
|
|
|
|
|
if all_k_queried && !all_known_sorted.is_empty() {
|
|
|
|
|
|
self.done = true;
|
|
|
|
|
|
return vec![self.done_action()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut actions = Vec::new();
|
2026-02-15 09:47:05 +00:00
|
|
|
|
for (node_id, _) in candidates {
|
2026-02-12 09:13:50 +00:00
|
|
|
|
self.queried.insert(node_id);
|
|
|
|
|
|
self.pending.insert(node_id);
|
2026-02-15 09:47:05 +00:00
|
|
|
|
actions.push(LookupAction::Query { node_id });
|
2026-02-12 09:13:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actions
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 09:47:05 +00:00
|
|
|
|
fn k_closest(&self) -> Vec<NodeId> {
|
2026-02-12 09:13:50 +00:00
|
|
|
|
let mut sorted: Vec<_> = self
|
|
|
|
|
|
.known
|
|
|
|
|
|
.iter()
|
2026-02-15 09:47:05 +00:00
|
|
|
|
.map(|(id, dist)| (*id, *dist))
|
2026-02-12 09:13:50 +00:00
|
|
|
|
.collect();
|
2026-02-15 09:47:05 +00:00
|
|
|
|
sorted.sort_by(|a, b| a.1.cmp(&b.1));
|
2026-02-12 09:13:50 +00:00
|
|
|
|
sorted.truncate(self.k);
|
2026-02-15 09:47:05 +00:00
|
|
|
|
sorted.into_iter().map(|(id, _)| id).collect()
|
2026-02-12 09:13:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn done_action(&self) -> LookupAction {
|
|
|
|
|
|
LookupAction::Done {
|
|
|
|
|
|
closest: self.k_closest(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|