1261 lines
44 KiB
Rust
1261 lines
44 KiB
Rust
//! K-RPC implementation.
|
||||
|
|
|
|||
|
|
mod closest_nodes;
|
|||
|
|
pub(crate) mod config;
|
|||
|
|
mod info;
|
|||
|
|
mod iterative_query;
|
|||
|
|
mod put_query;
|
|||
|
|
pub(crate) mod server;
|
|||
|
|
mod socket;
|
|||
|
|
|
|||
|
|
use std::collections::HashMap;
|
|||
|
|
use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs};
|
|||
|
|
use std::num::NonZeroUsize;
|
|||
|
|
use std::time::{Duration, Instant};
|
|||
|
|
|
|||
|
|
use lru::LruCache;
|
|||
|
|
use tracing::{debug, error, info};
|
|||
|
|
|
|||
|
|
use iterative_query::IterativeQuery;
|
|||
|
|
use put_query::PutQuery;
|
|||
|
|
|
|||
|
|
use crate::common::{
|
|||
|
|
validate_immutable, ErrorSpecific, FindNodeRequestArguments, GetImmutableResponseArguments,
|
|||
|
|
GetMutableResponseArguments, GetPeersResponseArguments, GetValueRequestArguments, Id, Message,
|
|||
|
|
MessageType, MutableItem, NoMoreRecentValueResponseArguments, NoValuesResponseArguments, Node,
|
|||
|
|
PutRequestSpecific, RequestSpecific, RequestTypeSpecific, ResponseSpecific, RoutingTable,
|
|||
|
|
MAX_BUCKET_SIZE_K,
|
|||
|
|
};
|
|||
|
|
use server::Server;
|
|||
|
|
|
|||
|
|
use self::messages::{GetPeersRequestArguments, PutMutableRequestArguments};
|
|||
|
|
use server::ServerSettings;
|
|||
|
|
use socket::KrpcSocket;
|
|||
|
|
|
|||
|
|
pub use crate::common::messages;
|
|||
|
|
pub use closest_nodes::ClosestNodes;
|
|||
|
|
pub use info::Info;
|
|||
|
|
pub use iterative_query::{GetMutableOutcome, GetRequestSpecific};
|
|||
|
|
pub use put_query::{ConcurrencyError, PutError, PutOutcome, PutQueryError};
|
|||
|
|
pub use socket::DEFAULT_REQUEST_TIMEOUT;
|
|||
|
|
|
|||
|
|
/// Default bootstrap nodes used to discover peers when no custom bootstrap list is configured.
|
|||
|
|
pub const DEFAULT_BOOTSTRAP_NODES: [&str; 4] = [
|
|||
|
|
"router.bittorrent.com:6881",
|
|||
|
|
"dht.transmissionbt.com:6881",
|
|||
|
|
"dht.libtorrent.org:25401",
|
|||
|
|
"relay.pkarr.org:6881",
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const REFRESH_TABLE_INTERVAL: Duration = Duration::from_secs(15 * 60);
|
|||
|
|
const PING_TABLE_INTERVAL: Duration = Duration::from_secs(5 * 60);
|
|||
|
|
|
|||
|
|
/// Result of `tick_get_queries`: completed queries and whether self-findnode finished.
|
|||
|
|
type GetQueriesResult = (Vec<GetQueryOutcome>, bool);
|
|||
|
|
|
|||
|
|
const MAX_CACHED_ITERATIVE_QUERIES: usize = 1000;
|
|||
|
|
|
|||
|
|
#[derive(Debug)]
|
|||
|
|
/// Internal Rpc called in the Dht thread loop, useful to create your own actor setup.
|
|||
|
|
pub struct Rpc {
|
|||
|
|
// Options
|
|||
|
|
bootstrap: Box<[SocketAddrV4]>,
|
|||
|
|
|
|||
|
|
socket: KrpcSocket,
|
|||
|
|
|
|||
|
|
// Routing
|
|||
|
|
/// Closest nodes to this node
|
|||
|
|
routing_table: RoutingTable,
|
|||
|
|
/// Last time we refreshed the routing table with a find_node query.
|
|||
|
|
last_table_refresh: Instant,
|
|||
|
|
/// Last time we pinged nodes in the routing table.
|
|||
|
|
last_table_ping: Instant,
|
|||
|
|
/// Closest responding nodes to specific target
|
|||
|
|
///
|
|||
|
|
/// as well as the:
|
|||
|
|
/// 1. dht size estimate based on closest claimed nodes,
|
|||
|
|
/// 2. dht size estimate based on closest responding nodes.
|
|||
|
|
/// 3. number of subnets with unique 6 bits prefix in ipv4
|
|||
|
|
cached_iterative_queries: LruCache<Id, CachedIterativeQuery>,
|
|||
|
|
|
|||
|
|
// Active IterativeQueries
|
|||
|
|
iterative_queries: HashMap<Id, IterativeQuery>,
|
|||
|
|
/// Put queries are special, since they have to wait for a corresponding
|
|||
|
|
/// get query to finish, update the closest_nodes, then `query_all` these.
|
|||
|
|
put_queries: HashMap<Id, PutQuery>,
|
|||
|
|
|
|||
|
|
/// Sum of Dht size estimates from closest nodes from get queries.
|
|||
|
|
dht_size_estimates_sum: f64,
|
|||
|
|
|
|||
|
|
/// Sum of Dht size estimates from closest _responding_ nodes from get queries.
|
|||
|
|
responders_based_dht_size_estimates_sum: f64,
|
|||
|
|
responders_based_dht_size_estimates_count: usize,
|
|||
|
|
|
|||
|
|
/// Sum of the number of subnets with 6 bits prefix in the closest nodes ipv4
|
|||
|
|
subnets_sum: usize,
|
|||
|
|
|
|||
|
|
server: Server,
|
|||
|
|
|
|||
|
|
public_address: Option<SocketAddrV4>,
|
|||
|
|
firewalled: bool,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Rpc {
|
|||
|
|
/// Creates a new RPC instance and prepares the routing table and socket.
|
|||
|
|
///
|
|||
|
|
/// This does not perform network IO by itself. Call [`Rpc::tick`] to bootstrap
|
|||
|
|
/// and perform scheduled maintenance.
|
|||
|
|
///
|
|||
|
|
/// Returns an instance ready to accept `get`/`put` requests and handle incoming
|
|||
|
|
/// messages via [`Rpc::handle_message`] if you integrate it with your socket loop.
|
|||
|
|
pub fn new(config: config::Config) -> Result<Self, std::io::Error> {
|
|||
|
|
let id = if let Some(ip) = config.public_ip {
|
|||
|
|
Id::from_ip(ip.into())
|
|||
|
|
} else {
|
|||
|
|
Id::random()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let socket = KrpcSocket::new(&config)?;
|
|||
|
|
|
|||
|
|
Ok(Rpc {
|
|||
|
|
bootstrap: config
|
|||
|
|
.bootstrap
|
|||
|
|
.unwrap_or_else(|| to_socket_address(&DEFAULT_BOOTSTRAP_NODES))
|
|||
|
|
.into(),
|
|||
|
|
socket,
|
|||
|
|
|
|||
|
|
routing_table: RoutingTable::new(id),
|
|||
|
|
iterative_queries: HashMap::new(),
|
|||
|
|
put_queries: HashMap::new(),
|
|||
|
|
|
|||
|
|
cached_iterative_queries: LruCache::new(
|
|||
|
|
NonZeroUsize::new(MAX_CACHED_ITERATIVE_QUERIES)
|
|||
|
|
.expect("MAX_CACHED_BUCKETS is NonZeroUsize"),
|
|||
|
|
),
|
|||
|
|
|
|||
|
|
last_table_refresh: Instant::now(),
|
|||
|
|
last_table_ping: Instant::now(),
|
|||
|
|
|
|||
|
|
dht_size_estimates_sum: 0.0,
|
|||
|
|
responders_based_dht_size_estimates_count: 0,
|
|||
|
|
|
|||
|
|
// Don't store to too many nodes just because you are in a cold start.
|
|||
|
|
responders_based_dht_size_estimates_sum: 1_000_000.0,
|
|||
|
|
subnets_sum: 20,
|
|||
|
|
|
|||
|
|
server: Server::new(config.server_settings),
|
|||
|
|
|
|||
|
|
public_address: None,
|
|||
|
|
firewalled: true,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// === Getters ===
|
|||
|
|
|
|||
|
|
/// Returns the node's Id
|
|||
|
|
pub fn id(&self) -> &Id {
|
|||
|
|
self.routing_table.id()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns the address the server is listening to.
|
|||
|
|
#[inline]
|
|||
|
|
pub fn local_addr(&self) -> SocketAddrV4 {
|
|||
|
|
self.socket.local_addr()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns the best guess for this node's Public address.
|
|||
|
|
///
|
|||
|
|
/// If [crate::DhtBuilder::public_ip] was set, this is what will be returned
|
|||
|
|
/// (plus the local port), otherwise it will rely on consensus from
|
|||
|
|
/// responding nodes voting on our public IP and port.
|
|||
|
|
pub fn public_address(&self) -> Option<SocketAddrV4> {
|
|||
|
|
self.public_address
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns `true` if we can't confirm that [Self::public_address] is publicly addressable.
|
|||
|
|
///
|
|||
|
|
/// If this node is firewalled, it won't switch to server mode if it is in adaptive mode,
|
|||
|
|
/// but if [crate::DhtBuilder::server_mode] was set to true, then whether or not this node is firewalled
|
|||
|
|
/// won't matter.
|
|||
|
|
pub fn firewalled(&self) -> bool {
|
|||
|
|
self.firewalled
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns whether or not this node is running in server mode.
|
|||
|
|
pub fn server_mode(&self) -> bool {
|
|||
|
|
self.socket.server_mode
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn routing_table(&self) -> &RoutingTable {
|
|||
|
|
&self.routing_table
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn routing_table_mut(&mut self) -> &mut RoutingTable {
|
|||
|
|
&mut self.routing_table
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns:
|
|||
|
|
/// 1. Normal Dht size estimate based on all closer `nodes` in query responses.
|
|||
|
|
/// 2. Standard deviaiton as a function of the number of samples used in this estimate.
|
|||
|
|
///
|
|||
|
|
/// [Read more](https://github.com/pubky/mainline/blob/main/docs/dht_size_estimate.md)
|
|||
|
|
pub fn dht_size_estimate(&self) -> (usize, f64) {
|
|||
|
|
let normal =
|
|||
|
|
self.dht_size_estimates_sum as usize / self.cached_iterative_queries.len().max(1);
|
|||
|
|
|
|||
|
|
// See https://github.com/pubky/mainline/blob/main/docs/standard-deviation-vs-lookups.png
|
|||
|
|
let std_dev = 0.281 * (self.cached_iterative_queries.len() as f64).powf(-0.529);
|
|||
|
|
|
|||
|
|
(normal, std_dev)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns a thread safe and lightweight summary of this node's
|
|||
|
|
/// information and statistics.
|
|||
|
|
pub fn info(&self) -> Info {
|
|||
|
|
Info::from(self)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// === Public Methods ===
|
|||
|
|
|
|||
|
|
/// Advances maintenance and in-flight queries by one step.
|
|||
|
|
///
|
|||
|
|
/// - Performs routing-table refreshes and liveness checks on schedule.
|
|||
|
|
/// - Progresses outstanding `get`/`put` queries and evicts completed ones.
|
|||
|
|
/// - May emit newly-available query responses.
|
|||
|
|
///
|
|||
|
|
/// Returns a [`RpcTickReport`] summarizing work done during this call.
|
|||
|
|
///
|
|||
|
|
/// Call this periodically; typical intervals are tied to IO loop cadence
|
|||
|
|
/// or a fixed timer. Missing calls will delay query completion and degrade
|
|||
|
|
/// the routing table quality.
|
|||
|
|
pub fn tick(&mut self) -> RpcTickReport {
|
|||
|
|
let mut done_put_queries = self.tick_put_queries();
|
|||
|
|
|
|||
|
|
let (done_get_queries, finished_self_findnode) = self.tick_get_queries();
|
|||
|
|
|
|||
|
|
self.cleanup_done_queries(&done_get_queries, &mut done_put_queries);
|
|||
|
|
|
|||
|
|
self.periodic_node_maintenance();
|
|||
|
|
|
|||
|
|
let new_query_response = self.handle_message();
|
|||
|
|
|
|||
|
|
if finished_self_findnode {
|
|||
|
|
self.log_bootstrap(self.id());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
RpcTickReport {
|
|||
|
|
done_get_queries,
|
|||
|
|
done_put_queries,
|
|||
|
|
new_query_response,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Send a request to the given address and return the transaction_id
|
|||
|
|
pub fn request(&mut self, address: SocketAddrV4, request: RequestSpecific) -> u32 {
|
|||
|
|
self.socket.request(address, request)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Send a response to the given address.
|
|||
|
|
pub fn response(
|
|||
|
|
&mut self,
|
|||
|
|
address: SocketAddrV4,
|
|||
|
|
transaction_id: u32,
|
|||
|
|
response: ResponseSpecific,
|
|||
|
|
) {
|
|||
|
|
self.socket.response(address, transaction_id, response)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Send an error to the given address.
|
|||
|
|
pub fn error(&mut self, address: SocketAddrV4, transaction_id: u32, error: ErrorSpecific) {
|
|||
|
|
self.socket.error(address, transaction_id, error)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Store a value in the closest nodes, optionally trigger a lookup query if
|
|||
|
|
/// the cached closest_nodes aren't fresh enough.
|
|||
|
|
///
|
|||
|
|
/// - `request`: the put request.
|
|||
|
|
pub fn put(
|
|||
|
|
&mut self,
|
|||
|
|
request: PutRequestSpecific,
|
|||
|
|
extra_nodes: Option<Box<[Node]>>,
|
|||
|
|
) -> Result<(), PutError> {
|
|||
|
|
let target = *request.target();
|
|||
|
|
|
|||
|
|
if let PutRequestSpecific::PutMutable(PutMutableRequestArguments {
|
|||
|
|
sig, cas, seq, ..
|
|||
|
|
}) = &request
|
|||
|
|
{
|
|||
|
|
if let Some(PutRequestSpecific::PutMutable(inflight_request)) = self
|
|||
|
|
.put_queries
|
|||
|
|
.get(&target)
|
|||
|
|
.map(|existing| &existing.request)
|
|||
|
|
{
|
|||
|
|
debug!(?inflight_request, ?request, "Possible conflict risk");
|
|||
|
|
|
|||
|
|
if *sig == inflight_request.sig {
|
|||
|
|
// Noop, the inflight query is sufficient.
|
|||
|
|
return Ok(());
|
|||
|
|
} else if *seq < inflight_request.seq {
|
|||
|
|
return Err(PutError::Concurrency(ConcurrencyError::NotMostRecent));
|
|||
|
|
} else if let Some(cas) = cas {
|
|||
|
|
if *cas == inflight_request.seq {
|
|||
|
|
// The user is aware of the inflight query and whiches to overrides it.
|
|||
|
|
//
|
|||
|
|
// Remove the inflight request, and create a new one.
|
|||
|
|
self.put_queries.remove(&target);
|
|||
|
|
} else {
|
|||
|
|
return Err(PutError::Concurrency(ConcurrencyError::CasFailed));
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
return Err(PutError::Concurrency(ConcurrencyError::ConflictRisk));
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut query = PutQuery::new(target, request.clone(), extra_nodes);
|
|||
|
|
|
|||
|
|
if let Some(closest_nodes) = self
|
|||
|
|
.cached_iterative_queries
|
|||
|
|
.get(&target)
|
|||
|
|
.map(|cached| cached.closest_responding_nodes.clone())
|
|||
|
|
.filter(|closest_nodes| {
|
|||
|
|
!closest_nodes.is_empty() && closest_nodes.iter().any(Node::valid_token)
|
|||
|
|
})
|
|||
|
|
{
|
|||
|
|
query.start(&mut self.socket, &closest_nodes)?
|
|||
|
|
} else {
|
|||
|
|
let salt = match request {
|
|||
|
|
PutRequestSpecific::PutMutable(args) => args.salt,
|
|||
|
|
_ => None,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
self.get(
|
|||
|
|
GetRequestSpecific::GetValue(GetValueRequestArguments {
|
|||
|
|
target,
|
|||
|
|
seq: None,
|
|||
|
|
salt,
|
|||
|
|
}),
|
|||
|
|
None,
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
self.put_queries.insert(target, query);
|
|||
|
|
|
|||
|
|
Ok(())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Send a message to closer and closer nodes until we can't find any more nodes.
|
|||
|
|
///
|
|||
|
|
/// Queries take few seconds to fully traverse the network, once it is done, it will be removed from
|
|||
|
|
/// self.iterative_queries. But until then, calling [Rpc::get] multiple times, will just return the list
|
|||
|
|
/// of responses seen so far.
|
|||
|
|
///
|
|||
|
|
/// Subsequent responses can be obtained from the [RpcTickReport::new_query_response] you get after calling [Rpc::tick].
|
|||
|
|
///
|
|||
|
|
/// Effectively, we are caching responses and backing off the network for the duration it takes
|
|||
|
|
/// to traverse it.
|
|||
|
|
///
|
|||
|
|
/// - `request` [RequestTypeSpecific], except [RequestTypeSpecific::Ping] and
|
|||
|
|
/// [RequestTypeSpecific::Put] which will be ignored.
|
|||
|
|
/// - `extra_nodes` option allows the query to visit specific nodes, that won't necessesarily be visited
|
|||
|
|
/// through the query otherwise.
|
|||
|
|
pub fn get(
|
|||
|
|
&mut self,
|
|||
|
|
request: GetRequestSpecific,
|
|||
|
|
extra_nodes: Option<&[SocketAddrV4]>,
|
|||
|
|
) -> Option<Vec<Response>> {
|
|||
|
|
let target = match request {
|
|||
|
|
GetRequestSpecific::FindNode(FindNodeRequestArguments { target }) => target,
|
|||
|
|
GetRequestSpecific::GetPeers(GetPeersRequestArguments { info_hash, .. }) => info_hash,
|
|||
|
|
GetRequestSpecific::GetValue(GetValueRequestArguments { target, .. }) => target,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// If query is still active, no need to create a new one.
|
|||
|
|
if let Some(query) = self.iterative_queries.get(&target) {
|
|||
|
|
return Some(query.responses().to_vec());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let node_id = self.routing_table.id();
|
|||
|
|
|
|||
|
|
if target == *node_id {
|
|||
|
|
debug!(?node_id, "Bootstrapping the routing table");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut query = IterativeQuery::new(*self.id(), target, request);
|
|||
|
|
|
|||
|
|
// Seed the query either with the closest nodes from the routing table, or the
|
|||
|
|
// bootstrapping nodes if the closest nodes are not enough.
|
|||
|
|
|
|||
|
|
let routing_table_closest = self.routing_table.closest_secure(
|
|||
|
|
target,
|
|||
|
|
self.responders_based_dht_size_estimate(),
|
|||
|
|
self.average_subnets(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// If we don't have enough or any closest nodes, call the bootstrapping nodes.
|
|||
|
|
if routing_table_closest.is_empty() || routing_table_closest.len() < self.bootstrap.len() {
|
|||
|
|
for bootstrapping_node in self.bootstrap.clone() {
|
|||
|
|
query.visit(&mut self.socket, bootstrapping_node);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Some(extra_nodes) = extra_nodes {
|
|||
|
|
for extra_node in extra_nodes {
|
|||
|
|
query.visit(&mut self.socket, *extra_node)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Seed this query with the closest nodes we know about.
|
|||
|
|
for node in routing_table_closest {
|
|||
|
|
query.add_candidate(node)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Some(CachedIterativeQuery {
|
|||
|
|
closest_responding_nodes,
|
|||
|
|
..
|
|||
|
|
}) = self.cached_iterative_queries.get(&target)
|
|||
|
|
{
|
|||
|
|
for node in closest_responding_nodes {
|
|||
|
|
query.add_candidate(node.clone())
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// After adding the nodes, we need to start the query.
|
|||
|
|
query.start(&mut self.socket);
|
|||
|
|
|
|||
|
|
self.iterative_queries.insert(target, query);
|
|||
|
|
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// === Private Methods ===
|
|||
|
|
|
|||
|
|
/// Handles a single inbound KRPC request.
|
|||
|
|
///
|
|||
|
|
/// Responsibilities:
|
|||
|
|
/// - During initial bootstrap (no known bootstrap nodes), adds the requester of
|
|||
|
|
/// a `FindNode` to the routing table to seed it.
|
|||
|
|
/// - If running in server mode, forwards the request to the embedded server and
|
|||
|
|
/// emits a response or error using the provided `transaction_id`.
|
|||
|
|
/// - Detects successful NAT traversal: when a `Ping` arrives from our own public
|
|||
|
|
/// address, clears `firewalled`.
|
|||
|
|
/// - Ensures node ID/IP consistency: if our ID is invalid for the observed
|
|||
|
|
/// public IPv4, generates a new secure ID, resets the routing table, and
|
|||
|
|
/// initiates a `FindNode` to repopulate it.
|
|||
|
|
///
|
|||
|
|
/// Parameters:
|
|||
|
|
/// - `from`: Source socket address of the requester.
|
|||
|
|
/// - `transaction_id`: Transaction ID to echo in any response/error.
|
|||
|
|
/// - `request_specific`: Parsed request payload and type.
|
|||
|
|
///
|
|||
|
|
/// Side effects:
|
|||
|
|
/// - May add `from` to the routing table (bootstrap exception).
|
|||
|
|
/// - May send a protocol response or error.
|
|||
|
|
/// - May set `firewalled = false` on self-`Ping`.
|
|||
|
|
/// - May rotate this node’s ID, reset the routing table, and trigger a
|
|||
|
|
/// rebootstrap query.
|
|||
|
|
///
|
|||
|
|
/// Returns: Nothing.
|
|||
|
|
fn handle_request(
|
|||
|
|
&mut self,
|
|||
|
|
from: SocketAddrV4,
|
|||
|
|
transaction_id: u32,
|
|||
|
|
request_specific: RequestSpecific,
|
|||
|
|
) {
|
|||
|
|
// By default we only add nodes that responds to our requests.
|
|||
|
|
//
|
|||
|
|
// This is the only exception; the first node creating the DHT,
|
|||
|
|
// without this exception, the bootstrapping node's routing table
|
|||
|
|
// will never be populated.
|
|||
|
|
if self.bootstrap.is_empty() {
|
|||
|
|
if let RequestTypeSpecific::FindNode(param) = &request_specific.request_type {
|
|||
|
|
self.routing_table.add(Node::new(param.target, from));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let is_ping = matches!(request_specific.request_type, RequestTypeSpecific::Ping);
|
|||
|
|
|
|||
|
|
if self.server_mode() {
|
|||
|
|
let server = &mut self.server;
|
|||
|
|
|
|||
|
|
match server.handle_request(&self.routing_table, from, request_specific) {
|
|||
|
|
Some(MessageType::Error(error)) => {
|
|||
|
|
self.error(from, transaction_id, error);
|
|||
|
|
}
|
|||
|
|
Some(MessageType::Response(response)) => {
|
|||
|
|
self.response(from, transaction_id, response);
|
|||
|
|
}
|
|||
|
|
_ => {}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Some(our_address) = self.public_address {
|
|||
|
|
if from == our_address && is_ping {
|
|||
|
|
self.firewalled = false;
|
|||
|
|
|
|||
|
|
let ipv4 = our_address.ip();
|
|||
|
|
|
|||
|
|
// Restarting our routing table with new secure Id if necessary.
|
|||
|
|
if !self.id().is_valid_for_ip(*ipv4) {
|
|||
|
|
let new_id = Id::from_ipv4(*ipv4);
|
|||
|
|
|
|||
|
|
info!(
|
|||
|
|
"Our current id {} is not valid for address {}. Using new id {}",
|
|||
|
|
self.id(),
|
|||
|
|
our_address,
|
|||
|
|
new_id
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
self.get(
|
|||
|
|
GetRequestSpecific::FindNode(FindNodeRequestArguments { target: new_id }),
|
|||
|
|
None,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
self.routing_table = RoutingTable::new(new_id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Handles an inbound KRPC response for RPC, updating in-flight queries and optionally
|
|||
|
|
/// returning a final value for the associated target.
|
|||
|
|
///
|
|||
|
|
/// Behavior:
|
|||
|
|
/// - Ignores responses marked read-only, recording them as invalid responses.
|
|||
|
|
/// - If it matches an in-flight PutQuery, treats `Ping` as a storage ACK (success/error)
|
|||
|
|
/// and stops further handling.
|
|||
|
|
/// - If it matches an in-flight iterative query:
|
|||
|
|
/// - Incorporates network info: adds closer candidates, records responder token,
|
|||
|
|
/// and votes on the observed requester IP.
|
|||
|
|
/// - On value responses:
|
|||
|
|
/// - `GetPeers` → returns `(target, Response::Peers)`
|
|||
|
|
/// - `GetImmutable` → validates content; on success returns `(target, Response::Immutable)`
|
|||
|
|
/// - `GetMutable` → verifies record (sig/seq/salt); on success returns `(target, Response::Mutable)`
|
|||
|
|
/// - Logs and continues on `NoValues` / `NoMoreRecentValue` / `Error`.
|
|||
|
|
/// - On any expected response, adds the responder (by author ID) to the routing table.
|
|||
|
|
///
|
|||
|
|
/// Parameters:
|
|||
|
|
/// - `from`: Responder socket address.
|
|||
|
|
/// - `message`: Decoded KRPC message.
|
|||
|
|
///
|
|||
|
|
/// Returns:
|
|||
|
|
/// - `Some((target, Response))` when a terminal value is obtained for the query.
|
|||
|
|
/// - `None` otherwise.
|
|||
|
|
fn handle_response(&mut self, from: SocketAddrV4, message: Message) -> Option<(Id, Response)> {
|
|||
|
|
// If someone claims to be readonly, then let's not store anything even if they respond.
|
|||
|
|
if message.read_only {
|
|||
|
|
if let Some(query) = self
|
|||
|
|
.iterative_queries
|
|||
|
|
.values_mut()
|
|||
|
|
.find(|query| query.is_inflight_query_request(message.transaction_id))
|
|||
|
|
{
|
|||
|
|
query.record_invalid_response();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return None;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// If the response looks like a Ping response, check StoreQueries for the transaction_id.
|
|||
|
|
if let Some(query) = self
|
|||
|
|
.put_queries
|
|||
|
|
.values_mut()
|
|||
|
|
.find(|query| query.inflight(message.transaction_id))
|
|||
|
|
{
|
|||
|
|
match message.message_type {
|
|||
|
|
MessageType::Response(ResponseSpecific::Ping(_)) => {
|
|||
|
|
// Mark storage at that node as a success.
|
|||
|
|
query.success();
|
|||
|
|
}
|
|||
|
|
MessageType::Error(error) => query.error(error),
|
|||
|
|
_ => {}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return None;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut should_add_node = false;
|
|||
|
|
let author_id = message.get_author_id();
|
|||
|
|
let from_version = message.version.to_owned();
|
|||
|
|
|
|||
|
|
// Get corresponding query for message.transaction_id
|
|||
|
|
if let Some(query) = self
|
|||
|
|
.iterative_queries
|
|||
|
|
.values_mut()
|
|||
|
|
.find(|query| query.is_inflight(message.transaction_id))
|
|||
|
|
{
|
|||
|
|
let is_query_response = query.is_inflight_query_request(message.transaction_id);
|
|||
|
|
let is_mutable_get_query_response = is_query_response
|
|||
|
|
&& matches!(
|
|||
|
|
&query.request.request_type,
|
|||
|
|
RequestTypeSpecific::GetValue(_)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// KrpcSocket would not give us a response from the wrong address for the transaction_id
|
|||
|
|
should_add_node = true;
|
|||
|
|
|
|||
|
|
if let Some(nodes) = message.get_closer_nodes() {
|
|||
|
|
for node in nodes {
|
|||
|
|
query.add_candidate(node.clone());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Some((responder_id, token)) = message.get_token() {
|
|||
|
|
query.add_responding_node(Node::new_with_token(responder_id, from, token.into()));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Some(proposed_ip) = message.requester_ip {
|
|||
|
|
query.add_address_vote(proposed_ip);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let target = query.target();
|
|||
|
|
|
|||
|
|
match message.message_type {
|
|||
|
|
MessageType::Response(ResponseSpecific::GetPeers(GetPeersResponseArguments {
|
|||
|
|
values,
|
|||
|
|
..
|
|||
|
|
})) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_invalid_response();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let response = Response::Peers(values);
|
|||
|
|
query.response(from, response.clone());
|
|||
|
|
|
|||
|
|
return Some((target, response));
|
|||
|
|
}
|
|||
|
|
MessageType::Response(ResponseSpecific::GetImmutable(
|
|||
|
|
GetImmutableResponseArguments {
|
|||
|
|
v, responder_id, ..
|
|||
|
|
},
|
|||
|
|
)) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_invalid_response();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if validate_immutable(&v, query.target()) {
|
|||
|
|
let response = Response::Immutable(v);
|
|||
|
|
query.response(from, response.clone());
|
|||
|
|
|
|||
|
|
return Some((target, response));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let target = query.target();
|
|||
|
|
debug!(
|
|||
|
|
?v,
|
|||
|
|
?target,
|
|||
|
|
?responder_id,
|
|||
|
|
?from,
|
|||
|
|
?from_version,
|
|||
|
|
"Invalid immutable value"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
MessageType::Response(ResponseSpecific::GetMutable(
|
|||
|
|
GetMutableResponseArguments {
|
|||
|
|
v,
|
|||
|
|
seq,
|
|||
|
|
sig,
|
|||
|
|
k,
|
|||
|
|
responder_id,
|
|||
|
|
..
|
|||
|
|
},
|
|||
|
|
)) => {
|
|||
|
|
let salt = match query.request.request_type.clone() {
|
|||
|
|
RequestTypeSpecific::GetValue(args) => args.salt,
|
|||
|
|
_ => None,
|
|||
|
|
};
|
|||
|
|
let target = query.target();
|
|||
|
|
|
|||
|
|
match MutableItem::from_dht_message(query.target(), &k, v, seq, &sig, salt) {
|
|||
|
|
Ok(item) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_mutable_value();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let response = Response::Mutable(item);
|
|||
|
|
query.response(from, response.clone());
|
|||
|
|
|
|||
|
|
return Some((target, response));
|
|||
|
|
}
|
|||
|
|
Err(error) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_invalid_mutable_value();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
debug!(
|
|||
|
|
?error,
|
|||
|
|
?from,
|
|||
|
|
?responder_id,
|
|||
|
|
?from_version,
|
|||
|
|
"Invalid mutable record"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
MessageType::Response(ResponseSpecific::NoMoreRecentValue(
|
|||
|
|
NoMoreRecentValueResponseArguments {
|
|||
|
|
seq, responder_id, ..
|
|||
|
|
},
|
|||
|
|
)) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_no_more_recent();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
debug!(
|
|||
|
|
target= ?query.target(),
|
|||
|
|
salt= ?match query.request.request_type.clone() {
|
|||
|
|
RequestTypeSpecific::GetValue(args) => args.salt,
|
|||
|
|
_ => None,
|
|||
|
|
},
|
|||
|
|
?seq,
|
|||
|
|
?from,
|
|||
|
|
?responder_id,
|
|||
|
|
?from_version,
|
|||
|
|
"No more recent value"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
MessageType::Response(ResponseSpecific::NoValues(NoValuesResponseArguments {
|
|||
|
|
responder_id,
|
|||
|
|
..
|
|||
|
|
})) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_no_values();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
debug!(
|
|||
|
|
target= ?query.target(),
|
|||
|
|
salt= ?match query.request.request_type.clone() {
|
|||
|
|
RequestTypeSpecific::GetValue(args) => args.salt,
|
|||
|
|
_ => None,
|
|||
|
|
},
|
|||
|
|
?from,
|
|||
|
|
?responder_id,
|
|||
|
|
?from_version ,
|
|||
|
|
"No values"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
MessageType::Error(error) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_krpc_error();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
debug!(?error, ?from_version, "Get query got error response");
|
|||
|
|
}
|
|||
|
|
// Ping response is already handled in add_node()
|
|||
|
|
// FindNode response is already handled in query.add_candidate()
|
|||
|
|
// Requests are handled elsewhere
|
|||
|
|
MessageType::Response(ResponseSpecific::Ping(_))
|
|||
|
|
| MessageType::Response(ResponseSpecific::FindNode(_))
|
|||
|
|
| MessageType::Request(_) => {
|
|||
|
|
if is_mutable_get_query_response {
|
|||
|
|
query.record_invalid_response();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if should_add_node {
|
|||
|
|
// Add a node to our routing table on any expected incoming response.
|
|||
|
|
|
|||
|
|
if let Some(id) = author_id {
|
|||
|
|
self.routing_table.add(Node::new(id, from));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Periodically maintain the routing table:
|
|||
|
|
/// - Switches to server mode if eligible (and refresh is due)
|
|||
|
|
/// - Pings nodes and purges stale entries when needed
|
|||
|
|
/// - Repopulates via bootstrap if table is empty or refresh is due
|
|||
|
|
/// - Updates last_table_refresh and last_table_ping timers as needed
|
|||
|
|
fn periodic_node_maintenance(&mut self) {
|
|||
|
|
let refresh_is_due = self.last_table_refresh.elapsed() >= REFRESH_TABLE_INTERVAL;
|
|||
|
|
let ping_is_due = self.last_table_ping.elapsed() >= PING_TABLE_INTERVAL;
|
|||
|
|
|
|||
|
|
// Decide first, act once: avoid double populate in the same tick.
|
|||
|
|
let should_populate = self.routing_table.is_empty() || refresh_is_due;
|
|||
|
|
|
|||
|
|
if refresh_is_due {
|
|||
|
|
self.try_switching_to_server_mode();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ping_is_due {
|
|||
|
|
self.ping_and_purge();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if should_populate {
|
|||
|
|
self.populate();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Attempts to switch this node into server mode if eligible.
|
|||
|
|
///
|
|||
|
|
/// If the node is not currently operating
|
|||
|
|
/// in server mode and is not detected as being behind a firewall, it will promote the
|
|||
|
|
/// node into server mode (by setting the server_mode field to `true`).
|
|||
|
|
///
|
|||
|
|
/// Server mode enables the node to answer unsolicited requests and fulfill a key
|
|||
|
|
/// responsibility in the DHT. Nodes that are firewalled, or behind NAT, should not
|
|||
|
|
/// enable server mode unless explicitly configured to do so.
|
|||
|
|
fn try_switching_to_server_mode(&mut self) {
|
|||
|
|
if !self.server_mode() && !self.firewalled() {
|
|||
|
|
info!("Adaptive mode: have been running long enough (not firewalled), switching to server mode");
|
|||
|
|
self.socket.server_mode = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Purge stale nodes and ping nodes that need probing when due is reached.
|
|||
|
|
///
|
|||
|
|
/// It will purge stale nodes from the routing table and periodcially ping nodes.
|
|||
|
|
/// It will reset the last_table_ping timer.
|
|||
|
|
fn ping_and_purge(&mut self) {
|
|||
|
|
self.last_table_ping = Instant::now();
|
|||
|
|
|
|||
|
|
let (to_purge, to_ping) = self.purge_and_ping_candidates();
|
|||
|
|
|
|||
|
|
self.purge_nodes(&to_purge);
|
|||
|
|
self.ping_nodes(&to_ping);
|
|||
|
|
|
|||
|
|
if to_purge.is_empty() && to_ping.is_empty() {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
debug!(
|
|||
|
|
removed = to_purge.len(),
|
|||
|
|
pinged = to_ping.len(),
|
|||
|
|
"Node maintenance executed"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Pure decision function: compute which nodes to remove and which to ping.
|
|||
|
|
fn purge_and_ping_candidates(&self) -> (Vec<Id>, Vec<SocketAddrV4>) {
|
|||
|
|
let mut to_purge = Vec::with_capacity(self.routing_table.size());
|
|||
|
|
let mut to_ping = Vec::with_capacity(self.routing_table.size());
|
|||
|
|
|
|||
|
|
for node in self.routing_table.nodes() {
|
|||
|
|
if node.is_stale() {
|
|||
|
|
to_purge.push(*node.id())
|
|||
|
|
} else if node.should_ping() {
|
|||
|
|
to_ping.push(node.address())
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(to_purge, to_ping)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Remove nodes from the routing table.
|
|||
|
|
fn purge_nodes(&mut self, ids: &[Id]) {
|
|||
|
|
for id in ids {
|
|||
|
|
self.routing_table.remove(id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Ping nodes.
|
|||
|
|
fn ping_nodes(&mut self, addrs: &[SocketAddrV4]) {
|
|||
|
|
for address in addrs {
|
|||
|
|
self.ping(*address);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Populate routing table by asking bootstrap nodes to find ourselves,
|
|||
|
|
/// Response will allow to add closest nodes candidates to routing table.
|
|||
|
|
///
|
|||
|
|
/// Reset the last_table_refresh timer.
|
|||
|
|
fn populate(&mut self) {
|
|||
|
|
self.last_table_refresh = Instant::now();
|
|||
|
|
|
|||
|
|
if self.bootstrap.is_empty() {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
self.get(
|
|||
|
|
GetRequestSpecific::FindNode(FindNodeRequestArguments { target: *self.id() }),
|
|||
|
|
None,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Send a ping request to a node.
|
|||
|
|
fn ping(&mut self, address: SocketAddrV4) {
|
|||
|
|
self.socket.request(
|
|||
|
|
address,
|
|||
|
|
RequestSpecific {
|
|||
|
|
requester_id: *self.id(),
|
|||
|
|
request_type: RequestTypeSpecific::Ping,
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn update_address_votes_from_iterative_query(&mut self, query: &IterativeQuery) {
|
|||
|
|
let Some(new_address) = query.best_address() else {
|
|||
|
|
return;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let needs_confirm = match self.public_address {
|
|||
|
|
None => true,
|
|||
|
|
Some(current) => current != new_address,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if needs_confirm {
|
|||
|
|
debug!(
|
|||
|
|
?new_address,
|
|||
|
|
"Query responses suggest a different public_address, trying to confirm.."
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
self.firewalled = true;
|
|||
|
|
self.ping(new_address);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
self.public_address = Some(new_address);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn cache_iterative_query(&mut self, query: &IterativeQuery, closest_responding_nodes: &[Node]) {
|
|||
|
|
if self.cached_iterative_queries.len() >= MAX_CACHED_ITERATIVE_QUERIES {
|
|||
|
|
let q = self.cached_iterative_queries.pop_lru();
|
|||
|
|
self.decrement_cached_iterative_query_stats(q.map(|q| q.1));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let closest = query.closest();
|
|||
|
|
let responders = query.responders();
|
|||
|
|
|
|||
|
|
if closest.nodes().is_empty() {
|
|||
|
|
// We are clearly offline.
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let dht_size_estimate = closest.dht_size_estimate();
|
|||
|
|
let responders_dht_size_estimate = responders.dht_size_estimate();
|
|||
|
|
let subnets_count = closest.subnets_count();
|
|||
|
|
|
|||
|
|
let previous = self.cached_iterative_queries.put(
|
|||
|
|
query.target(),
|
|||
|
|
CachedIterativeQuery {
|
|||
|
|
closest_responding_nodes: closest_responding_nodes.into(),
|
|||
|
|
dht_size_estimate,
|
|||
|
|
responders_dht_size_estimate,
|
|||
|
|
subnets: subnets_count,
|
|||
|
|
|
|||
|
|
is_find_node: matches!(
|
|||
|
|
query.request.request_type,
|
|||
|
|
RequestTypeSpecific::FindNode(_)
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
self.decrement_cached_iterative_query_stats(previous);
|
|||
|
|
|
|||
|
|
self.dht_size_estimates_sum += dht_size_estimate;
|
|||
|
|
self.responders_based_dht_size_estimates_sum += responders_dht_size_estimate;
|
|||
|
|
self.subnets_sum += subnets_count as usize;
|
|||
|
|
self.responders_based_dht_size_estimates_count += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn responders_based_dht_size_estimate(&self) -> usize {
|
|||
|
|
self.responders_based_dht_size_estimates_sum as usize
|
|||
|
|
/ self.responders_based_dht_size_estimates_count.max(1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn average_subnets(&self) -> usize {
|
|||
|
|
self.subnets_sum / self.cached_iterative_queries.len().max(1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn decrement_cached_iterative_query_stats(&mut self, query: Option<CachedIterativeQuery>) {
|
|||
|
|
if let Some(CachedIterativeQuery {
|
|||
|
|
dht_size_estimate,
|
|||
|
|
responders_dht_size_estimate,
|
|||
|
|
subnets,
|
|||
|
|
is_find_node,
|
|||
|
|
..
|
|||
|
|
}) = query
|
|||
|
|
{
|
|||
|
|
self.dht_size_estimates_sum -= dht_size_estimate;
|
|||
|
|
self.responders_based_dht_size_estimates_sum -= responders_dht_size_estimate;
|
|||
|
|
self.subnets_sum -= subnets as usize;
|
|||
|
|
|
|||
|
|
if !is_find_node {
|
|||
|
|
self.responders_based_dht_size_estimates_count -= 1;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// === tick() helpers ===
|
|||
|
|
|
|||
|
|
/// Advance all PUT queries, return done ones.
|
|||
|
|
fn tick_put_queries(&mut self) -> Vec<(Id, Result<PutOutcome, PutError>)> {
|
|||
|
|
let mut done_put_queries = Vec::with_capacity(self.put_queries.len());
|
|||
|
|
|
|||
|
|
for (id, query) in self.put_queries.iter_mut() {
|
|||
|
|
match query.poll_completion(&self.socket) {
|
|||
|
|
Ok(Some(outcome)) => done_put_queries.push((*id, Ok(outcome))),
|
|||
|
|
Ok(None) => (),
|
|||
|
|
Err(error) => done_put_queries.push((*id, Err(error))),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
done_put_queries
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Advance all GET/FIND_NODE queries, return done ones and whether table refresh/find_node to self is finished.
|
|||
|
|
fn tick_get_queries(&mut self) -> GetQueriesResult {
|
|||
|
|
let self_id = *self.id();
|
|||
|
|
let responders_based_dht_size_estimate = self.responders_based_dht_size_estimate();
|
|||
|
|
let average_subnets = self.average_subnets();
|
|||
|
|
|
|||
|
|
let mut done_get_queries = Vec::with_capacity(self.iterative_queries.len());
|
|||
|
|
let mut finished_self_findnode = false;
|
|||
|
|
|
|||
|
|
for (id, query) in self.iterative_queries.iter_mut() {
|
|||
|
|
if !query.tick(&mut self.socket) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let closest_nodes = if let RequestTypeSpecific::FindNode(_) = query.request.request_type
|
|||
|
|
{
|
|||
|
|
finished_self_findnode = *id == self_id;
|
|||
|
|
|
|||
|
|
query
|
|||
|
|
.closest()
|
|||
|
|
.nodes()
|
|||
|
|
.iter()
|
|||
|
|
.take(MAX_BUCKET_SIZE_K)
|
|||
|
|
.cloned()
|
|||
|
|
.collect::<Box<[_]>>()
|
|||
|
|
} else {
|
|||
|
|
query
|
|||
|
|
.responders()
|
|||
|
|
.take_until_secure(responders_based_dht_size_estimate, average_subnets)
|
|||
|
|
.to_vec()
|
|||
|
|
.into_boxed_slice()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
done_get_queries.push(GetQueryOutcome {
|
|||
|
|
id: *id,
|
|||
|
|
closest_nodes,
|
|||
|
|
mutable_outcome: matches!(
|
|||
|
|
query.request.request_type,
|
|||
|
|
RequestTypeSpecific::GetValue(_)
|
|||
|
|
)
|
|||
|
|
.then(|| query.mutable_outcome()),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(done_get_queries, finished_self_findnode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Remove completed GET and PUT queries from internal state.
|
|||
|
|
fn cleanup_done_queries(
|
|||
|
|
&mut self,
|
|||
|
|
done_get: &[GetQueryOutcome],
|
|||
|
|
done_put: &mut Vec<(Id, Result<PutOutcome, PutError>)>,
|
|||
|
|
) {
|
|||
|
|
// Has to happen _before_ `self.socket.recv_from()`.
|
|||
|
|
for done in done_get {
|
|||
|
|
let query = match self.iterative_queries.remove(&done.id) {
|
|||
|
|
Some(query) => query,
|
|||
|
|
None => continue,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
self.update_address_votes_from_iterative_query(&query);
|
|||
|
|
self.cache_iterative_query(&query, &done.closest_nodes);
|
|||
|
|
|
|||
|
|
// Only for get queries, not find node.
|
|||
|
|
if matches!(query.request.request_type, RequestTypeSpecific::FindNode(_)) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let put_query = match self.put_queries.get_mut(&done.id) {
|
|||
|
|
Some(put_query) => put_query,
|
|||
|
|
None => continue,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if put_query.started() {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let Err(error) = put_query.start(&mut self.socket, &done.closest_nodes) {
|
|||
|
|
done_put.push((done.id, Err(error)))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (id, _) in done_put.iter() {
|
|||
|
|
self.put_queries.remove(id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Handle one incoming message, either a request or a response message. One message per tick.
|
|||
|
|
fn handle_message(&mut self) -> Option<(Id, Response)> {
|
|||
|
|
self.socket
|
|||
|
|
.recv_from()
|
|||
|
|
.and_then(|(message, from)| match message.message_type {
|
|||
|
|
MessageType::Request(request_specific) => {
|
|||
|
|
self.handle_request(from, message.transaction_id, request_specific);
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
_ => self.handle_response(from, message),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Check if routing table is empty and log an error if so.
|
|||
|
|
fn log_bootstrap(&self, self_id: &Id) {
|
|||
|
|
let table_size = self.routing_table.size();
|
|||
|
|
if table_size == 0 {
|
|||
|
|
error!("Could not bootstrap the routing table");
|
|||
|
|
} else {
|
|||
|
|
debug!(?self_id, table_size, "Populated the routing table");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct CachedIterativeQuery {
|
|||
|
|
closest_responding_nodes: Box<[Node]>,
|
|||
|
|
dht_size_estimate: f64,
|
|||
|
|
responders_dht_size_estimate: f64,
|
|||
|
|
subnets: u8,
|
|||
|
|
|
|||
|
|
/// Keeping track of find_node queries, because they shouldn't
|
|||
|
|
/// be counted in `responders_based_dht_size_estimates_count`
|
|||
|
|
is_find_node: bool,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// State change after a call to [Rpc::tick], including
|
|||
|
|
/// done PUT, GET, and FIND_NODE queries, as well as any
|
|||
|
|
/// incoming value response for any GET query.
|
|||
|
|
#[derive(Debug, Clone)]
|
|||
|
|
/// Completed GET/FIND_NODE query details returned from [Rpc::tick].
|
|||
|
|
pub struct GetQueryOutcome {
|
|||
|
|
/// Query target id.
|
|||
|
|
pub id: Id,
|
|||
|
|
/// Closest responding nodes discovered by the query.
|
|||
|
|
pub closest_nodes: Box<[Node]>,
|
|||
|
|
/// Mutable GET diagnostics, present for GET value queries.
|
|||
|
|
pub mutable_outcome: Option<GetMutableOutcome>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Debug, Clone)]
|
|||
|
|
pub struct RpcTickReport {
|
|||
|
|
/// All the [Id]s of the done [Rpc::get] queries.
|
|||
|
|
pub done_get_queries: Vec<GetQueryOutcome>,
|
|||
|
|
/// All the [Id]s of the done [Rpc::put] queries,
|
|||
|
|
/// and either the successful [PutOutcome] or a [PutError].
|
|||
|
|
pub done_put_queries: Vec<(Id, Result<PutOutcome, PutError>)>,
|
|||
|
|
/// Received GET query response.
|
|||
|
|
pub new_query_response: Option<(Id, Response)>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Debug, Clone)]
|
|||
|
|
pub enum Response {
|
|||
|
|
Peers(Vec<SocketAddrV4>),
|
|||
|
|
Immutable(Box<[u8]>),
|
|||
|
|
Mutable(MutableItem),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub(crate) fn to_socket_address<T: ToSocketAddrs>(bootstrap: &[T]) -> Vec<SocketAddrV4> {
|
|||
|
|
bootstrap
|
|||
|
|
.iter()
|
|||
|
|
.flat_map(|s| {
|
|||
|
|
s.to_socket_addrs().map(|addrs| {
|
|||
|
|
addrs
|
|||
|
|
.filter_map(|addr| match addr {
|
|||
|
|
SocketAddr::V4(addr_v4) => Some(addr_v4),
|
|||
|
|
_ => None,
|
|||
|
|
})
|
|||
|
|
.collect::<Box<[_]>>()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
.flatten()
|
|||
|
|
.collect()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[cfg(test)]
|
|||
|
|
mod tests {
|
|||
|
|
use std::net::{Ipv4Addr, SocketAddrV4};
|
|||
|
|
|
|||
|
|
use crate::common::FindNodeResponseArguments;
|
|||
|
|
use ed25519_dalek::SigningKey;
|
|||
|
|
|
|||
|
|
use super::*;
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn get_does_not_echo_inflight_mutable_put() {
|
|||
|
|
let mut rpc = Rpc::new(config::Config {
|
|||
|
|
bootstrap: Some(vec![]),
|
|||
|
|
bind_address: Some(Ipv4Addr::LOCALHOST),
|
|||
|
|
..Default::default()
|
|||
|
|
})
|
|||
|
|
.unwrap();
|
|||
|
|
|
|||
|
|
let signer = SigningKey::from_bytes(&[
|
|||
|
|
56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7,
|
|||
|
|
228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103,
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
let item = MutableItem::new(signer, b"value", 1000, None);
|
|||
|
|
let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, None));
|
|||
|
|
let target = *request.target();
|
|||
|
|
|
|||
|
|
rpc.put(request, None).unwrap();
|
|||
|
|
|
|||
|
|
let responses = rpc
|
|||
|
|
.get(
|
|||
|
|
GetRequestSpecific::GetValue(GetValueRequestArguments {
|
|||
|
|
target,
|
|||
|
|
seq: None,
|
|||
|
|
salt: None,
|
|||
|
|
}),
|
|||
|
|
None,
|
|||
|
|
)
|
|||
|
|
.unwrap();
|
|||
|
|
|
|||
|
|
assert!(responses.is_empty());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn mutable_get_counts_wrong_primary_response_as_invalid() {
|
|||
|
|
let mut rpc = Rpc::new(config::Config {
|
|||
|
|
bootstrap: Some(vec![]),
|
|||
|
|
bind_address: Some(Ipv4Addr::LOCALHOST),
|
|||
|
|
..Default::default()
|
|||
|
|
})
|
|||
|
|
.unwrap();
|
|||
|
|
|
|||
|
|
let target = Id::random();
|
|||
|
|
let responder = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 42_000);
|
|||
|
|
|
|||
|
|
rpc.get(
|
|||
|
|
GetRequestSpecific::GetValue(GetValueRequestArguments {
|
|||
|
|
target,
|
|||
|
|
seq: None,
|
|||
|
|
salt: None,
|
|||
|
|
}),
|
|||
|
|
Some(&[responder]),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let response = rpc.handle_response(
|
|||
|
|
responder,
|
|||
|
|
Message {
|
|||
|
|
transaction_id: 0,
|
|||
|
|
message_type: MessageType::Response(ResponseSpecific::FindNode(
|
|||
|
|
FindNodeResponseArguments {
|
|||
|
|
responder_id: Id::random(),
|
|||
|
|
nodes: Vec::new().into_boxed_slice(),
|
|||
|
|
},
|
|||
|
|
)),
|
|||
|
|
version: None,
|
|||
|
|
read_only: false,
|
|||
|
|
requester_ip: None,
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let outcome = rpc
|
|||
|
|
.iterative_queries
|
|||
|
|
.get(&target)
|
|||
|
|
.expect("mutable GET query should still be active")
|
|||
|
|
.mutable_outcome();
|
|||
|
|
|
|||
|
|
assert!(response.is_none());
|
|||
|
|
assert_eq!(outcome.valid_responses(), 0);
|
|||
|
|
assert_eq!(outcome.invalid_responses, 1);
|
|||
|
|
assert_eq!(outcome.responded(), 1);
|
|||
|
|
assert_eq!(outcome.timed_out(), 0);
|
|||
|
|
}
|
|||
|
|
}
|