Skip to content

Restore per-request and handshake timeouts for NYM mixnet fetches - #731

Open
j0ntz wants to merge 1 commit into
masterfrom
jon/nym-mixfetch-v2-chain-wedge
Open

Restore per-request and handshake timeouts for NYM mixnet fetches#731
j0ntz wants to merge 1 commit into
masterfrom
jon/nym-mixfetch-v2-chain-wedge

Conversation

@j0ntz

@j0ntz j0ntz commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Asana: https://app.asana.com/1/9976422036640/project/1215088146871429/task/1216608079766867

Restores an explicit per-request timeout on NYM mixnet fetches, re-expressed on the v1 (@nymproject/mix-fetch@1.4.4) stack that master reverted to in 2.47.1.

Background

This PR was originally authored against the v2 mix-fetch upgrade (base 2.47.0). That upgrade was reverted to v1 in #732, so the branch has been reset onto the current v1 master and the change re-expressed for the v1 API. The v1 revert already re-applied the 60s handshake bound (SETUP_TIMEOUT_MS, commit a323e15f), so the remaining piece is the per-request side.

What this changes

mixFetchOptions.mixFetchOverride.requestTimeoutMs (300000) asks mix-fetch to bound each request, but a request whose exit connection stalls has been observed hanging past it. David Coen's 2026-07-20 staging report (26071707, Android) showed Coreum/Avalanche sends sitting on "Calculating Fee" indefinitely with NYM on, after the v1 revert landed. This wraps each mixFetch call in our own REQUEST_TIMEOUT_MS race (fetchWithTimeout), so the promise rejects on our side once the budget elapses and an engine can fail over to its next server instead of awaiting the request forever. The wrapper keeps v1's 3-arg IMixFetchFn signature, so call sites are unchanged, and swallows the abandoned request's late rejection so it does not surface as an unhandled rejection in the worker.

The value stays at 300000 to match the native requestTimeoutMs it backstops. Reducing that budget (5 minutes still reads as a stuck spinner) is separate scope handled in #734 (task 1216729024836433), which also adds concurrency bounds and per-request logging; the two touch the same file and whichever lands second reconciles.

Testing

  • tsc --noEmit clean; mocha suite green (162 passing).
  • Built the change into the RN worker bundle, updot-linked into edge-react-gui, and force-rebuilt the iOS sim app (verified the installed edge-core-js.bundle/edge-core.js carries the wrapper).
  • In-app on the iOS sim (edge-funds): with NYM enabled (Avalanche + Base), the core log shows mixFetch initialized successfully through the wrapped initMixFetch on every launch; drove a live Ethereum NYM toggle (Network privacy for ethereum: nym) and reverted it. Wallets sync normally through the wrapper with no spurious timeout and no regression.
  • Not reproducible here: an actual per-request timeout firing needs a stalled/dead exit connection on demand. The wrapper is transparent on the success path; the rejection path is covered by logic + the suite's fuzzyTimeout tests. Screenshots attached.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2e8fd21. Configure here.

Comment thread src/util/nym.ts
@j0ntz
j0ntz force-pushed the jon/nym-mixfetch-v2-chain-wedge branch from 2e8fd21 to 6ab87fe Compare July 15, 2026 20:33
@j0ntz

j0ntz commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Important context: this is a mitigation, not the fix for the QA report

A v1 baseline A/B (requested on the task after this PR opened) changes how this PR should be read. Please don't merge it thinking it resolves the staging report.

I built the v1 baseline (dab7afd1 / v2.46.1, the parent of the v2 upgrade 816be236) with @nymproject/mix-fetch@1.4.4 and its exact pinned gateway + networkRequester config, and ran the identical endpoint matrix against the live mixnet concurrently with a healthy pinned-IPR v2 tunnel, same browser, same instant — so mixnet weather can't explain the gap.

Tunnel setup

Config Result
v1, pinned gateway + networkRequester 8.3s OK
v2, pinned IPR 8.0s OK
v2, auto-discovery (as shipped in #729) FAILED 2/2 (66s, 67s); 1/5 across the run

Per-request, both tunnels healthy, same moment

Endpoint v1 v2
ETH cloudflare-eth 2576 / 2618ms (HTTP 429, reachable) 120029ms timed out
AVAX api.avax.network 2090 / 2656 / 2080ms, HTTP 200 120012ms timed out
Cosmos Hub :443 2447ms, HTTP 200, real node_info 120011ms timed out
Coreum :26657 fails in 1034ms 120008ms timed out

v1 was completing AVAX in ~2s repeatedly while v2 timed out on that same endpoint at the same instant.

What that means

  • Avalanche and Cosmos Hub were NOT broken on v1. Both work in ~2s. v2 broke them.
  • Coreum was already broken on v1 (fails in ~1s, consistent with the :26657 exit policy). v2's only change there is the unbounded hang.
  • ETH did not get faster. It regressed.

So v2 is a comprehensive regression, not merely the timeout mis-port this PR fixes.

Why this PR is still worth landing

An unbounded per-request wait is a bug on its own terms, and this restores v1's proven 300000ms bound plus a sane handshake budget. It converts a permanent "Calculating Fee" spinner into a surfaced error. It just cannot make v2 deliver.

The likely real fix is reverting #729 to 1.4.4 (neither version is npm-deprecated). Note that pinning preferredIpr would NOT rescue v2: even a healthy pinned v2 tunnel timed out on all four endpoints, so this is the smolmix stack rather than IPR selection. Tracked on the task; the revert decision is the maintainers' call.

@j0ntz j0ntz mentioned this pull request Jul 16, 2026
2 tasks
@j0ntz

j0ntz commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #732 (revert to mix-fetch 1.4.4). The multi-node A/B confirmed v2 is a comprehensive regression, not just the timeout mis-port this PR bounds. This PR's setup-timeout hardening is carried forward onto the v1 API in #732. Recommend closing this in favor of #732 once the revert is approved.

mix-fetch's own requestTimeoutMs is meant to bound each request, but a
request whose exit connection stalls has been observed hanging past it
(David Coen's 2026-07-20 staging report: Coreum/Avalanche sends sat on
'Calculating Fee' indefinitely with NYM on, after the v1 revert). Wrap
each mixFetch call in our own REQUEST_TIMEOUT_MS race so the promise
rejects on our side and an engine fails over to its next server instead
of awaiting forever. The 60s handshake bound is already in place.

This re-expresses PR #731 on the v1 (1.4.4) mix-fetch stack that master
reverted to; #731 was authored against v2.
@j0ntz
j0ntz force-pushed the jon/nym-mixfetch-v2-chain-wedge branch from 6ab87fe to 4e8f2d4 Compare July 23, 2026 01:02
@j0ntz

j0ntz commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence — NYM per-request timeout wrapper (iOS sim)

agent proof 1216608079766867 01 nym avax base enabled

agent proof 1216608079766867 01 nym avax base enabled

agent proof 1216608079766867 02 nym ethereum toggled

agent proof 1216608079766867 02 nym ethereum toggled

Captured by the agent's in-app test run (build-and-test).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant