Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src-gui/src/utils/parseUtils.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
3 changes: 2 additions & 1 deletion src-gui/src/utils/parseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion swap-p2p/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 7 additions & 1 deletion swap-p2p/src/out_event/bob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -103,6 +103,12 @@ impl From<identify::Event> for OutEvent {
}
}

impl From<relay::client::Event> for OutEvent {
fn from(_: relay::client::Event) -> Self {
OutEvent::Other
}
}

impl From<rendezvous::discovery::Event> for OutEvent {
fn from(event: rendezvous::discovery::Event) -> Self {
OutEvent::Discovery(event)
Expand Down
2 changes: 1 addition & 1 deletion swap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 14 additions & 15 deletions swap/src/cli/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -835,21 +835,20 @@ mod builder {
let rendezvous_peer_ids: Vec<PeerId> =
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 {
Expand Down
7 changes: 6 additions & 1 deletion swap/src/cli/behaviour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -57,6 +60,7 @@ impl Behaviour {
env_config: env::Config,
bitcoin_wallet: Arc<dyn BitcoinWallet>,
identity: identity::Keypair,
relay: relay::client::Behaviour,
namespace: XmrBtcNamespace,
rendezvous_nodes: Vec<PeerId>,
wormhole_store: Arc<dyn wormhole::WormholeStore + Send + Sync>,
Expand All @@ -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),

Expand Down
10 changes: 6 additions & 4 deletions swap/src/network/swarm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -132,20 +132,22 @@ where
Ok((swarm, onion_addresses, onion_service_handle))
}

pub async fn cli<T>(
pub async fn cli<T, B>(
identity: identity::Keypair,
maybe_tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
behaviour: T,
build_behaviour: B,
) -> Result<(Swarm<T>, Option<TorDialPriorityTracker>)>
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))?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relay hop exhausts yamux stream limit

Medium Severity

Outbound circuit dials open a long-lived hop stream on the existing connection to the relay, which still uses the CLI transport’s yamux cap of MAX_NUM_STREAMS (5). That cap was sized for a couple of short-lived protocol streams, not one hop stream per relayed peer. Connecting to more than a few makers through the same relay, which is the usual public-relay layout, will fail stream opens and drop those dials.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f498d46. Configure here.

.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT))
.build();

Expand Down
22 changes: 12 additions & 10 deletions swap/tests/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading