//! 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, 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, // Active IterativeQueries iterative_queries: HashMap, /// 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, /// 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, 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 { 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 { 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>, ) -> 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> { 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, Vec) { 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) { 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)> { 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::>() } 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)>, ) { // 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, } #[derive(Debug, Clone)] pub struct RpcTickReport { /// All the [Id]s of the done [Rpc::get] queries. pub done_get_queries: Vec, /// 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)>, /// Received GET query response. pub new_query_response: Option<(Id, Response)>, } #[derive(Debug, Clone)] pub enum Response { Peers(Vec), Immutable(Box<[u8]>), Mutable(MutableItem), } pub(crate) fn to_socket_address(bootstrap: &[T]) -> Vec { 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::>() }) }) .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); } }