From 4ba4405c0e137f83015d3625a640122089b14ed1 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:29:49 +0200 Subject: [PATCH] fix(relay): decouple global circuit ceiling from the per-peer limit `relay::Config.max_circuits` is a relay-wide ceiling in rust-libp2p, but it was set to `max_res_per_peer` (default 512), capping the entire relay at 512 concurrent circuits regardless of `--p2p-max-connections` (default 16384). Past that, every further circuit request is denied with RESOURCE_LIMIT_EXCEEDED (libp2p-relay-0.21.1 behaviour.rs:539-541 denies on `circuits.len() >= max_circuits`). go-libp2p has no global circuit cap at all: `Resources.MaxCircuits` is documented as the per-peer limit and is enforced only against the source and destination peer counts (go-libp2p@v0.41.1 p2p/protocol/circuitv2/relay/relay.go:293,301). Charon's `relayResources.MaxCircuits = config.MaxResPerPeer` (charon@v1.7.1 cmd/relay/p2p.go:67) is therefore a *per-peer* 512, with no relay-wide ceiling behind it. Keep `max_circuits_per_peer = max_res_per_peer` (mirrors Charon) and size the global ceiling from `max_conns`, the same connection budget that already feeds `max_reservations`. That preserves a safety valve rust-libp2p offers and go-libp2p lacks, without the artificial throttle. Fixes #484 Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- crates/relay-server/src/config.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/relay-server/src/config.rs b/crates/relay-server/src/config.rs index 499b382c..70fd3e33 100644 --- a/crates/relay-server/src/config.rs +++ b/crates/relay-server/src/config.rs @@ -60,7 +60,16 @@ pub(crate) fn create_relay_config(config: &Config) -> relay::Config { max_reservations: config.max_conns, max_reservations_per_peer: config.max_res_per_peer, reservation_duration: Duration::from_secs(ONE_HOUR_SECONDS), - max_circuits: config.max_res_per_peer, + // rust-libp2p splits circuit limits into a global ceiling + // (`max_circuits`) and a per-peer one (`max_circuits_per_peer`); + // go-libp2p's `Resources.MaxCircuits` is only the per-peer limit and + // has no global counterpart (go-libp2p@v0.41.1 + // p2p/protocol/circuitv2/relay/relay.go:293,301 checks the source and + // destination peer counts only). So only the per-peer value mirrors + // Charon's `MaxCircuits = MaxResPerPeer`; the global ceiling is sized + // from the connection budget instead, keeping a safety valve + // go-libp2p lacks without throttling total relay throughput. + max_circuits: config.max_conns, max_circuits_per_peer: config.max_res_per_peer, max_circuit_duration: Duration::from_secs(ONE_HOUR_SECONDS), max_circuit_bytes: MB_32, @@ -110,7 +119,8 @@ mod tests { let relay_config = create_relay_config(&test_config(64, 8)); assert_eq!(relay_config.max_reservations, 64); assert_eq!(relay_config.max_reservations_per_peer, 8); - assert_eq!(relay_config.max_circuits, 8); + // Global circuit ceiling tracks `max_conns`, not `max_res_per_peer`. + assert_eq!(relay_config.max_circuits, 64); assert_eq!(relay_config.max_circuits_per_peer, 8); assert_eq!( relay_config.reservation_duration, @@ -123,6 +133,15 @@ mod tests { assert_eq!(relay_config.max_circuit_bytes, MB_32); } + #[test] + fn global_circuit_ceiling_is_decoupled_from_per_peer_limit() { + // CLI defaults (`crates/cli/src/commands/relay.rs`), matching Charon's + // `p2p-max-reservations` / `p2p-max-connections`. + let relay_config = create_relay_config(&test_config(16384, 512)); + assert_eq!(relay_config.max_circuits, 16384); + assert_eq!(relay_config.max_circuits_per_peer, 512); + } + #[test] fn zero_max_res_per_peer_uses_default_per_ip_capacity() { // max_res_per_peer = 0 must not panic (NonZeroU32 fallback path) and