From 04d5b484979fd7c46683183d2eb966eb89fb02b0 Mon Sep 17 00:00:00 2001 From: binarybaron Date: Fri, 28 Aug 2026 14:28:44 +0200 Subject: [PATCH] Enable circuit relay connections for GUI --- CHANGELOG.md | 1 + Cargo.lock | 27 ++++++++++++++++++++++++++ src-gui/src/utils/parseUtils.test.ts | 18 +++++++++++++++++ src-gui/src/utils/parseUtils.ts | 3 ++- swap-p2p/Cargo.toml | 2 +- swap-p2p/src/out_event/bob.rs | 8 +++++++- swap/Cargo.toml | 2 +- swap/src/cli/api.rs | 29 ++++++++++++++-------------- swap/src/cli/behaviour.rs | 7 ++++++- swap/src/network/swarm.rs | 10 ++++++---- swap/tests/harness/mod.rs | 22 +++++++++++---------- 11 files changed, 95 insertions(+), 34 deletions(-) create mode 100644 src-gui/src/utils/parseUtils.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ffdb579a62..facfc323c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ 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. - ASB: The `get-swaps` RPC response now includes the Bitcoin redeem address for each swap. ## [4.13.3] - 2026-08-05 diff --git a/Cargo.lock b/Cargo.lock index f70851e6d3..6e2b962370 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 0000000000..ed9733d8b1 --- /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 401885a260..15c640fe47 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 0edd7a94ff..a9ef9d7800 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 9cd2b87f9f..5c9b66c741 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 0e6bf87031..70335c9890 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 c0e9fe3b71..a27fd409a5 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 3eb23fe4ca..662a5c6ba5 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 76ba4e8d54..76475fd894 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 a078a497a7..0e2dca4f43 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)