diff --git a/CHANGELOG.md b/CHANGELOG.md index c3f357829..c79b04eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- GUI: Support outbound connections to makers through libp2p circuit relays. + ## [4.14.0] - 2026-08-22 - ASB: The `get-swaps` RPC response now includes the Bitcoin redeem address for each swap. diff --git a/Cargo.lock b/Cargo.lock index a2675c8c4..5c46a86d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5413,6 +5413,7 @@ dependencies = [ "libp2p-noise", "libp2p-ping", "libp2p-quic", + "libp2p-relay", "libp2p-rendezvous", "libp2p-request-response", "libp2p-swarm", @@ -5635,6 +5636,7 @@ dependencies = [ "libp2p-identify", "libp2p-identity", "libp2p-ping", + "libp2p-relay", "libp2p-swarm", "pin-project", "prometheus-client", @@ -5708,6 +5710,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "libp2p-relay" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d1c667cfabf3dd675c8e3cea63b7b98434ecf51721b7894cbb01d29983a6a9b" +dependencies = [ + "asynchronous-codec 0.7.0", + "bytes", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec 0.3.1", + "rand 0.8.5", + "static_assertions", + "thiserror 1.0.69", + "tracing", + "void", + "web-time", +] + [[package]] name = "libp2p-rendezvous" version = "0.14.0" diff --git a/src-gui/src/utils/parseUtils.test.ts b/src-gui/src/utils/parseUtils.test.ts new file mode 100644 index 000000000..ed9733d8b --- /dev/null +++ b/src-gui/src/utils/parseUtils.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { isValidMultiAddressWithPeerId } from "./parseUtils"; + +describe("isValidMultiAddressWithPeerId", () => { + it("accepts a circuit relay address with relay and destination peer IDs", () => { + const address = + "/dns4/relay.example/tcp/443/wss/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw/p2p-circuit/p2p/12D3KooWMc39w7bZz4RLmJKuUiK9YkbKoEHACZWcL71XNns5dPuD"; + + expect(isValidMultiAddressWithPeerId(address)).toBe(true); + }); + + it("rejects a circuit relay address without a destination peer ID", () => { + const address = + "/dns4/relay.example/tcp/443/wss/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw/p2p-circuit"; + + expect(isValidMultiAddressWithPeerId(address)).toBe(false); + }); +}); diff --git a/src-gui/src/utils/parseUtils.ts b/src-gui/src/utils/parseUtils.ts index 401885a26..15c640fe4 100644 --- a/src-gui/src/utils/parseUtils.ts +++ b/src-gui/src/utils/parseUtils.ts @@ -51,8 +51,9 @@ export function isValidMultiAddressWithPeerId( try { const multiAddress = new Multiaddr(multiAddressStr); const peerId = multiAddress.getPeerId(); + const protocols = multiAddress.protoNames(); - return peerId !== null; + return peerId !== null && protocols[protocols.length - 1] === "p2p"; } catch { return false; } diff --git a/swap-p2p/Cargo.toml b/swap-p2p/Cargo.toml index 0edd7a94f..a9ef9d780 100644 --- a/swap-p2p/Cargo.toml +++ b/swap-p2p/Cargo.toml @@ -14,7 +14,7 @@ swap-serde = { path = "../swap-serde" } # Networking async-trait = { workspace = true, optional = true } -libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "ping", "identify"] } +libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "ping", "identify", "relay"] } # Metrics prometheus-client = "0.22" diff --git a/swap-p2p/src/out_event/bob.rs b/swap-p2p/src/out_event/bob.rs index 9cd2b87f9..5c9b66c74 100644 --- a/swap-p2p/src/out_event/bob.rs +++ b/swap-p2p/src/out_event/bob.rs @@ -4,7 +4,7 @@ use libp2p::{ InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel, }, }; -use libp2p::{identify, ping}; +use libp2p::{identify, ping, relay}; use crate::observe; use crate::protocols::{ @@ -103,6 +103,12 @@ impl From for OutEvent { } } +impl From for OutEvent { + fn from(_: relay::client::Event) -> Self { + OutEvent::Other + } +} + impl From for OutEvent { fn from(event: rendezvous::discovery::Event) -> Self { OutEvent::Discovery(event) diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 25f0f8e2a..23c979768 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -29,7 +29,7 @@ tor-llcrypto = { workspace = true } tor-rtcompat = { workspace = true, features = ["tokio"] } # LibP2P -libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa", "websocket", "metrics"] } +libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa", "websocket", "metrics", "relay"] } libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } # Error handling diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index c0e9fe3b7..a27fd409a 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -835,21 +835,20 @@ mod builder { let rendezvous_peer_ids: Vec = rendezvous_points.iter().map(|(p, _)| *p).collect(); - let behaviour = crate::cli::Behaviour::new( - env_config, - wallet.clone(), - seed.derive_libp2p_identity(), - namespace, - rendezvous_peer_ids.clone(), - db.clone(), - ); - - let (mut swarm, tor_priority_tracker) = crate::network::swarm::cli( - seed.derive_libp2p_identity(), - tor_client_for_swarm, - behaviour, - ) - .await?; + let identity = seed.derive_libp2p_identity(); + let (mut swarm, tor_priority_tracker) = + crate::network::swarm::cli(identity.clone(), tor_client_for_swarm, |relay| { + crate::cli::Behaviour::new( + env_config, + wallet.clone(), + identity, + relay, + namespace, + rendezvous_peer_ids.clone(), + db.clone(), + ) + }) + .await?; if let Some(tor_priority_tracker) = &tor_priority_tracker { for peer_id in &rendezvous_peer_ids { diff --git a/swap/src/cli/behaviour.rs b/swap/src/cli/behaviour.rs index 3eb23fe4c..662a5c6ba 100644 --- a/swap/src/cli/behaviour.rs +++ b/swap/src/cli/behaviour.rs @@ -8,7 +8,7 @@ use crate::network::{ use anyhow::Result; use bitcoin_wallet::BitcoinWallet; use libp2p::swarm::NetworkBehaviour; -use libp2p::{PeerId, identify, identity, ping}; +use libp2p::{PeerId, identify, identity, ping, relay}; use std::sync::Arc; use std::time::Duration; use swap_env::env; @@ -25,6 +25,9 @@ const MAX_REDIAL_INTERVAL: Duration = Duration::from_secs(30); #[behaviour(to_swarm = "OutEvent")] #[allow(missing_debug_implementations)] pub struct Behaviour { + /// Enables outbound connections through circuit relays. + relay: relay::client::Behaviour, + /// Fetch a quote from a specific peer, usually before starting a swap pub direct_quote: quote::Behaviour, /// Periodically request quotes from any peers that might offer them @@ -57,6 +60,7 @@ impl Behaviour { env_config: env::Config, bitcoin_wallet: Arc, identity: identity::Keypair, + relay: relay::client::Behaviour, namespace: XmrBtcNamespace, rendezvous_nodes: Vec, wormhole_store: Arc, @@ -67,6 +71,7 @@ impl Behaviour { let pingConfig = ping::Config::new().with_timeout(Duration::from_secs(60)); Self { + relay, direct_quote: quote::bob(), quotes: quotes_cached::Behaviour::new(identifyConfig), diff --git a/swap/src/network/swarm.rs b/swap/src/network/swarm.rs index 76ba4e8d5..76475fd89 100644 --- a/swap/src/network/swarm.rs +++ b/swap/src/network/swarm.rs @@ -9,7 +9,7 @@ use libp2p::connection_limits::ConnectionLimits; use libp2p::core::muxing::StreamMuxerBox; use libp2p::metrics::{BandwidthTransport, Registry}; use libp2p::swarm::NetworkBehaviour; -use libp2p::{Multiaddr, Swarm, identity}; +use libp2p::{Multiaddr, Swarm, identity, noise, relay, yamux}; use libp2p::{PeerId, SwarmBuilder}; use libp2p_tor::TorDialPriorityTracker; use std::fmt::Debug; @@ -132,20 +132,22 @@ where Ok((swarm, onion_addresses, onion_service_handle)) } -pub async fn cli( +pub async fn cli( identity: identity::Keypair, maybe_tor_client: Option>>, - behaviour: T, + build_behaviour: B, ) -> Result<(Swarm, Option)> where T: NetworkBehaviour, + B: FnOnce(relay::client::Behaviour) -> T, { let (transport, tor_priority_tracker) = cli::transport::new(&identity, maybe_tor_client)?; let swarm = SwarmBuilder::with_existing_identity(identity) .with_tokio() .with_other_transport(|_| transport)? - .with_behaviour(|_| behaviour)? + .with_relay_client(noise::Config::new, yamux::Config::default)? + .with_behaviour(|_, relay| build_behaviour(relay))? .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT)) .build(); diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index a078a497a..0e2dca4f4 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -771,16 +771,18 @@ impl BobParams { ) -> Result<(cli::EventLoop, cli::EventLoopHandle)> { let identity = self.seed.derive_libp2p_identity(); - let behaviour = cli::Behaviour::new( - self.env_config, - self.bitcoin_wallet.clone(), - identity.clone(), - XmrBtcNamespace::Testnet, - Vec::new(), - db.clone(), - ); - let (mut swarm, tor_priority_tracker) = - swarm::cli(identity.clone(), None, behaviour).await?; + let (mut swarm, tor_priority_tracker) = swarm::cli(identity.clone(), None, |relay| { + cli::Behaviour::new( + self.env_config, + self.bitcoin_wallet.clone(), + identity, + relay, + XmrBtcNamespace::Testnet, + Vec::new(), + db.clone(), + ) + }) + .await?; swarm.add_peer_address(self.alice_peer_id, self.alice_address.clone()); cli::EventLoop::new(swarm, db.clone(), None, tor_priority_tracker)