Skip to main content

decdn_node/dht/
dial_addrs.rs

1//! `NodeId → registry-published direct dial addresses` (ADR 001 § Node
2//! Discovery), mirrored into an iroh [`MemoryLookup`] that the runtime registers
3//! on the node's endpoint.
4//!
5//! A node publishes only its relay URL through pkarr, so a dial by bare
6//! `NodeId` learns no IP address from pkarr and opens over the relay. The
7//! `CapacityBond` registry watcher keeps this directory in step with each
8//! node's on-chain `multiaddrs`. With its lookup on the endpoint, every
9//! `EndpointAddr::new(node_id)` dial (DHT RPCs, upstream probes and pulls,
10//! waiver requests) resolves the registry addresses and connects directly when
11//! the node is reachable.
12//!
13//! The addresses are self-attested and additive. A stale or wrong address
14//! loses the path race to the relay, and the QUIC handshake authenticates the
15//! node id, so an address cannot redirect a dial to a different node.
16
17use std::collections::HashMap;
18use std::net::SocketAddr;
19use std::sync::{Arc, RwLock};
20
21use iroh::address_lookup::MemoryLookup;
22use iroh::{EndpointAddr, PublicKey};
23use tracing::debug;
24
25use crate::dht::chain_projection::{with_read, with_write};
26use crate::dht::routing::NodeId;
27
28/// Provenance tag on every address this directory hands iroh, so a connection's
29/// path source reads as the registry in iroh's own diagnostics.
30const PROVENANCE: &str = "capacity_bond_registry";
31
32/// Lock label for [`with_read`] / [`with_write`] poison recovery logs.
33const LABEL: &str = "chain dial-address directory";
34
35/// The registry's `NodeId → direct dial addresses` projection and the iroh
36/// lookup it feeds. Cloning shares both.
37#[derive(Debug, Clone)]
38pub struct DialAddrDirectory {
39    addrs: Arc<RwLock<HashMap<NodeId, Vec<SocketAddr>>>>,
40    lookup: MemoryLookup,
41}
42
43impl Default for DialAddrDirectory {
44    fn default() -> Self {
45        Self {
46            addrs: Arc::default(),
47            lookup: MemoryLookup::with_provenance(PROVENANCE),
48        }
49    }
50}
51
52impl DialAddrDirectory {
53    /// The iroh address lookup this directory feeds. The runtime adds it to the
54    /// node's endpoint.
55    #[must_use]
56    pub fn lookup(&self) -> MemoryLookup {
57        self.lookup.clone()
58    }
59
60    /// The direct addresses held for `node`, or `None` when it has none.
61    #[must_use]
62    pub fn get(&self, node: &NodeId) -> Option<Vec<SocketAddr>> {
63        with_read(&self.addrs, LABEL, |m| m.get(node).cloned())
64    }
65
66    /// Replace `node`'s addresses. An empty list removes the node.
67    pub(crate) fn set(&self, node: NodeId, addrs: Vec<SocketAddr>) {
68        with_write(&self.addrs, LABEL, |m| {
69            if addrs.is_empty() {
70                m.remove(&node);
71                self.lookup_remove(&node);
72            } else {
73                self.lookup_set(&node, &addrs);
74                m.insert(node, addrs);
75            }
76        });
77    }
78
79    /// Remove `node` and its addresses.
80    pub(crate) fn remove(&self, node: &NodeId) {
81        with_write(&self.addrs, LABEL, |m| {
82            m.remove(node);
83            self.lookup_remove(node);
84        });
85    }
86
87    /// Swap in `next` wholesale. A node absent from `next`, or present with an
88    /// empty list, leaves the lookup.
89    pub(crate) fn replace_all(&self, mut next: HashMap<NodeId, Vec<SocketAddr>>) {
90        next.retain(|_, addrs| !addrs.is_empty());
91        with_write(&self.addrs, LABEL, |m| {
92            for gone in m.keys().filter(|id| !next.contains_key(id)) {
93                self.lookup_remove(gone);
94            }
95            for (id, addrs) in &next {
96                self.lookup_set(id, addrs);
97            }
98            *m = next;
99        });
100    }
101
102    fn lookup_set(&self, node: &NodeId, addrs: &[SocketAddr]) {
103        let Some(pk) = public_key(node) else {
104            return;
105        };
106        let target = addrs
107            .iter()
108            .fold(EndpointAddr::new(pk), |t, sock| t.with_ip_addr(*sock));
109        self.lookup.set_endpoint_info(target);
110    }
111
112    fn lookup_remove(&self, node: &NodeId) {
113        if let Some(pk) = public_key(node) {
114            self.lookup.remove_endpoint_info(pk);
115        }
116    }
117}
118
119/// `node` as an iroh key, or `None` (logged) when the registry id is not a
120/// valid ed25519 point. Such a node is undialable, so it needs no lookup entry.
121fn public_key(node: &NodeId) -> Option<PublicKey> {
122    PublicKey::from_bytes(node.as_bytes())
123        .inspect_err(|e| debug!(%node, error = %e, "registry node id is not a valid public key"))
124        .ok()
125}
126
127#[cfg(test)]
128#[allow(clippy::unwrap_used, clippy::expect_used)]
129mod tests;