Files
tsunagi/vendor/mainline/src/dht.rs
T
ab b4f3e57c8d Implement fast userspace multihop mesh routing
Replace the one-intermediate-peer relay with protocol-scoped connectivity
graphs and precomputed shortest-path/ECMP forwarding snapshots. Independent
transport readers forward opaque transit frames without a plugin or TUN
round trip. Carry source, destination, a bounded hop limit and stable flow
tags; preserve end-to-end WireGuard links across topology changes.

Classify IP flows before encryption and preserve their tags through the
WireGuard pending queue. Add offline four-agent path-change coverage, loop
and isolation tests, and an opt-in release forwarding microbenchmark. Bump
control/data ALPNs while preserving persistent network identities and state.

Also include the pending Windows Mainline idle-timeout fix and its regression
test, using a reproducible vendored dependency patch.

Validation: fmt, workspace Clippy with warnings denied, and 307 release tests
passed. Two pre-existing Windows SQLite wipe failures were excluded; public
DHT and the manual benchmark remain ignored by default. The forwarding
microbenchmark measured 103 ns (64 B) and 202 ns (1280 B) per transit packet,
excluding encryption and socket I/O.
2026-09-22 18:08:26 +03:00

1364 lines
45 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Dht node.
use std::{
collections::HashMap,
net::{Ipv4Addr, SocketAddrV4, ToSocketAddrs},
thread,
time::Duration,
};
use flume::{Receiver, Sender, TryRecvError};
use tracing::info;
use crate::{
common::{
hash_immutable, most_recent_mutable_item, AnnouncePeerRequestArguments,
FindNodeRequestArguments, GetPeersRequestArguments, GetValueRequestArguments, Id,
MutableItem, PutImmutableRequestArguments, PutMutableRequestArguments, PutRequestSpecific,
},
rpc::{
to_socket_address, ConcurrencyError, GetMutableOutcome, GetRequestSpecific, Info, PutError,
PutOutcome, PutQueryError, Response, Rpc,
},
Node, ServerSettings,
};
use crate::rpc::config::Config;
#[derive(Debug, Clone)]
/// Mainline Dht node.
pub struct Dht(pub(crate) Sender<ActorMessage>);
#[derive(Debug, Default, Clone)]
/// A builder for the [Dht] node.
pub struct DhtBuilder(Config);
impl DhtBuilder {
/// Set this node's server_mode.
pub fn server_mode(&mut self) -> &mut Self {
self.0.server_mode = true;
self
}
/// Set a custom settings for the node to use at server mode.
///
/// Defaults to [ServerSettings::default]
pub fn server_settings(&mut self, server_settings: ServerSettings) -> &mut Self {
self.0.server_settings = server_settings;
self
}
/// Set bootstrapping nodes.
pub fn bootstrap<T: ToSocketAddrs>(&mut self, bootstrap: &[T]) -> &mut Self {
self.0.bootstrap = Some(to_socket_address(bootstrap));
self
}
/// Add more bootstrap nodes to default bootstrapping nodes.
///
/// Useful when you want to augment the default bootstrapping nodes with
/// dynamic list of nodes you have seen in previous sessions.
pub fn extra_bootstrap<T: ToSocketAddrs>(&mut self, extra_bootstrap: &[T]) -> &mut Self {
let mut bootstrap = self.0.bootstrap.clone().unwrap_or_default();
for address in to_socket_address(extra_bootstrap) {
bootstrap.push(address);
}
self.0.bootstrap = Some(bootstrap);
self
}
/// Remove the existing bootstrapping nodes, usually to create the first node in a new network.
pub fn no_bootstrap(&mut self) -> &mut Self {
self.0.bootstrap = Some(vec![]);
self
}
/// Set an explicit port to listen on.
pub fn port(&mut self, port: u16) -> &mut Self {
self.0.port = Some(port);
self
}
/// A known public IPv4 address for this node to generate
/// a secure node Id from according to [BEP_0042](https://www.bittorrent.org/beps/bep_0042.html)
///
/// Defaults to depending on suggestions from responding nodes.
pub fn public_ip(&mut self, public_ip: Ipv4Addr) -> &mut Self {
self.0.public_ip = Some(public_ip);
self
}
/// UDP socket request timeout duration.
///
/// The longer this duration is, the longer queries take until they are deemeed "done".
/// The shortet this duration is, the more responses from busy nodes we miss out on,
/// which affects the accuracy of queries trying to find closest nodes to a target.
///
/// Defaults to [crate::DEFAULT_REQUEST_TIMEOUT]
pub fn request_timeout(&mut self, request_timeout: Duration) -> &mut Self {
self.0.request_timeout = request_timeout;
self
}
/// Set the address to bind to.
///
/// Defaults to 0.0.0.0 (all interfaces).
pub fn bind_address(&mut self, bind_address: Ipv4Addr) -> &mut Self {
self.0.bind_address = Some(bind_address);
self
}
/// Create a Dht node.
pub fn build(&self) -> Result<Dht, std::io::Error> {
Dht::new(self.0.clone())
}
}
impl Dht {
/// Create a new Dht node.
///
/// Could return an error if it failed to bind to the specified
/// port or other io errors while binding the udp socket.
pub fn new(config: Config) -> Result<Self, std::io::Error> {
let (sender, receiver) = flume::unbounded();
thread::Builder::new()
.name("Mainline Dht actor thread".to_string())
.spawn(move || run(config, receiver))?;
let (tx, rx) = flume::bounded(1);
sender
.send(ActorMessage::Check(tx))
.expect("actor thread unexpectedly shutdown");
rx.recv().expect("actor thread unexpectedly shutdown")?;
Ok(Dht(sender))
}
/// Returns a builder to edit settings before creating a Dht node.
pub fn builder() -> DhtBuilder {
DhtBuilder::default()
}
/// Create a new DHT client with default bootstrap nodes.
pub fn client() -> Result<Self, std::io::Error> {
Dht::builder().build()
}
/// Create a new DHT node that is running in [Server mode][DhtBuilder::server_mode] as
/// soon as possible.
///
/// You shouldn't use this option unless you are sure your
/// DHT node is publicly accessible (not firewalled) _AND_ will be long running,
/// and/or you are running your own local network for testing.
///
/// If you are not sure, use [Self::client] and it will switch
/// to server mode when/if these two conditions are met.
pub fn server() -> Result<Self, std::io::Error> {
Dht::builder().server_mode().build()
}
// === Getters ===
/// Information and statistics about this [Dht] node.
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn info(&self) -> Info {
let (tx, rx) = flume::bounded::<Info>(1);
self.send(ActorMessage::Info(tx));
rx.recv().expect("actor thread unexpectedly shutdown")
}
/// Turn this node's routing table to a list of bootstrapping nodes.
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn to_bootstrap(&self) -> Vec<String> {
let (tx, rx) = flume::bounded::<Vec<String>>(1);
self.send(ActorMessage::ToBootstrap(tx));
rx.recv().expect("actor thread unexpectedly shutdown")
}
// === Public Methods ===
/// Block until the bootstrapping query is done.
///
/// Returns true if the bootstrapping was successful.
#[allow(deprecated)]
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn bootstrapped(&self) -> bool {
let info = self.info();
let nodes = self.find_node(*info.id());
!nodes.is_empty()
}
// === Find nodes ===
/// Returns the closest 20 [secure](Node::is_secure) nodes to a target [Id].
///
/// Mostly useful to crawl the DHT.
///
/// The returned nodes are claims by other nodes, they may be lies, or may have churned
/// since they were last seen, but haven't been pinged yet.
///
/// You might need to ping them to confirm they exist, and responsive, or if you want to
/// learn more about them like the client they are using, or if they support a given BEP.
///
/// If you are trying to find the closest nodes to a target with intent to [Self::put],
/// a request directly to these nodes (using `extra_nodes` parameter), then you should
/// use [Self::get_closest_nodes] instead.
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn find_node(&self, target: Id) -> Box<[Node]> {
let (tx, rx) = flume::bounded::<Box<[Node]>>(1);
self.send(ActorMessage::Get(
GetRequestSpecific::FindNode(FindNodeRequestArguments { target }),
ResponseSender::ClosestNodes(tx),
));
rx.recv()
.expect("Query was dropped before sending a response, please open an issue.")
}
// === Peers ===
/// Get peers for a given infohash.
///
/// Note: each node of the network will only return a _random_ subset (usually 20)
/// of the total peers it has for a given infohash, so if you are getting responses
/// from 20 nodes, you can expect up to 400 peers in total, but if there are more
/// announced peers on that infohash, you are likely to miss some, the logic here
/// for Bittorrent is that any peer will introduce you to more peers through "peer exchange"
/// so if you are implementing something different from Bittorrent, you might want
/// to implement your own logic for gossipping more peers after you discover the first ones.
#[allow(deprecated)]
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn get_peers(&self, info_hash: Id) -> GetIterator<Vec<SocketAddrV4>> {
let (tx, rx) = flume::unbounded::<Vec<SocketAddrV4>>();
self.send(ActorMessage::Get(
GetRequestSpecific::GetPeers(GetPeersRequestArguments { info_hash }),
ResponseSender::Peers(tx),
));
GetIterator(rx.into_iter())
}
/// Announce a peer for a given infohash.
///
/// The peer will be announced on this process IP.
/// If explicit port is passed, it will be used, otherwise the port will be implicitly
/// assumed by remote nodes to be the same ase port they received the request from.
#[allow(deprecated)]
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn announce_peer(&self, info_hash: Id, port: Option<u16>) -> Result<Id, PutQueryError> {
let (port, implied_port) = match port {
Some(port) => (port, None),
None => (0, Some(true)),
};
self.put(
PutRequestSpecific::AnnouncePeer(AnnouncePeerRequestArguments {
info_hash,
port,
implied_port,
}),
None,
)
.map_err(|error| match error {
PutError::Query(error) => error,
PutError::Concurrency(_) => {
unreachable!("should not receive a concurrency error from announce peer query")
}
})
}
// === Immutable data ===
/// Get an Immutable data by its sha1 hash.
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn get_immutable(&self, target: Id) -> Option<Box<[u8]>> {
let (tx, rx) = flume::unbounded::<Box<[u8]>>();
self.send(ActorMessage::Get(
GetRequestSpecific::GetValue(GetValueRequestArguments {
target,
seq: None,
salt: None,
}),
ResponseSender::Immutable(tx),
));
rx.recv().map(Some).unwrap_or(None)
}
/// Put an immutable data to the DHT.
#[allow(deprecated)]
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn put_immutable(&self, value: &[u8]) -> Result<Id, PutQueryError> {
let target: Id = hash_immutable(value).into();
self.put(
PutRequestSpecific::PutImmutable(PutImmutableRequestArguments {
target,
v: value.into(),
}),
None,
)
.map_err(|error| match error {
PutError::Query(error) => error,
PutError::Concurrency(_) => {
unreachable!("should not receive a concurrency error from put immutable query")
}
})
}
// === Mutable data ===
/// Get a mutable data by its `public_key` and optional `salt`.
///
/// You can ask for items `more_recent_than` than a certain `seq`,
/// usually one that you already have seen before, similar to `If-Modified-Since` header in HTTP.
///
/// # Order
///
/// The order of [MutableItem]s returned by this iterator is not guaranteed to
/// reflect their `seq` value. You should not assume that the later items are
/// more recent than earlier ones.
///
/// Consider using [Self::get_mutable_most_recent] if that is what you need.
#[allow(deprecated)]
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn get_mutable(
&self,
public_key: &[u8; 32],
salt: Option<&[u8]>,
more_recent_than: Option<i64>,
) -> GetIterator<MutableItem> {
let salt = salt.map(|s| s.into());
let target = MutableItem::target_from_key(public_key, salt.as_deref());
let (tx, rx) = flume::unbounded::<MutableItem>();
self.send(ActorMessage::Get(
GetRequestSpecific::GetValue(GetValueRequestArguments {
target,
seq: more_recent_than,
salt,
}),
ResponseSender::Mutable(tx),
));
GetIterator(rx.into_iter())
}
/// Get the most recent [MutableItem] from the network.
#[allow(deprecated)]
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn get_mutable_most_recent(
&self,
public_key: &[u8; 32],
salt: Option<&[u8]>,
) -> Option<MutableItem> {
self.get_mutable(public_key, salt, None)
.fold(None::<MutableItem>, most_recent_mutable_item)
}
/// Put a mutable data to the DHT.
///
/// # Lost Update Problem
///
/// As mainline DHT is a distributed system, it is vulnerable to [Write–write conflict](https://en.wikipedia.org/wiki/Write-write_conflict).
///
/// ## Read first
///
/// To mitigate the risk of lost updates, you should call the [Self::get_mutable_most_recent] method
/// then start authoring the new [MutableItem] based on the most recent as in the following example:
///
///```rust
/// use mainline::{Dht, MutableItem, SigningKey, Testnet};
/// use std::net::Ipv4Addr;
///
/// let testnet = Testnet::builder(3).build().unwrap();
/// let dht = Dht::builder()
/// .bootstrap(&testnet.bootstrap)
/// .bind_address(Ipv4Addr::LOCALHOST)
/// .build()
/// .unwrap();
///
/// let signing_key = SigningKey::from_bytes(&[0; 32]);
/// let key = signing_key.verifying_key().to_bytes();
/// let salt = Some(b"salt".as_ref());
///
/// let (item, cas) = if let Some(most_recent) = dht .get_mutable_most_recent(&key, salt) {
/// // 1. Optionally Create a new value to take the most recent's value in consideration.
/// let mut new_value = most_recent.value().to_vec();
/// new_value.extend_from_slice(b" more data");
///
/// // 2. Increment the sequence number to be higher than the most recent's.
/// let most_recent_seq = most_recent.seq();
/// let new_seq = most_recent_seq + 1;
///
/// (
/// MutableItem::new(signing_key, &new_value, new_seq, salt),
/// // 3. Use the most recent [MutableItem::seq] as a `CAS`.
/// Some(most_recent_seq)
/// )
/// } else {
/// (MutableItem::new(signing_key, b"first value", 1, salt), None)
/// };
///
/// dht.put_mutable(item, cas).unwrap();
/// ```
///
/// ## Errors
///
/// In addition to the [PutQueryError] common with all PUT queries, PUT mutable item
/// query has other [Concurrency errors][ConcurrencyError], that try to detect write conflict
/// risks or obvious conflicts.
///
/// If you are lucky to get one of these errors (which is not guaranteed), then you should
/// read the most recent item again, and repeat the steps in the previous example.
#[allow(deprecated)]
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn put_mutable(&self, item: MutableItem, cas: Option<i64>) -> Result<Id, PutMutableError> {
let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, cas));
self.put(request, None).map_err(|error| match error {
PutError::Query(err) => PutMutableError::Query(err),
PutError::Concurrency(err) => PutMutableError::Concurrency(err),
})
}
// === Raw ===
/// Get closet nodes to a specific target, that support [BEP_0044](https://www.bittorrent.org/beps/bep_0044.html).
///
/// Useful to [Self::put] a request to nodes further from the 20 closest nodes to the
/// [PutRequestSpecific::target]. Which itself is useful to circumvent [extreme vertical sybil attacks](https://github.com/pubky/mainline/blob/main/docs/censorship-resistance.md#extreme-vertical-sybil-attacks).
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn get_closest_nodes(&self, target: Id) -> Box<[Node]> {
let (tx, rx) = flume::unbounded::<Box<[Node]>>();
self.send(ActorMessage::Get(
GetRequestSpecific::GetValue(GetValueRequestArguments {
target,
salt: None,
seq: None,
}),
ResponseSender::ClosestNodes(tx),
));
rx.recv()
.expect("Query was dropped before sending a response, please open an issue.")
}
/// Send a PUT request to the closest nodes, and optionally some extra nodes.
///
/// This is useful to put data to regions of the DHT other than the closest nodes
/// to this request's [target][PutRequestSpecific::target].
///
/// You can find nodes close to other regions of the network by calling
/// [Self::get_closest_nodes] with the target that you want to find the closest nodes to.
///
/// Note: extra nodes need to have [Node::valid_token].
#[deprecated(note = "use the async API via Dht::as_async() instead")]
pub fn put(
&self,
request: PutRequestSpecific,
extra_nodes: Option<Box<[Node]>>,
) -> Result<Id, PutError> {
self.put_inner(request, extra_nodes)
.recv()
.expect("Query was dropped before sending a response, please open an issue.")
.map(|outcome| outcome.target)
}
// === Private Methods ===
pub(crate) fn put_inner(
&self,
request: PutRequestSpecific,
extra_nodes: Option<Box<[Node]>>,
) -> flume::Receiver<Result<PutOutcome, PutError>> {
let (tx, rx) = flume::bounded::<Result<PutOutcome, PutError>>(1);
self.send(ActorMessage::Put(request, tx, extra_nodes));
rx
}
pub(crate) fn send(&self, message: ActorMessage) {
self.0
.send(message)
.expect("actor thrread unexpectedly shutdown");
}
}
#[deprecated(note = "use the async stream returned by AsyncDht instead")]
pub struct GetIterator<T>(flume::IntoIter<T>);
#[allow(deprecated)]
impl<T> Iterator for GetIterator<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
fn run(config: Config, receiver: Receiver<ActorMessage>) {
match Rpc::new(config) {
Ok(mut rpc) => {
let address = rpc.local_addr();
info!(?address, "Mainline DHT listening");
let mut put_senders = HashMap::new();
let mut get_senders = HashMap::new();
loop {
match receiver.try_recv() {
Ok(actor_message) => match actor_message {
ActorMessage::Check(sender) => {
let _ = sender.send(Ok(()));
}
ActorMessage::Info(sender) => {
let _ = sender.send(rpc.info());
}
ActorMessage::Put(request, sender, extra_nodes) => {
let target = *request.target();
match rpc.put(request, extra_nodes) {
Ok(()) => {
let senders = put_senders.entry(target).or_insert(vec![]);
senders.push(sender);
}
Err(error) => {
let _ = sender.send(Err(error));
}
};
}
ActorMessage::Get(request, sender) => {
let target = *request.target();
if let Some(responses) = rpc.get(request, None) {
for response in responses {
send(&sender, response);
}
};
let senders = get_senders.entry(target).or_insert(vec![]);
senders.push(sender);
}
ActorMessage::ToBootstrap(sender) => {
let _ = sender.send(rpc.routing_table().to_bootstrap());
}
ActorMessage::SeedRouting(nodes, sender) => {
for node in nodes {
rpc.routing_table_mut().add(node);
}
let _ = sender.send(());
}
},
Err(TryRecvError::Disconnected) => {
// Node was dropped, kill this thread.
tracing::debug!("mainline::Dht's actor thread was shutdown after Drop.");
break;
}
Err(TryRecvError::Empty) => {
// No op
}
}
let report = rpc.tick();
// Response for an ongoing GET query
if let Some((target, response)) = report.new_query_response {
if let Some(senders) = get_senders.get(&target) {
for sender in senders {
send(sender, response.clone());
}
}
}
// Cleanup done GET queries
for done in report.done_get_queries {
if let Some(senders) = get_senders.remove(&done.id) {
for sender in senders {
match sender {
ResponseSender::ClosestNodes(sender) => {
let _ = sender.send(done.closest_nodes.clone());
}
ResponseSender::MutableDetailed { outcome, .. } => {
let _ = outcome
.send(done.mutable_outcome.clone().unwrap_or_default());
}
ResponseSender::Peers(_)
| ResponseSender::Mutable(_)
| ResponseSender::Immutable(_) => {}
}
}
}
}
// Cleanup done PUT query and send its final result.
for (id, result) in report.done_put_queries {
if let Some(senders) = put_senders.remove(&id) {
for sender in senders {
let _ = sender.send(result.clone());
}
}
}
}
}
Err(err) => {
if let Ok(ActorMessage::Check(sender)) = receiver.try_recv() {
let _ = sender.send(Err(err));
}
}
};
}
fn send(sender: &ResponseSender, response: Response) {
match (sender, response) {
(ResponseSender::Peers(s), Response::Peers(r)) => {
let _ = s.send(r);
}
(ResponseSender::Mutable(s), Response::Mutable(r)) => {
let _ = s.send(r);
}
(ResponseSender::MutableDetailed { values, .. }, Response::Mutable(r)) => {
let _ = values.send(r);
}
(ResponseSender::Immutable(s), Response::Immutable(r)) => {
let _ = s.send(r);
}
_ => {}
}
}
#[derive(Debug)]
pub(crate) enum ActorMessage {
Info(Sender<Info>),
Put(
PutRequestSpecific,
Sender<Result<PutOutcome, PutError>>,
Option<Box<[Node]>>,
),
Get(GetRequestSpecific, ResponseSender),
Check(Sender<Result<(), std::io::Error>>),
ToBootstrap(Sender<Vec<String>>),
SeedRouting(Vec<Node>, Sender<()>),
}
#[derive(Debug, Clone)]
pub enum ResponseSender {
ClosestNodes(Sender<Box<[Node]>>),
Peers(Sender<Vec<SocketAddrV4>>),
Mutable(Sender<MutableItem>),
MutableDetailed {
values: Sender<MutableItem>,
outcome: Sender<GetMutableOutcome>,
},
Immutable(Sender<Box<[u8]>>),
}
/// Builder for creating a [Testnet] with custom configuration.
///
/// # Defaults
///
/// - `bind_address`: `127.0.0.1` (localhost)
/// - `seeded`: `true` - nodes start with fully populated routing tables
///
/// # Example
///
/// ```ignore
/// use std::net::Ipv4Addr;
/// use mainline::Testnet;
///
/// // Use localhost (default)
/// let testnet = Testnet::builder(3).build().unwrap();
///
/// // Use all interfaces (0.0.0.0)
/// let testnet = Testnet::builder(3)
/// .bind_address(Ipv4Addr::UNSPECIFIED)
/// .build()
/// .unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct TestnetBuilder {
count: usize,
bind_address: Ipv4Addr,
seeded: bool,
}
impl TestnetBuilder {
/// Create a new builder with the specified number of nodes.
///
/// # Defaults
///
/// - `bind_address`: `127.0.0.1` (localhost)
/// - `seeded`: `true`
pub fn new(count: usize) -> Self {
Self {
count,
bind_address: Ipv4Addr::LOCALHOST,
seeded: true,
}
}
/// Set the address to bind all nodes to.
///
/// Defaults to `127.0.0.1` (localhost).
/// Use `Ipv4Addr::UNSPECIFIED` (`0.0.0.0`) to bind to all interfaces.
pub fn bind_address(&mut self, bind_address: Ipv4Addr) -> &mut Self {
self.bind_address = bind_address;
self
}
/// Whether to pre-seed routing tables with all nodes.
///
/// Defaults to `true`.
///
/// When `true`, all nodes start with fully populated routing tables.
/// When `false`, nodes bootstrap from each other which is faster at startup
/// but may not have immediate full connectivity.
pub fn seeded(&mut self, seeded: bool) -> &mut Self {
self.seeded = seeded;
self
}
/// Build the testnet.
///
/// Nodes will be bound to the configured `bind_address` (default: `127.0.0.1`).
///
/// This will block until all nodes are created (and seeded if `seeded` is true).
pub fn build(&self) -> Result<Testnet, std::io::Error> {
if self.seeded {
Testnet::build_seeded(self.count, self.bind_address)
} else {
Testnet::build_unseeded(self.count, self.bind_address)
}
}
}
/// Create a testnet of Dht nodes to run tests against instead of the real mainline network.
///
/// # Bind Address
///
/// The convenience methods ([`Self::new`], [`Self::new_unseeded`], etc.) bind to `0.0.0.0`
/// for backwards compatibility. Use [`Self::builder`] to bind to a different address.
// TODO(breaking): In the next major version, change the default bind address from
// `0.0.0.0` to `127.0.0.1` for better cross-platform compatibility (especially macOS).
#[derive(Debug)]
pub struct Testnet {
/// bootstrapping nodes for this testnet.
pub bootstrap: Vec<String>,
/// all nodes in this testnet
pub nodes: Vec<Dht>,
}
// TODO(breaking): In the next major version, change `new()` and related methods to bind
// to `127.0.0.1` instead of `0.0.0.0` for better macOS compatibility. The builder already
// defaults to `127.0.0.1`.
impl Testnet {
/// Returns a builder to configure and create a [Testnet].
///
/// The builder defaults to binding to `127.0.0.1` (localhost).
///
/// # Example
///
/// ```ignore
/// let testnet = Testnet::builder(3).build().unwrap();
/// ```
pub fn builder(count: usize) -> TestnetBuilder {
TestnetBuilder::new(count)
}
/// Create a new testnet with a certain size.
///
/// Note: this network will be shutdown as soon as this struct
/// gets dropped, if you want the network to be `'static`, then
/// you should call [Self::leak].
///
/// This will block until all nodes are seeded with local peers.
/// If you are using an async runtime, consider using [Self::new_async].
///
/// # Bind Address
///
/// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] to bind to
/// a different address.
pub fn new(count: usize) -> Result<Testnet, std::io::Error> {
Testnet::build_seeded(count, Ipv4Addr::UNSPECIFIED)
}
/// Create a new testnet without pre-seeding routing tables.
///
/// This is faster at startup, but nodes will not start with fully populated routing tables.
/// Use this when your tests do not require immediate full connectivity.
///
/// # Bind Address
///
/// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] with `.seeded(false)`
/// to bind to a different address.
pub fn new_unseeded(count: usize) -> Result<Testnet, std::io::Error> {
Testnet::build_unseeded(count, Ipv4Addr::UNSPECIFIED)
}
#[cfg(feature = "async")]
/// Similar to [Self::new], but available for async contexts.
///
/// # Bind Address
///
/// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] to bind to
/// a different address.
pub async fn new_async(count: usize) -> Result<Testnet, std::io::Error> {
Testnet::build_seeded(count, Ipv4Addr::UNSPECIFIED)
}
#[cfg(feature = "async")]
/// Similar to [Self::new_unseeded], but available for async contexts.
///
/// # Bind Address
///
/// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] with `.seeded(false)`
/// to bind to a different address.
pub async fn new_unseeded_async(count: usize) -> Result<Testnet, std::io::Error> {
Testnet::build_unseeded(count, Ipv4Addr::UNSPECIFIED)
}
#[allow(deprecated)]
fn build_seeded(count: usize, bind_address: Ipv4Addr) -> Result<Testnet, std::io::Error> {
let mut nodes = Vec::with_capacity(count);
for _ in 0..count {
let node = Dht::builder()
.server_mode()
.no_bootstrap()
.bind_address(bind_address)
.build()?;
nodes.push(node);
}
let infos: Vec<_> = nodes.iter().map(|node| node.info()).collect();
let bootstrap = infos
.iter()
.map(|info| info.local_addr().to_string())
.collect::<Vec<_>>();
let seeded_nodes: Vec<_> = infos
.iter()
.map(|info| Node::new(*info.id(), info.local_addr()))
.collect();
for (node, info) in nodes.iter().zip(infos.iter()) {
let peers = seeded_nodes
.iter()
.filter(|peer| peer.id() != info.id())
.cloned()
.collect::<Vec<_>>();
let (tx, rx) = flume::bounded(1);
node.send(ActorMessage::SeedRouting(peers, tx));
let _ = rx.recv();
}
Ok(Self { bootstrap, nodes })
}
#[allow(deprecated)]
fn build_unseeded(count: usize, bind_address: Ipv4Addr) -> Result<Testnet, std::io::Error> {
let mut nodes = Vec::with_capacity(count);
let mut bootstrap = Vec::new();
for i in 0..count {
if i == 0 {
let node = Dht::builder()
.server_mode()
.no_bootstrap()
.bind_address(bind_address)
.build()?;
let info = node.info();
bootstrap.push(info.local_addr().to_string());
nodes.push(node);
} else {
let node = Dht::builder()
.server_mode()
.bootstrap(&bootstrap)
.bind_address(bind_address)
.build()?;
nodes.push(node);
}
}
Ok(Self { bootstrap, nodes })
}
/// By default as soon as this testnet gets dropped,
/// all the nodes get dropped and the entire network is shutdown.
///
/// This method uses [Box::leak] to keep nodes running, which is
/// useful if you need to keep running the testnet in the process
/// even if this struct gets dropped.
pub fn leak(&self) {
for node in self.nodes.clone() {
Box::leak(Box::new(node));
}
}
}
#[derive(thiserror::Error, Debug)]
/// Put MutableItem errors.
pub enum PutMutableError {
#[error(transparent)]
/// Common PutQuery errors
Query(#[from] PutQueryError),
#[error(transparent)]
/// PutQuery for [crate::MutableItem] errors
Concurrency(#[from] ConcurrencyError),
}
#[cfg(test)]
mod test {
#![allow(deprecated)]
use std::net::Ipv4Addr;
use std::str::FromStr;
use ed25519_dalek::SigningKey;
use crate::rpc::ConcurrencyError;
use super::*;
#[test]
fn bind_twice() {
let a = Dht::client().unwrap();
let result = Dht::builder()
.port(a.info().local_addr().port())
.server_mode()
.build();
assert!(result.is_err());
}
#[test]
fn announce_get_peer() {
let testnet = Testnet::builder(10).build().unwrap();
let a = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let b = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let info_hash = Id::random();
a.announce_peer(info_hash, Some(45555))
.expect("failed to announce");
let peers = b.get_peers(info_hash).next().expect("No peers");
assert_eq!(peers.first().unwrap().port(), 45555);
}
#[test]
fn put_get_immutable() {
let testnet = Testnet::builder(10).build().unwrap();
let a = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let b = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let value = b"Hello World!";
let expected_target = Id::from_str("e5f96f6f38320f0f33959cb4d3d656452117aadb").unwrap();
let target = a.put_immutable(value).unwrap();
assert_eq!(target, expected_target);
let response = b.get_immutable(target).unwrap();
assert_eq!(response, value.to_vec().into_boxed_slice());
}
#[test]
fn find_node_no_values() {
let client = Dht::builder().no_bootstrap().build().unwrap();
client.find_node(Id::random());
}
#[test]
fn put_get_immutable_no_values() {
let client = Dht::builder().no_bootstrap().build().unwrap();
assert_eq!(client.get_immutable(Id::random()), None);
}
#[test]
fn put_get_mutable() {
let testnet = Testnet::builder(10).build().unwrap();
let a = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let b = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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 seq = 1000;
let value = b"Hello World!";
let item = MutableItem::new(signer.clone(), value, seq, None);
a.put_mutable(item.clone(), None).unwrap();
let response = b
.get_mutable(signer.verifying_key().as_bytes(), None, None)
.next()
.expect("No mutable values");
assert_eq!(&response, &item);
}
#[test]
fn put_get_mutable_no_more_recent_value() {
let testnet = Testnet::builder(10).build().unwrap();
let a = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let b = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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 seq = 1000;
let value = b"Hello World!";
let item = MutableItem::new(signer.clone(), value, seq, None);
a.put_mutable(item.clone(), None).unwrap();
let response = b
.get_mutable(signer.verifying_key().as_bytes(), None, Some(seq))
.next();
assert!(&response.is_none());
}
#[test]
fn get_mutable_most_recent_prefers_highest_seq() {
let testnet = Testnet::builder(10).build().unwrap();
let client = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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 newer = MutableItem::new(signer.clone(), b"newer", 1001, None);
client.put_mutable(newer.clone(), None).unwrap();
let older = MutableItem::new(signer, b"older", 1000, None);
let (sender, _) = flume::bounded::<Result<PutOutcome, PutError>>(1);
let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(older, None));
client
.0
.send(ActorMessage::Put(request, sender, None))
.unwrap();
let most_recent = client
.get_mutable_most_recent(newer.key(), None)
.expect("No mutable values");
assert_eq!(most_recent.seq(), newer.seq());
assert_eq!(most_recent.value(), newer.value());
}
#[test]
fn repeated_put_query() {
let testnet = Testnet::builder(10).build().unwrap();
let a = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let id = a.put_immutable(&[1, 2, 3]).unwrap();
assert_eq!(a.put_immutable(&[1, 2, 3]).unwrap(), id);
}
#[test]
fn concurrent_get_mutable() {
let testnet = Testnet::builder(10).build().unwrap();
let a = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let b = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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 key = signer.verifying_key().to_bytes();
let seq = 1000;
let value = b"Hello World!";
let item = MutableItem::new(signer.clone(), value, seq, None);
a.put_mutable(item.clone(), None).unwrap();
let _response_first = b
.get_mutable(&key, None, None)
.next()
.expect("No mutable values");
let response_second = b
.get_mutable(&key, None, None)
.next()
.expect("No mutable values");
assert_eq!(&response_second, &item);
}
#[test]
fn concurrent_put_mutable_same() {
let testnet = Testnet::builder(10).build().unwrap();
let client = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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 seq = 1000;
let value = b"Hello World!";
let item = MutableItem::new(signer.clone(), value, seq, None);
let mut handles = vec![];
for _ in 0..2 {
let client = client.clone();
let item = item.clone();
let handle = std::thread::spawn(move || client.put_mutable(item, None).unwrap());
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
#[test]
fn concurrent_put_mutable_different() {
let testnet = Testnet::builder(10).build().unwrap();
let client = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.unwrap();
let mut handles = vec![];
for i in 0..2 {
let client = client.clone();
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 seq = 1000;
let mut value = b"Hello World!".to_vec();
value.push(i);
let item = MutableItem::new(signer.clone(), &value, seq, None);
let handle = std::thread::spawn(move || {
let result = client.put_mutable(item, None);
if i == 0 {
assert!(result.is_ok())
} else {
assert!(matches!(
result,
Err(PutMutableError::Concurrency(ConcurrencyError::ConflictRisk))
))
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
#[test]
fn concurrent_put_mutable_different_with_cas() {
let testnet = Testnet::builder(10).build().unwrap();
let client = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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,
]);
// First
{
let item = MutableItem::new(signer.clone(), &[], 1000, None);
let (sender, _) = flume::bounded::<Result<PutOutcome, PutError>>(1);
let request =
PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, None));
client
.0
.send(ActorMessage::Put(request, sender, None))
.unwrap();
}
std::thread::sleep(Duration::from_millis(100));
// Second
{
let item = MutableItem::new(signer, &[], 1001, None);
client.put_mutable(item, Some(1000)).unwrap();
}
}
#[test]
fn conflict_302_seq_less_than_current() {
let testnet = Testnet::builder(10).build().unwrap();
let client = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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,
]);
client
.put_mutable(MutableItem::new(signer.clone(), &[], 1001, None), None)
.unwrap();
assert!(matches!(
client.put_mutable(MutableItem::new(signer, &[], 1000, None), None),
Err(PutMutableError::Concurrency(
ConcurrencyError::NotMostRecent
))
));
}
#[test]
fn conflict_301_cas() {
let testnet = Testnet::builder(10).build().unwrap();
let client = Dht::builder()
.bootstrap(&testnet.bootstrap)
.bind_address(Ipv4Addr::LOCALHOST)
.build()
.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,
]);
client
.put_mutable(MutableItem::new(signer.clone(), &[], 1001, None), None)
.unwrap();
assert!(matches!(
client.put_mutable(MutableItem::new(signer, &[], 1002, None), Some(1000)),
Err(PutMutableError::Concurrency(ConcurrencyError::CasFailed))
));
}
#[test]
fn populate_bootstrapping_node_routing_table() {
let size = 3;
let testnet = Testnet::builder(size).build().unwrap();
assert!(testnet
.nodes
.iter()
.all(|n| n.to_bootstrap().len() == size - 1));
}
}