summaryrefslogtreecommitdiffstats
path: root/src/transport/identity.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/transport/identity.rs')
-rw-r--r--src/transport/identity.rs748
1 files changed, 748 insertions, 0 deletions
diff --git a/src/transport/identity.rs b/src/transport/identity.rs
new file mode 100644
index 0000000..94faded
--- /dev/null
+++ b/src/transport/identity.rs
@@ -0,0 +1,748 @@
+//! The identity layer: between the node and its transports, whose identities are
+//! addresses (Mycelium's public keys), so that the rest of the node deals in node IDs
+//! (`crate::identity`) only, whatever transport reaches a peer. A node may have several
+//! transports; a peer is reached on one both have, so nodes on several transports
+//! carry the traffic between the others.
+//!
+//! A node proves who it is to an address with WHO (which asks back) or ME (the answer):
+//! its public node key, and a signature over its own address and the recipient's
+//! (`signed`). The transport authenticates the sender's address, so a proof is good for
+//! that pair of addresses only: replayed from another address, to another recipient or
+//! on another transport, it fails. Once a node proved itself at an address, messages
+//! from there are its.
+//!
+//! * A message from an address no node proved itself at is dropped, and the address is
+//! asked WHO (at most every `ASK_INTERVAL`): after a restart, the first message from
+//! a peer brings its proof back, and the peer's node retries what was lost.
+//! * A seed (`resolve_seed`) is an address, or something one of the transports resolves
+//! to one; it is asked WHO, and its ME says which node is there.
+//! * Where other nodes are comes from the member list and HELLO_REPLYs
+//! (`learn_addresses`): hints, not proofs. A message to a node goes to the addresses
+//! it proved itself at, best first (`Paths::rank`, `Paths::send_first`: the next
+//! when a send fails); with only hints, those are asked WHO, and the send is refused
+//! until one answers (the node retries refused sends). A node proven on some
+//! transports is asked WHO at its hints on the others too, now and then
+//! (`OTHER_PATHS_ASK_INTERVAL`), so that each transport both have becomes a path.
+//!
+//! Addresses and the transports behind them are the paths layer's
+//! (`transport::paths`): the books keep addresses as paths keep them.
+
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex, MutexGuard};
+use std::time::Duration;
+
+use anyhow::{Result, anyhow, bail};
+use async_trait::async_trait;
+use tokio::sync::oneshot;
+use tokio::time::Instant;
+use tracing::{debug, info};
+
+use crate::identity::{self, NodeKey};
+use crate::locks::MutexExt;
+use crate::transport::firewall::Firewall;
+use crate::transport::paths::{Gate, PathHandler, Paths, scheme_of};
+use crate::transport::stats::{IdentityCounts, IdentityStats, TransportStats};
+use crate::transport::traffic::Traffic;
+use crate::transport::{
+ Destination, EncodedMessage, HandlerFn, IdentityProof, MessageHandler, MessageKind,
+ ParsedMessage, Transport,
+};
+
+pub(crate) const WHO_LABEL: &[u8] = b"continuum/who/1";
+pub(crate) const ME_LABEL: &[u8] = b"continuum/me/1";
+/// An address is asked WHO at most this often, unless a seed is being resolved.
+const ASK_INTERVAL: Duration = Duration::from_secs(5);
+/// A node's addresses on transports it hasn't proven itself on are asked WHO at most
+/// this often (`Book::unproven_hints`): over Tor, a WHO is a dial that takes seconds,
+/// and an address that doesn't answer shouldn't be pressed.
+const OTHER_PATHS_ASK_INTERVAL: Duration = Duration::from_secs(60);
+/// How long resolving a seed waits for its ME.
+const RESOLVE_TIMEOUT: Duration = Duration::from_secs(10);
+/// Addresses kept per node from hints: about one per transport.
+const MAX_HINTS: usize = 8;
+/// Addresses with a proven node, nodes with hints, and addresses asked, each at most:
+/// anyone can send us proofs (keys are free), so the books are bounded.
+const MAX_ENTRIES: usize = 4096;
+
+/// A send refused because the peer hasn't proven where it is yet (`send_encoded`):
+/// expected when a peer known only from hints is first contacted, and retried.
+#[derive(Debug, thiserror::Error)]
+#[error("{0} hasn't proven where it is yet; asked where it is said to be")]
+pub(crate) struct NotProven(String);
+
+/// Whether `error` is (or was caused by) a [`NotProven`].
+pub(crate) fn is_not_proven(error: &anyhow::Error) -> bool {
+ error.chain().any(|cause| cause.is::<NotProven>())
+}
+
+/// What a node under the identity layer needs (`node::NodeConfig::identity`).
+#[derive(Clone)]
+pub(crate) struct IdentityConfig {
+ pub(crate) key: Arc<NodeKey>,
+ /// The address scheme of the node's transport (`NodeConfig::transport`), e.g.
+ /// `mycelium`.
+ pub(crate) scheme: &'static str,
+ /// Further transports, by scheme, after the first in preference.
+ pub(crate) more: Vec<(&'static str, Arc<dyn Transport>)>,
+}
+
+/// The bytes a WHO (`label`: `WHO_LABEL`) or ME proof signs: the label, then the
+/// sender's and the recipient's address, each with its length.
+pub(crate) fn signed(label: &[u8], from: &str, to: &str) -> Vec<u8> {
+ let mut bytes = label.to_vec();
+ for address in [from, to] {
+ bytes.extend_from_slice(&(address.len() as u32).to_be_bytes());
+ bytes.extend_from_slice(address.as_bytes());
+ }
+ bytes
+}
+
+pub(crate) struct IdentityTransport {
+ shared: Arc<Shared>,
+}
+
+struct Shared {
+ paths: Arc<Paths>,
+ /// The debug firewall, by node ID; paths applies it (`Gate`).
+ firewall: Option<Arc<Firewall>>,
+ key: Arc<NodeKey>,
+ node_id: String,
+ /// WHO and ME are counted here, under the address (`traffic`).
+ traffic: Arc<Traffic>,
+ // A std mutex: every access is short and never spans an await.
+ book: Mutex<Book>,
+}
+
+/// Who is where, by address.
+#[derive(Default)]
+struct Book {
+ /// The node that proved itself at an address, and when.
+ at: HashMap<String, (String, Instant)>,
+ /// The addresses each node last proved itself at, one per scheme.
+ proven: HashMap<String, Vec<String>>,
+ /// Where others say a node is.
+ hints: HashMap<String, Vec<String>>,
+ /// When each address was last asked WHO.
+ asked: HashMap<String, Instant>,
+ /// Seeds being resolved: who waits for a ME from where.
+ waiting: HashMap<String, Vec<oneshot::Sender<String>>>,
+ /// Since start (`stats`).
+ counts: IdentityCounts,
+}
+
+impl Book {
+ /// `node` proved itself at `address`. Returns whether that is news.
+ fn bind(&mut self, address: &str, node: &str, now: Instant) -> bool {
+ let news = self.at.get(address).is_none_or(|(known, _)| known != node);
+ // One address per node and scheme, and one node per address: a node that
+ // moved, or an address taken over by another node, leaves no stale entry.
+ let scheme = scheme_of(address);
+ let addresses = self.proven.entry(node.to_owned()).or_default();
+ if let Some(slot) = addresses.iter_mut().find(|at| scheme_of(at) == scheme) {
+ if *slot != address {
+ let previous = std::mem::replace(slot, address.to_owned());
+ self.at.remove(&previous);
+ self.counts.address_changes += 1;
+ }
+ } else {
+ addresses.push(address.to_owned());
+ }
+ if let Some((previous, _)) = self.at.insert(address.to_owned(), (node.to_owned(), now))
+ && previous != node
+ {
+ self.forget_proof(&previous, address);
+ }
+ if self.at.len() > MAX_ENTRIES
+ && let Some(oldest) = self
+ .at
+ .iter()
+ .min_by_key(|(_, (_, at))| *at)
+ .map(|(address, _)| address.clone())
+ && let Some((node, _)) = self.at.remove(&oldest)
+ {
+ self.forget_proof(&node, &oldest);
+ }
+ for waiter in self.waiting.remove(address).into_iter().flatten() {
+ let _ = waiter.send(node.to_owned());
+ }
+ news
+ }
+
+ fn forget_proof(&mut self, node: &str, address: &str) {
+ if let Some(addresses) = self.proven.get_mut(node) {
+ addresses.retain(|at| at != address);
+ if addresses.is_empty() {
+ self.proven.remove(node);
+ }
+ }
+ }
+
+ /// What the books hold now, and the counts.
+ fn stats(&self) -> IdentityStats {
+ let mut proven = std::collections::BTreeMap::new();
+ for addresses in self.proven.values() {
+ for address in addresses {
+ *proven.entry(scheme_of(address).to_owned()).or_insert(0) += 1;
+ }
+ }
+ IdentityStats {
+ proven,
+ hinted: self
+ .hints
+ .keys()
+ .filter(|node| !self.proven.contains_key(*node))
+ .count(),
+ counts: self.counts,
+ }
+ }
+
+ /// Where `node` is said to be on transports it hasn't proven itself on yet.
+ fn unproven_hints(&self, node: &str) -> Vec<String> {
+ let proven = self.proven.get(node);
+ self.hints
+ .get(node)
+ .into_iter()
+ .flatten()
+ .filter(|hint| {
+ proven.is_none_or(|proven| {
+ !proven
+ .iter()
+ .any(|address| scheme_of(address) == scheme_of(hint))
+ })
+ })
+ .cloned()
+ .collect()
+ }
+
+ /// Whether to ask `address` WHO now (always, if `force`), noting that it was asked.
+ fn ask(&mut self, address: &str, now: Instant, force: bool) -> bool {
+ let interval = if force { Duration::ZERO } else { ASK_INTERVAL };
+ self.ask_every(address, now, interval)
+ }
+
+ /// Whether to ask `address` WHO now, if it wasn't within `interval`, noting that it
+ /// was asked.
+ fn ask_every(&mut self, address: &str, now: Instant, interval: Duration) -> bool {
+ if self
+ .asked
+ .get(address)
+ .is_some_and(|at| now.duration_since(*at) < interval)
+ {
+ return false;
+ }
+ if self.asked.len() >= MAX_ENTRIES {
+ // The longest interval: other paths' asks are kept until they are due again.
+ self.asked
+ .retain(|_, at| now.duration_since(*at) < OTHER_PATHS_ASK_INTERVAL);
+ }
+ self.asked.insert(address.to_owned(), now);
+ true
+ }
+}
+
+impl IdentityTransport {
+ /// Over `transport`, whose addresses have `config.scheme`, and `config.more`.
+ /// `firewall`: the debug firewall, if any, which paths applies by address.
+ pub(crate) fn new(
+ transport: Arc<dyn Transport>,
+ config: IdentityConfig,
+ traffic: Arc<Traffic>,
+ firewall: Option<Arc<Firewall>>,
+ ) -> Self {
+ let node_id = config.key.id();
+ let shared = Arc::new_cyclic(|shared: &std::sync::Weak<Shared>| {
+ let gate: std::sync::Weak<dyn Gate> = shared.clone();
+ Shared {
+ paths: Arc::new(Paths::new(
+ std::iter::once((config.scheme, transport))
+ .chain(config.more)
+ .collect(),
+ Some(Arc::clone(&traffic)),
+ firewall.is_some().then_some(gate),
+ )),
+ firewall,
+ key: config.key,
+ node_id,
+ traffic,
+ book: Mutex::default(),
+ }
+ });
+ Self { shared }
+ }
+}
+
+/// The firewall's rules are by node ID: an address is blocked if the node proven at it
+/// is. Addresses no node proved itself at aren't (nothing could name them).
+impl Gate for Shared {
+ fn blocks_inbound(&self, address: &str) -> bool {
+ self.blocks(address, Firewall::blocks_inbound)
+ }
+
+ fn blocks_outbound(&self, address: &str) -> bool {
+ self.blocks(address, Firewall::blocks_outbound)
+ }
+}
+
+impl Shared {
+ fn blocks(&self, address: &str, rule: fn(&Firewall, &str) -> bool) -> bool {
+ let Some(firewall) = &self.firewall else {
+ return false;
+ };
+ let node = self.lock().at.get(address).map(|(node, _)| node.clone());
+ node.is_some_and(|node| rule(firewall, &node))
+ }
+}
+
+impl Shared {
+ fn lock(&self) -> MutexGuard<'_, Book> {
+ self.book.lock_or_recover()
+ }
+
+ /// Our proof for `address` (as paths keep it), from our address on its transport.
+ fn proof(&self, label: &[u8], address: &str) -> Option<IdentityProof> {
+ let message = signed(label, &self.paths.own_address(address)?, address);
+ Some(IdentityProof {
+ key: self.key.public(),
+ signature: self.key.sign(&message),
+ })
+ }
+
+ /// Notes the node `proof` proves to be at `address` (as paths keep it); None if it
+ /// proves nothing.
+ fn prove(&self, address: &str, label: &[u8], proof: &IdentityProof) -> Option<String> {
+ let message = signed(label, address, &self.paths.own_address(address)?);
+ if !identity::verify(&proof.key, &message, &proof.signature) {
+ self.lock().counts.proofs_rejected += 1;
+ debug!(from = %address, "Ignoring an identity proof that doesn't hold");
+ return None;
+ }
+ let node = identity::node_id_of(&proof.key);
+ if node == self.node_id {
+ debug!(from = %address, "Ignoring a proof of our own node key");
+ return None;
+ }
+ if self.lock().bind(address, &node, Instant::now()) {
+ debug!(node = %node, address = %address, "Node proved its address");
+ }
+ Some(node)
+ }
+
+ /// Asks `address` WHO, unless it was asked recently (`force`: anyway).
+ fn ask(self: &Arc<Self>, address: &str, force: bool) {
+ if !self.lock().ask(address, Instant::now(), force) {
+ return;
+ }
+ self.send_who(address);
+ }
+
+ /// Asks `node`'s addresses on transports it hasn't proven itself on WHO, now and then
+ /// (`OTHER_PATHS_ASK_INTERVAL`): its ME there proves another path to it, which the
+ /// paths layer then measures, and fails over to.
+ fn ask_other_paths(self: &Arc<Self>, node: &str) {
+ let due: Vec<String> = {
+ let mut book = self.lock();
+ let now = Instant::now();
+ book.unproven_hints(node)
+ .into_iter()
+ .filter(|address| book.ask_every(address, now, OTHER_PATHS_ASK_INTERVAL))
+ .collect()
+ };
+ for address in due {
+ self.send_who(&address);
+ }
+ }
+
+ fn send_who(self: &Arc<Self>, address: &str) {
+ let shared = Arc::clone(self);
+ let address = address.to_owned();
+ tokio::spawn(async move { shared.send_proof(MessageKind::Who, &address).await });
+ }
+
+ /// Sends a WHO or ME (`kind`) to `address`.
+ async fn send_proof(&self, kind: MessageKind, address: &str) {
+ let Some(address) = self.paths.canonical(address) else {
+ return;
+ };
+ let address = address.as_str();
+ let message = if kind == MessageKind::Who {
+ self.proof(WHO_LABEL, address).map(ParsedMessage::Who)
+ } else {
+ self.proof(ME_LABEL, address).map(ParsedMessage::Me)
+ };
+ let Some(message) = message else {
+ return;
+ };
+ let encoded = match self.paths.encode(address, &message) {
+ Ok(encoded) => encoded,
+ Err(error) => {
+ debug!(to = %address, error = %format!("{error:#}"), "Couldn't encode an identity proof");
+ return;
+ }
+ };
+ match self.paths.send(&encoded).await {
+ Ok(()) => self
+ .traffic
+ .sent(address, encoded.kind, encoded.payload_len),
+ Err(error) => {
+ self.traffic
+ .refused(address, encoded.kind, encoded.payload_len);
+ debug!(to = %address, error = %format!("{error:#}"), "Couldn't send an identity proof");
+ }
+ }
+ }
+
+ /// A message from `address`.
+ async fn receive(
+ self: Arc<Self>,
+ address: String,
+ message: ParsedMessage,
+ payload_len: usize,
+ handler: &HandlerFn,
+ ) -> Result<()> {
+ match message {
+ ParsedMessage::Who(proof) => {
+ self.traffic
+ .received(&address, MessageKind::Who, payload_len);
+ if self.prove(&address, WHO_LABEL, &proof).is_some() {
+ let shared = Arc::clone(&self);
+ tokio::spawn(async move { shared.send_proof(MessageKind::Me, &address).await });
+ }
+ Ok(())
+ }
+ ParsedMessage::Me(proof) => {
+ self.traffic
+ .received(&address, MessageKind::Me, payload_len);
+ self.prove(&address, ME_LABEL, &proof);
+ Ok(())
+ }
+ message => {
+ let node = self.lock().at.get(&address).map(|(node, _)| node.clone());
+ match node {
+ Some(node) => handler(node, message, payload_len).await,
+ None => {
+ self.lock().counts.dropped_unproven += 1;
+ debug!(
+ from = %address,
+ kind = ?message.kind(),
+ "Dropping a message from an address no node proved itself at; asking who is there"
+ );
+ self.ask(&address, false);
+ Ok(())
+ }
+ }
+ }
+ }
+ }
+
+ /// Where to send to `peer`: an address it proved itself at, else one it is said to
+ /// be at; the most preferred transport's first.
+ fn destination(&self, peer: &str) -> Option<String> {
+ self.proven_at(peer)
+ .into_iter()
+ .next()
+ .or_else(|| self.hinted_at(peer).into_iter().next())
+ }
+
+ /// The addresses `peer` proved itself at, best first (`Paths::rank`).
+ fn proven_at(&self, peer: &str) -> Vec<String> {
+ let proven = self.lock().proven.get(peer).cloned().unwrap_or_default();
+ self.paths.rank(&proven)
+ }
+
+ /// Where `peer` is said to be, best first.
+ fn hinted_at(&self, peer: &str) -> Vec<String> {
+ let hints = self.lock().hints.get(peer).cloned().unwrap_or_default();
+ self.paths.rank(&hints)
+ }
+}
+
+#[async_trait]
+impl Transport for IdentityTransport {
+ fn local_id(&self) -> &str {
+ &self.shared.node_id
+ }
+
+ fn is_peer_id(&self, id: &str) -> bool {
+ identity::is_node_id(id)
+ }
+
+ fn delivery_limit(&self) -> Duration {
+ self.shared.paths.delivery_limit()
+ }
+
+ fn local_addresses(&self) -> Vec<String> {
+ self.shared.paths.local_addresses()
+ }
+
+ /// Where `peer` proved itself, else where it is said to be, as others can use it.
+ fn addresses_of(&self, peer: &str) -> Vec<String> {
+ let addresses = {
+ let book = self.shared.lock();
+ book.proven
+ .get(peer)
+ .or_else(|| book.hints.get(peer))
+ .cloned()
+ .unwrap_or_default()
+ };
+ let mut locations = Vec::new();
+ for address in addresses {
+ for location in self.shared.paths.locations(&address) {
+ if !locations.contains(&location) {
+ locations.push(location);
+ }
+ }
+ }
+ locations
+ }
+
+ fn learn_addresses(&self, peer: &str, addresses: &[String]) {
+ if peer == self.shared.node_id || !identity::is_node_id(peer) {
+ return;
+ }
+ let mut known: Vec<String> = Vec::new();
+ for address in addresses {
+ if let Some(address) = self.shared.paths.canonical(address)
+ && !self.shared.paths.is_own(&address)
+ && !known.contains(&address)
+ {
+ known.push(address);
+ }
+ }
+ known.truncate(MAX_HINTS);
+ if known.is_empty() {
+ return;
+ }
+ let mut book = self.shared.lock();
+ if book.hints.len() < MAX_ENTRIES || book.hints.contains_key(peer) {
+ book.hints.insert(peer.to_owned(), known);
+ }
+ }
+
+ fn can_reach(&self, peer: &str) -> bool {
+ self.shared.destination(peer).is_some()
+ }
+
+ fn transports(&self) -> Vec<String> {
+ self.shared.paths.transports()
+ }
+
+ /// The address `encode` picks, the transport's connection there, and every path
+ /// to `peer` as paths measures it, best first.
+ fn destination(&self, peer: &str) -> Option<Destination> {
+ let proven = self.shared.proven_at(peer);
+ let (address, is_proven) = match proven.first() {
+ Some(address) => (address.clone(), true),
+ None => (self.shared.hinted_at(peer).into_iter().next()?, false),
+ };
+ let connection = self.shared.paths.connection(&address);
+ Some(Destination {
+ address,
+ proven: is_proven,
+ connection,
+ paths: self.shared.paths.info(&proven),
+ })
+ }
+
+ /// The paths layer answers them (`Paths::receive`).
+ fn answers_pings(&self) -> bool {
+ true
+ }
+
+ /// The best path's, where messages to `peer` go.
+ fn rtt(&self, peer: &str) -> Option<Duration> {
+ let address = self.shared.proven_at(peer).into_iter().next()?;
+ self.shared.paths.rtt(&address)
+ }
+
+ /// The identity layer's, and each transport's.
+ fn stats(&self) -> TransportStats {
+ let mut stats = self.shared.paths.stats();
+ stats.merge(TransportStats {
+ identity: Some(self.shared.lock().stats()),
+ ..TransportStats::default()
+ });
+ stats
+ }
+
+ fn discovered_seeds(&self) -> Vec<String> {
+ self.shared.paths.discovered_seeds()
+ }
+
+ /// A node ID resolves to itself if this node knows where it is; anything else is
+ /// an address (`<scheme>:...`) or what one of the transports resolves to one (the
+ /// first that does), asked WHO.
+ async fn resolve_seed(&self, seed: &str) -> Result<String> {
+ if identity::is_node_id(seed) {
+ if self.shared.destination(seed).is_some() {
+ return Ok(seed.to_owned());
+ }
+ bail!("a node ID alone doesn't say where that node is; give its address");
+ }
+ let address = self.shared.paths.resolve(seed).await?;
+ if self.shared.paths.is_own(&address) {
+ bail!("{seed} is this node");
+ }
+ let receiver = {
+ let mut book = self.shared.lock();
+ if let Some((node, _)) = book.at.get(&address) {
+ return Ok(node.clone());
+ }
+ let (sender, receiver) = oneshot::channel();
+ let waiting = book.waiting.entry(address.clone()).or_default();
+ waiting.retain(|waiter| !waiter.is_closed());
+ waiting.push(sender);
+ receiver
+ };
+ self.shared.ask(&address, true);
+ match tokio::time::timeout(RESOLVE_TIMEOUT, receiver).await {
+ Ok(Ok(node)) => {
+ info!(seed = %seed, node = %node, "Seed is node");
+ Ok(node)
+ }
+ _ => {
+ self.shared.lock().counts.seeds_unanswered += 1;
+ Err(anyhow!(
+ "no node answered at {address} within {RESOLVE_TIMEOUT:?}"
+ ))
+ }
+ }
+ }
+
+ /// For the address `destination` picks (a send there is refused until `peer`
+ /// proved itself there, `send_encoded`).
+ fn encode(&self, peer: &str, message: &ParsedMessage) -> Result<EncodedMessage> {
+ let Some(address) = self.shared.destination(peer) else {
+ bail!("no address known for {peer} on this node's transports");
+ };
+ let mut encoded = self.shared.paths.encode(&address, message)?;
+ encoded.peer = peer.to_owned();
+ Ok(encoded)
+ }
+
+ /// To the addresses `peer` proved itself at, best first, the next when a send
+ /// fails (`Paths::send_first`); refused if it proved itself nowhere yet.
+ async fn send_encoded(&self, message: &EncodedMessage) -> Result<()> {
+ let proven = self.shared.proven_at(&message.peer);
+ if !proven.is_empty() {
+ self.shared.ask_other_paths(&message.peer);
+ }
+ if proven.is_empty() {
+ let hints = {
+ let mut book = self.shared.lock();
+ book.counts.refused_unproven += 1;
+ book.hints.get(&message.peer).cloned().unwrap_or_default()
+ };
+ for address in hints {
+ self.shared.ask(&address, false);
+ }
+ return Err(NotProven(message.peer.clone()).into());
+ }
+ self.shared
+ .paths
+ .send_first(message, &proven)
+ .await
+ .map(|_| ())
+ }
+
+ /// Every transport's consumer (`Paths::start_consumer`); returns when the first
+ /// one does.
+ async fn start_consumer(&self, handler: MessageHandler) -> Result<()> {
+ let handler: Arc<HandlerFn> = Arc::from(handler);
+ let shared = Arc::clone(&self.shared);
+ let receive: Arc<PathHandler> = Arc::new(move |address, message, payload_len| {
+ let shared = Arc::clone(&shared);
+ let handler = Arc::clone(&handler);
+ Box::pin(async move {
+ shared
+ .receive(address, message, payload_len, &*handler)
+ .await
+ })
+ });
+ self.shared.paths.start_consumer(receive).await
+ }
+
+ /// When the first transport exits.
+ async fn wait_for_exit(&self) -> std::io::Result<std::process::ExitStatus> {
+ self.shared.paths.wait_for_exit().await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn signed_bytes_say_both_addresses() {
+ // A length before each address: "a:bc" to "d" isn't "a:b" to "cd".
+ assert_ne!(
+ signed(WHO_LABEL, "a:bc", "d"),
+ signed(WHO_LABEL, "a:b", "cd")
+ );
+ assert_ne!(signed(WHO_LABEL, "a", "b"), signed(ME_LABEL, "a", "b"));
+ assert_ne!(signed(WHO_LABEL, "a", "b"), signed(WHO_LABEL, "b", "a"));
+ }
+
+ #[test]
+ fn one_address_per_node_and_scheme_and_one_node_per_address() {
+ let now = Instant::now();
+ let mut book = Book::default();
+ assert!(book.bind("s:x", "n1", now));
+ assert!(!book.bind("s:x", "n1", now));
+ // n1 moved to s:y: s:x is no longer n1's.
+ assert!(book.bind("s:y", "n1", now));
+ assert!(!book.at.contains_key("s:x"));
+ assert_eq!(book.proven["n1"], ["s:y"]);
+ // On another transport too.
+ assert!(book.bind("t:x", "n1", now));
+ assert_eq!(book.proven["n1"], ["s:y", "t:x"]);
+ // n2 took over s:y: n1 is proven on t only.
+ assert!(book.bind("s:y", "n2", now));
+ assert_eq!(book.proven["n1"], ["t:x"]);
+ assert_eq!(book.at["s:y"].0, "n2");
+ // Only n1's move from s:x to s:y changed an address.
+ assert_eq!(book.counts.address_changes, 1);
+ let stats = book.stats();
+ assert_eq!(stats.proven["s"], 1);
+ assert_eq!(stats.proven["t"], 1);
+ }
+
+ #[test]
+ fn other_paths_are_the_hints_on_transports_not_proven() {
+ let now = Instant::now();
+ let mut book = Book::default();
+ book.hints
+ .insert("n1".into(), vec!["s:x".into(), "t:x".into(), "u:x".into()]);
+ // Proven nowhere: every hint.
+ assert_eq!(book.unproven_hints("n1"), ["s:x", "t:x", "u:x"]);
+ // Proven on s (even at another address): the others.
+ book.bind("s:y", "n1", now);
+ assert_eq!(book.unproven_hints("n1"), ["t:x", "u:x"]);
+ assert!(book.ask_every("t:x", now, OTHER_PATHS_ASK_INTERVAL));
+ assert!(!book.ask_every("t:x", now + ASK_INTERVAL, OTHER_PATHS_ASK_INTERVAL));
+ assert!(book.ask_every(
+ "t:x",
+ now + OTHER_PATHS_ASK_INTERVAL,
+ OTHER_PATHS_ASK_INTERVAL
+ ));
+ assert!(book.unproven_hints("n2").is_empty());
+ }
+
+ #[test]
+ fn asking_is_rate_limited() {
+ let now = Instant::now();
+ let mut book = Book::default();
+ assert!(book.ask("s:x", now, false));
+ assert!(!book.ask("s:x", now, false));
+ assert!(book.ask("s:x", now, true));
+ assert!(book.ask("s:x", now + ASK_INTERVAL, false));
+ }
+
+ #[tokio::test]
+ async fn a_waiting_seed_learns_the_node() {
+ let mut book = Book::default();
+ let (sender, receiver) = oneshot::channel();
+ book.waiting.insert("s:x".into(), vec![sender]);
+ book.bind("s:x", "n1", Instant::now());
+ assert_eq!(receiver.await.unwrap(), "n1");
+ }
+}