From 87dffc63842cb21eb03c7501e6dfee88f7ba15da Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Mon, 17 Aug 2026 11:31:52 -0700 Subject: [PATCH] Rerandomize the NYM mixnet client id on each connect clientId names the mixnet client's persistent key storage, so the fixed id made every setup adopt the previous registration. A registration that went bad therefore stayed bad for every later connect and across app launches, which surfaces as a wallet that never syncs and never recovers. Also drop the third argument at both fetch call sites: initMixFetch has already created the instance, and the instance-bound mixFetch takes only (url, args), so the options object was silently discarded. --- CHANGELOG.md | 2 ++ src/io/browser/browser-io.ts | 14 +++----- src/io/react-native/react-native-worker.ts | 14 +++----- src/util/nym.ts | 38 +++++++++++++++++++--- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b15bfa580..7779ad1d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- fixed: Rerandomize the NYM mixnet client id on each connect, so a client registration that goes bad is abandoned instead of being reloaded from storage by every later connect and every later app launch. + ## 2.47.1 (2026-07-17) - fixed: Revert `@nymproject/mix-fetch` to v1 (1.4.4), restoring the pinned gateway and network requester. The v2 stack shipped in 2.47.0 fails to complete small HTTPS JSON-RPC requests through most exit nodes and its exit-node auto-discovery rarely converges, which left wallets with NYM privacy enabled unable to sync or send. diff --git a/src/io/browser/browser-io.ts b/src/io/browser/browser-io.ts index 929582467..4c4aee818 100644 --- a/src/io/browser/browser-io.ts +++ b/src/io/browser/browser-io.ts @@ -3,7 +3,7 @@ import { makeLocalStorageDisklet } from 'disklet' import { LogBackend, makeLog } from '../../core/log/log' import { EdgeFetchOptions, EdgeFetchResponse, EdgeIo } from '../../types/types' import { scrypt } from '../../util/crypto/scrypt' -import { initMixFetch, mixFetchOptions } from '../../util/nym' +import { initMixFetch } from '../../util/nym' import { fetchCorsProxy } from './fetch-cors-proxy' // Only try CORS proxy/bridge techniques up to 5 times @@ -50,14 +50,10 @@ export function makeBrowserIo(logBackend: LogBackend): EdgeIo { if (privacy === 'nym') { const nymFetch = await initMixFetch(log) - return await nymFetch( - uri, - { - ...opts, - mode: 'unsafe-ignore-cors' as RequestMode - }, - mixFetchOptions - ) + return await nymFetch(uri, { + ...opts, + mode: 'unsafe-ignore-cors' as RequestMode + }) } if (corsBypass === 'always') { return await fetchCorsProxy(uri, opts) diff --git a/src/io/react-native/react-native-worker.ts b/src/io/react-native/react-native-worker.ts index 4eb777c89..2711ba261 100644 --- a/src/io/react-native/react-native-worker.ts +++ b/src/io/react-native/react-native-worker.ts @@ -17,7 +17,7 @@ import { EdgeFetchResponse, EdgeIo } from '../../types/types' -import { initMixFetch, mixFetchOptions } from '../../util/nym' +import { initMixFetch } from '../../util/nym' import { hideProperties } from '../hidden-properties' import { makeNativeBridge } from './native-bridge' import { WorkerApi, YAOB_THROTTLE_MS } from './react-native-types' @@ -177,14 +177,10 @@ async function makeIo(logBackend: LogBackend): Promise { if (privacy === 'nym') { const nymFetch = await initMixFetch(log) - const response = await nymFetch( - uri, - { - ...opts, - mode: 'unsafe-ignore-cors' as RequestMode - }, - mixFetchOptions - ) + const response = await nymFetch(uri, { + ...opts, + mode: 'unsafe-ignore-cors' as RequestMode + }) return response } if (corsBypass === 'always') { diff --git a/src/util/nym.ts b/src/util/nym.ts index 04cb8ff28..138b5d073 100644 --- a/src/util/nym.ts +++ b/src/util/nym.ts @@ -9,10 +9,37 @@ import { import { EdgeLog } from '../types/types' /** - * Configuration options for the NYM mixFetch client. + * Mint the client id for one mixnet client. + * + * `clientId` names the client's persistent key storage, so every setup that + * passes the same id adopts whatever registration the previous one left + * behind. A registration that went bad therefore stays bad for every later + * connect, including across app launches, which surfaces as a wallet that + * never syncs and never recovers. Nym's guidance is to rerandomise on + * connect: a fresh id simply abandons the poisoned storage. + * + * The cost is a fresh gateway registration per setup, which the measured + * ~10s handshake already covers. + */ +const makeClientId = (): string => { + const bytes = new Uint8Array(16) + const { crypto } = globalThis + if (crypto?.getRandomValues != null) { + crypto.getRandomValues(bytes) + } else { + for (let i = 0; i < bytes.length; ++i) { + bytes[i] = Math.floor(Math.random() * 256) + } + } + const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')) + return `edge-core-js-${hex.join('')}` +} + +/** + * Configuration options for the NYM mixFetch client, minus the per-setup + * `clientId` that `initMixFetch` adds. */ -export const mixFetchOptions: SetupMixFetchOps = { - clientId: 'edge-core-js-2026-03-10', +const mixFetchOptions: Omit = { preferredGateway: '5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8', // with WSS preferredNetworkRequester: '5x6q9UfVHs5AohKMUqeivj7a556kVVy7QwoKige8xHxh.6CFoB3kJaDbYz6oafPJxNxNjzahpT2NtgtytcSyN9EvF@5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8', @@ -42,7 +69,10 @@ let mixFetchInitPromise: Promise | null = null export async function initMixFetch(log: EdgeLog): Promise { if (mixFetchInitPromise == null) { log('Initializing mixFetch...') - const pending = createMixFetch(mixFetchOptions) + const pending = createMixFetch({ + ...mixFetchOptions, + clientId: makeClientId() + }) // The timeout below can abandon this setup while it is still in flight. // Deliberately do NOT tear it down on late completion: `createMixFetch` // resolves to a healthy global singleton, and disconnecting it (a