From fc708a1c1dad15f268786d22cff883c16d251d65 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 29 Oct 2026 00:00:47 +0100 Subject: libp2p: support seeds that are only peer ID --- src/transport/libp2p.rs | 96 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 11 deletions(-) diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs index 9217c79..c3e2d98 100644 --- a/src/transport/libp2p.rs +++ b/src/transport/libp2p.rs @@ -698,6 +698,32 @@ impl Book { .collect() } + /// Where `peer` may be reached through the relays we know (named, then members that + /// relay): for a peer known by its ID only (a seed `/p2p/`), which may hold + /// a slot on one of them. `local`: this node, never its own relay. + fn through_relays(&self, peer: PeerId, local: PeerId) -> Vec { + let mut relays: Vec<(PeerId, Multiaddr)> = self + .named_relays + .iter() + .map(|(relay, named)| (*relay, named.at.clone())) + .collect(); + relays.sort_by_key(|(_, address)| address.to_string()); + let mut members: Vec<(PeerId, Multiaddr)> = self + .relays + .iter() + .filter(|(relay, _)| !self.named_relays.contains_key(relay)) + .map(|(relay, address)| (*relay, address.clone())) + .collect(); + members.sort_by_key(|(_, address)| address.to_string()); + relays + .into_iter() + .chain(members) + .filter(|(relay, _)| *relay != local && *relay != peer) + .map(|(_, address)| address.with(Protocol::P2pCircuit).with(Protocol::P2p(peer))) + .take(ADDRESSES_PER_PEER) + .collect() + } + /// Whether all of `addresses` are known of `peer` (`remember`). fn knows(&self, peer: PeerId, addresses: &[Multiaddr]) -> bool { let Some(known) = self.addresses.get(&peer) else { @@ -849,11 +875,19 @@ impl Libp2pTransport { /// Connected to `peer`, if it can be had in time. async fn connect(&self, peer: PeerId) -> Result<()> { - let addresses = lock(&self.book) - .addresses - .get(&peer) - .map(|known| dialable(known, self.peer_id)) - .unwrap_or_default(); + let addresses = { + let book = lock(&self.book); + let known = book + .addresses + .get(&peer) + .map(|known| dialable(known, self.peer_id)) + .unwrap_or_default(); + if known.is_empty() { + book.through_relays(peer, self.peer_id) + } else { + known + } + }; let (done, connected) = oneshot::channel(); self.commands .send(Command::Connect { @@ -1728,7 +1762,9 @@ impl Transport for Libp2pTransport { return Some(relay.to_string()); } let peer = split_peer(&address)?; - if peer != self.peer_id { + address.pop(); + // A peer ID only says nothing about where. + if peer != self.peer_id && !address.is_empty() { lock(&self.book).remember(peer, address); } Some(peer.to_string()) @@ -1798,14 +1834,26 @@ impl Transport for Libp2pTransport { /// A multiaddr ending in a peer ID is that peer; one without is connected to, to /// find out. A bare peer ID resolves if this transport knows where it is. async fn resolve_seed(&self, seed: &str) -> Result { - if let Ok(peer) = PeerId::from_str(seed) { - if lock(&self.book).addresses.contains_key(&peer) { + let address = match PeerId::from_str(seed) { + Ok(peer) => Multiaddr::empty().with(Protocol::P2p(peer)), + Err(_) => { + Multiaddr::from_str(seed).with_context(|| format!("{seed} is no multiaddr"))? + } + }; + // A peer ID only (`/p2p/`): reached where it's known, or through the + // relays we know. + if let [Protocol::P2p(peer)] = address.iter().collect::>()[..] { + let book = lock(&self.book); + if book.addresses.contains_key(&peer) + || !book.through_relays(peer, self.peer_id).is_empty() + { return Ok(peer.to_string()); } - bail!("no address known for libp2p peer {peer}; give a multiaddr"); + bail!( + "no address known for libp2p peer {peer}, and no relay to reach it through; \ + give a multiaddr, or a relay (libp2p-relays)" + ); } - let address = - Multiaddr::from_str(seed).with_context(|| format!("{seed} is no multiaddr"))?; if let Some(peer) = self.locate(seed) { return Ok(peer); } @@ -2171,6 +2219,32 @@ mod tests { assert!(a.connection(b.local_id()).is_some()); } + /// A seed that's a peer ID only is reached through the relays the node knows. + #[tokio::test] + async fn a_peer_id_seed_is_reached_through_a_relay() { + let (_r, r_address) = memory_node_with(true, Vec::new(), true); + let (b, _) = memory_node_with(false, vec![r_address.clone()], false); + let b = Arc::new(b); + let mut at_b = received(Arc::clone(&b)); + assert!( + eventually(|| !b.local_addresses().is_empty()).await, + "b got no slot on the relay" + ); + let (a, _) = memory_node_with(true, vec![r_address.clone()], false); + let seed = format!("/p2p/{}", b.local_id()); + assert_eq!(a.resolve_seed(&seed).await.unwrap(), b.local_id()); + assert_eq!(a.resolve_seed(b.local_id()).await.unwrap(), b.local_id()); + // Nothing to dial is remembered. + assert!(!lock(&a.book).addresses.contains_key(&b.peer_id)); + a.send_message(b.local_id(), &status()).await.unwrap(); + let (from, _) = at_b.recv().await.unwrap(); + assert_eq!(from, a.local_id()); + + // Without a relay, there's nowhere to look. + let (c, _) = memory_node(); + assert!(c.resolve_seed(&seed).await.is_err()); + } + /// b listens nowhere; a, which can be dialled, reaches it through the relay r: b /// then dials a directly (identify told it where), and both keep that. #[tokio::test] -- cgit v1.3.1