From b9a6ec2abce9fd023623550f0b8349728fd44449 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:58:10 -0400 Subject: [PATCH] smolmix + sdk: throughput and latency tuning for the Goblin read tunnel 256KB TCP buffers (was 8KB) + burst 64 (was 1) in smolmix; an IpMixStream::from_client constructor + best_ipr helper and reply-SURBs 10->4 in the ipr wrapper so a caller can back the tunnel with a high-traffic-profile MixnetClient. No behavior change to the default connect_new path used by the scoped exit. --- .../nym-sdk/src/ipr_wrapper/ip_mix_stream.rs | 40 ++++++++++++++++--- sdk/rust/nym-sdk/src/lib.rs | 5 +++ smolmix/core/src/device.rs | 6 ++- smolmix/core/src/tunnel.rs | 10 ++++- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs b/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs index 13cfb56e23..931834580a 100644 --- a/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs +++ b/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs @@ -53,22 +53,52 @@ impl IpMixStream { /// /// Returns a ready-to-use tunnel with allocated IP addresses. pub async fn new() -> Result { + let ipr_address = Self::best_ipr().await?; + Self::new_with_ipr(ipr_address).await + } + + /// Auto-discover the best available IPR exit for the mainnet. + /// + /// Exposed so a caller that wants to back the tunnel with its own tuned + /// [`MixnetClient`] (via [`IpMixStream::from_client`]) can still get the + /// auto-selected exit that [`IpMixStream::new`] would have used. + pub async fn best_ipr() -> Result { let network_defaults = NymNetworkDetails::new_mainnet(); let api_client = create_nym_api_client(network_defaults.nym_api_urls.ok_or(Error::NoNymAPIUrl)?)?; - let ipr_address = get_best_ipr(api_client).await?; - Self::new_with_ipr(ipr_address).await + get_best_ipr(api_client).await } /// Connect to a specific IPR address. /// /// Use this when you already know the IPR `Recipient` address (e.g. for /// testing against a specific exit node). For automatic discovery, use - /// [`IpMixStream::new`] instead. + /// [`IpMixStream::new`] instead. Backs the tunnel with a default + /// [`MixnetClient::connect_new`] client (full cover traffic, poisson per-hop + /// delays); for a tuned client use [`IpMixStream::from_client`]. pub async fn new_with_ipr(ipr_address: Recipient) -> Result { nym_network_defaults::setup_env(None::<&str>); - let mut client = MixnetClient::connect_new().await?; - let mut stream = client.open_stream(ipr_address, Some(10)).await?; + let client = MixnetClient::connect_new().await?; + Self::from_client(client, ipr_address).await + } + + /// Establish the IP tunnel over a caller-provided, already-connected + /// [`MixnetClient`]. + /// + /// This is the low-anonymity-tuning seam: the caller builds the client with + /// whatever traffic/anonymity [`DebugConfig`](crate::mixnet::config) it wants + /// (e.g. a higher-throughput / lower-cover-traffic preset for a public read + /// tunnel) and passes it in. Everything after the client — the stream open, + /// the IPR connect handshake and the allocated-IP bookkeeping — is identical + /// to [`new_with_ipr`](Self::new_with_ipr). + pub async fn from_client( + mut client: MixnetClient, + ipr_address: Recipient, + ) -> Result { + // 4 reply-SURBs per stream (was 10): the IPR replies steadily, so a + // smaller SURB budget still keeps the return path fed while cutting the + // per-write SURB overhead on this public read tunnel. + let mut stream = client.open_stream(ipr_address, Some(4)).await?; info!("Connecting to IP packet router at {ipr_address}"); let allocated_ips = Self::connect_tunnel(&mut stream).await?; diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index 4006d8e2dd..cdbb70c0fe 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -141,6 +141,11 @@ pub use nym_network_defaults::NymContracts; /// println!("API: {:?}", network.endpoints); /// ``` pub use nym_network_defaults::NymNetworkDetails; +/// Export the network (mainnet by default) environment configuration. Callers +/// that build a [`MixnetClient`](mixnet::MixnetClient) themselves (rather than +/// via [`MixnetClient::connect_new`](mixnet::MixnetClient::connect_new)) should +/// call this first, mirroring what the higher-level constructors do internally. +pub use nym_network_defaults::setup_env; /// Validator/API endpoint configuration. pub use nym_network_defaults::ValidatorDetails; diff --git a/smolmix/core/src/device.rs b/smolmix/core/src/device.rs index 2aa78e70f2..c1502935a1 100644 --- a/smolmix/core/src/device.rs +++ b/smolmix/core/src/device.rs @@ -33,7 +33,11 @@ impl NymAsyncDevice { let mut capabilities = DeviceCapabilities::default(); capabilities.medium = Medium::Ip; capabilities.max_transmission_unit = 1500; - capabilities.max_burst_size = Some(1); + // Let the reactor process up to 64 packets per poll loop instead of one. + // max_burst_size = Some(1) serialized all I/O to a single packet per + // reactor wake, which throttles throughput badly over the mixnet's long + // RTT; 64 lets a burst drain in one pass. + capabilities.max_burst_size = Some(64); Self { rx, diff --git a/smolmix/core/src/tunnel.rs b/smolmix/core/src/tunnel.rs index 172800e435..5836bf599e 100644 --- a/smolmix/core/src/tunnel.rs +++ b/smolmix/core/src/tunnel.rs @@ -217,11 +217,19 @@ impl Tunnel { // Configure smoltcp: raw IP mode (no Ethernet), /32 for the allocated IP, // default route via unspecified (the IPR does the actual routing). let iface_config = Config::new(HardwareAddress::Ip); - let net_config = NetConfig::new( + let mut net_config = NetConfig::new( iface_config, IpCidr::new(IpAddress::from(allocated_ips.ipv4), 32), vec![IpAddress::from(Ipv4Address::UNSPECIFIED)], ); + // Lift the per-socket TCP window from tokio-smoltcp's 8 KB default to + // 256 KB. The mixnet RTT is large (5 hops + deliberate per-hop delay), + // so an 8 KB window caps bulk throughput at ~8 KB per round trip; 256 KB + // lets many segments stay in flight, which is what makes relay/NIP-11/ + // price reads fast. Only the two TCP buffers change — every other + // `BufferSize` field keeps its default. + net_config.buffer_size.tcp_rx_size = 262144; + net_config.buffer_size.tcp_tx_size = 262144; // Net::new spawns the smoltcp reactor as a background task. After this, // tcp_connect/udp_bind create sockets managed by that reactor.