@hisoka-io/anon-rpc-worker 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  All notable changes to `@hisoka-io/anon-rpc-worker`.
4
4
 
5
+ ## 0.4.0 (2026-10-10)
6
+
7
+ Built on `@hisoka-io/nox-client` 0.8.0. Every 0.3.0 config stays valid; with the new defaults, https calls go
8
+ through TLS tunnels and need exits on nox rc.9 or later with `[tunnel]` enabled.
9
+
10
+ ### Added
11
+
12
+ - End-to-end TLS (README "End-to-end TLS"): the worker runs TLS 1.3 and 1.2 itself (rustls with ring in a second
13
+ embedded WebAssembly module, `packages/nox-tls`, compiled at boot) and exits relay TLS records over one TCP
14
+ connection per tunnel. Requests and responses stay end-to-end encrypted between the worker and the RPC provider,
15
+ and the mixnet keeps the sender anonymous; the exit sees the provider's host name, timing and sizes.
16
+ - Config `tls` (`"required"` default, `"preferred"`, `"off"`), `tlsSession` (`"per-call"` default, `"keep-alive"`),
17
+ `tlsSpares`, `tlsSpareTtlMs`, `tlsKeepAliveMs`, `tlsMaxCallsPerSession`, `tlsOpenTimeoutMs`, `tlsCopyAfterMs`,
18
+ `tlsGapMs`, `tlsMaxCopies`.
19
+ - Per-call sessions with handshaken spares per recently used host, opened in the background after the first wallet
20
+ call settled; keep-alive sessions as an opt-in. Resumption and 0-RTT stay off; ALPN is `http/1.1`.
21
+ - HTTP/1.1 in the worker: caller headers keep their order and duplicates; framing, hop-by-hop and `User-Agent`
22
+ headers are set by the encoder alone; CR, LF and NUL are refused; a whole JSON request is padded to a size bucket;
23
+ responses are complete only by HTTP framing or TLS close_notify.
24
+ - Call code `permission-denied` (the tunnel exit refuses the destination). Log events `boot.tls`, `tls.open`,
25
+ `tls.fallback`, `tls.reject`, `tls.exit.skipped`, `tls.retry`.
26
+ - Build: `nox-tls` built with the pinned toolchain and a pinned clang (`DEBIAN_SNAPSHOT`, `CLANG_PACKAGE_VERSION`,
27
+ `CLANG_VERSION` in `scripts/toolchain.env`); provenance records its digest, clang, the webpki-roots version and
28
+ release date, and `extraRoots`. `build.mjs --extra-root` and `build-test-worker.mjs --extra-root` for test beds;
29
+ `build-worker.sh --release` refuses a bundle with extra roots.
30
+ - Release gate: `verify-snapshot.mjs --release` requires an exit whose capability hints carry `tunnel_v1`.
31
+
32
+ ## 0.3.0 (2026-10-06)
33
+
34
+ Every 0.2.0 config stays valid. Works against entries on nox 0.4.0-rc.6 and on nodes with claim protocol v2.
35
+
36
+ ### Added
37
+
38
+ - Config `hedgeAfterMs` (default 3,000, 0 turns it off): resendable reads send a second copy on another route at
39
+ about the observed p95 reply time; the first reply wins.
40
+ - The client runs with the low-latency settings: binary, retaining, long-polling claims where the entry supports
41
+ them; concurrent claims with data blocks first; immediate resend on a lost reply; two transport-failure resends;
42
+ same-entry resends for a single bridge; a standby entry kept connected (claims share the primary connection).
43
+ - Wallet calls first: the first registry check waits for the first call to settle (at most 15 s).
44
+ - Compressed replies: upstream requests carry `accept-encoding: gzip` where the runtime has `DecompressionStream`,
45
+ and the worker inflates gzip bodies itself, capped at `maxResponseBytes`. A 414 KB `eth_getLogs` reply crosses the
46
+ mixnet as about 41 KB (2 reply packets instead of 14), a full block of 490 KB as about 81 KB (3 instead of 16).
47
+ - Worker-local answers: `eth_chainId` and `net_version` per upstream URL once a second, independently routed
48
+ request agrees. Reads addressed by block hash (`eth_getBlockByHash`, `eth_getBlockReceipts`, `eth_getLogs`
49
+ with `blockHash`, EIP-1898 `{blockHash}` state reads, kept once two calls returned the same result) are behind
50
+ the `LocalAnswers` option `blockHashReads`, off in the worker until answers are checked against the hash. A hit answers at
51
+ once with the caller's JSON-RPC `id` and sends nothing. `Cache-Control: no-cache` or `no-store` skips it. An
52
+ empty log or receipt list is always fetched again, since an upstream behind the chain head answers `[]`.
53
+ - The client's send-path settings: chunked packet writes, a send-window warm-up after each entry connection
54
+ opens, and concurrent calls spread over the pinned and standby entries.
55
+
5
56
  ## 0.2.0 (2026-10-06)
6
57
 
7
58
  Built on `@hisoka-io/nox-client` 0.6.0 (`kps.discovery`). Every 0.1.0 config stays valid.
package/README.md CHANGED
@@ -10,15 +10,16 @@ ready the worker reads the registry through the mixnet itself. Where the nodes a
10
10
  looked up at run time, so operators change IPs whenever they like and new nodes join without a new bundle. A cold
11
11
  boot needs no seed server and no public RPC; the bundle reaches the network only through the harness's KPS dialer.
12
12
 
13
- What each party sees: the entry node sees the wallet's IP address and encrypted packets; the exit node sees the
14
- HTTP request (RPC URL, headers it forwards, body) and never who sent it. End-to-end TLS inside the worker,
15
- which also hides the request from the exit, is the next milestone.
13
+ What each party sees: the entry node sees the wallet's IP address and encrypted packets. With end-to-end TLS (the
14
+ default, `tls: "required"`), requests and responses stay end-to-end encrypted between the worker and the RPC
15
+ provider, and the mixnet keeps the sender anonymous: the worker runs TLS itself and the exit relays TLS ciphertext
16
+ over one TCP connection, seeing the provider's host name, timing and sizes (see [End-to-end TLS](#end-to-end-tls)).
16
17
 
17
18
  ## Layout
18
19
 
19
20
  | Path | Contents |
20
21
  |---|---|
21
- | `src/` | The worker: `worker.ts` (bundle entry), `core.ts` (boot, readiness, accept loop, deadlines), `config.ts`, `fetch-map.ts` (`fetch` to Nox `HttpRequest`), `jsonrpc.ts`, `errors.ts`, `log.ts`, `storage.ts`, `spec-types.ts` (anon-rpc SPEC 0.3.2 worker types) |
22
+ | `src/` | The worker: `worker.ts` (bundle entry), `core.ts` (boot, readiness, accept loop, deadlines), `config.ts`, `fetch-map.ts` (`fetch` to Nox `HttpRequest`, redirects, gzip), `tls/` (end-to-end TLS: `tunnel.ts` one exit tunnel, `channel.ts` TLS and HTTP/1.1 over it, `pool.ts` sessions, spares and exit choice, `transport.ts` the per-hop transport and error mapping, `module.ts` the TLS module bindings), `jsonrpc.ts`, `errors.ts`, `log.ts`, `storage.ts`, `spec-types.ts` (anon-rpc SPEC 0.3.2 worker types) |
22
23
  | `snapshot/` | The pinned registry snapshot, its keccak-256, JSON Schema and reviewed capability hints, and the discovery bootstrap (`nox-bootstrap.json`) |
23
24
  | `scripts/` | Snapshot generator and verifier, reproducible WASM and bundle build, hashing, provenance |
24
25
  | `specifier/` | Worker specifier contracts (Foundry) and a read-only deploy planner and inspector |
@@ -41,6 +42,7 @@ optional, and every config valid for 0.1.0 stays valid:
41
42
  | `checkpoint` | | | reserved; any value fails with `bad-config` |
42
43
  | `logLevel` | `"debug"`, `"info"`, `"warn"`, `"error"` | `"info"` | |
43
44
  | `attemptTimeoutMs` | integer | 12,000 | 3,000-60,000 |
45
+ | `hedgeAfterMs` | integer | 3,000 | 0-60,000. Resendable reads send a second copy on another route when no reply arrived after this long, raised to the observed p95 reply time and kept below `attemptTimeoutMs`; the first reply wins. 0 turns hedging off |
44
46
  | `callDeadlineMs` | integer | 25,000 | at least `attemptTimeoutMs`, at most 120,000 |
45
47
  | `maxConcurrentCalls` | integer | 16 | 1-64 |
46
48
  | `maxRequestBytes` | integer | 1,048,576 | 1,024-4,194,304 |
@@ -50,6 +52,16 @@ optional, and every config valid for 0.1.0 stays valid:
50
52
  | `bootRetryMaxMs` | integer | 60,000 | 5,000-300,000 |
51
53
  | `topologySources` | integer | 2 | 1-4 |
52
54
  | `warmup` | boolean | false | one echo through a full route before ready |
55
+ | `tls` | `"required"`, `"preferred"`, `"off"` | `"required"` | end-to-end TLS, see [End-to-end TLS](#end-to-end-tls) |
56
+ | `tlsSession` | `"per-call"`, `"keep-alive"` | `"per-call"` | one TLS session per call, or sessions shared by the calls to one host |
57
+ | `tlsSpares` | integer | 1 | 0-4 handshaken spare sessions per recently used host (per-call mode) |
58
+ | `tlsSpareTtlMs` | integer | 20,000 | 1,000-50,000; an unused spare is closed after this long |
59
+ | `tlsKeepAliveMs` | integer | 30,000 | 1,000-50,000; how long a keep-alive session serves calls after it opened |
60
+ | `tlsMaxCallsPerSession` | integer | 100 | 1-1,000 calls per keep-alive session |
61
+ | `tlsOpenTimeoutMs` | integer | 5,000 | 1,000-30,000; an exit that does not answer a tunnel open in this long is skipped for 10 minutes |
62
+ | `tlsCopyAfterMs` | integer | 2,500 | 500-30,000; another copy of the current tunnel exchange when nothing arrived for this long (raised to the observed p95) |
63
+ | `tlsGapMs` | integer | 800 | 100-10,000; another copy when a gap in the reply stream stays open this long |
64
+ | `tlsMaxCopies` | integer | 3 | 1-5 copies per exchange for silence or a gap |
53
65
 
54
66
  An unknown key, a wrong type or an out-of-range value fails the boot with `bad-config`, naming the field.
55
67
 
@@ -63,6 +75,70 @@ Examples:
63
75
  { gateways: ["100.56.0.72:15005:uEiBVDwIs40bsslDkM-BYb2AOHw3PHe70_bj5U_09r7vdIQ"] }
64
76
  ```
65
77
 
78
+ ## End-to-end TLS
79
+
80
+ The worker carries a TLS client (rustls with ring, compiled to WebAssembly, `packages/nox-tls`) and runs TLS with
81
+ the RPC provider itself. A Nox exit that advertises `tunnel_v1` holds one TCP connection to the provider per tunnel
82
+ and relays TLS records both ways (`ServiceRequest::TunnelV1`, nox `docs/tunnel.md`). Requests and responses stay
83
+ end-to-end encrypted between the worker and the provider, and the mixnet keeps the sender anonymous; the exit relays
84
+ ciphertext.
85
+
86
+ What the exit learns: the provider's host name (from SNI, its own DNS lookup and the IP) and port 443; when a tunnel
87
+ opens and closes; the number, size and timing of TLS records in both directions (a whole JSON request is padded to
88
+ 512 B, 1 KiB, 4 KiB, 16 KiB, then 16 KiB steps, so calls of similar size share one record size; response sizes stay
89
+ visible, and larger buckets are a later milestone); the TLS client fingerprint, shared by every worker of one
90
+ version; in keep-alive mode, which calls share a session. The URL path and query (API keys included), headers,
91
+ JSON-RPC methods and parameters, signed transactions, response content and the sender stay hidden from it, and the
92
+ worker detects any change or truncation of a response.
93
+
94
+ `tls`:
95
+
96
+ | `tls` | https on port 443, host by name | http, another port, or an IP-literal host | no known exit advertises `tunnel_v1` |
97
+ |---|---|---|---|
98
+ | `"required"` (default) | tunnel | rejects `unsupported` | rejects `network-error` |
99
+ | `"preferred"` | tunnel | exit `HttpRequest` (logged `tls.fallback`) | exit `HttpRequest` (logged `tls.fallback`) |
100
+ | `"off"` | exit `HttpRequest` | exit `HttpRequest` | exit `HttpRequest` |
101
+
102
+ The transport is chosen before any byte of a call leaves the worker, from the URL and the capability data, and a
103
+ call that started on a tunnel never continues on `HttpRequest`, redirects included. The capabilities of pinned
104
+ members are the hints in the bundle's snapshot (`snapshot/capabilities.json`), so tunnel exits change, and an exit
105
+ stops counting as one, only with a new bundle; with discovery, other members' capabilities come from the seed's
106
+ liveness data. `"preferred"` uses plaintext whenever no known exit advertises `tunnel_v1`; `"required"` keeps every
107
+ call end-to-end encrypted. An exit that answers `Disabled`, or no tunnel open within `tlsOpenTimeoutMs`, is skipped
108
+ for 10 minutes and the open moves to another tunnel exit; skipped exits still count as tunnel exits, so with every one
109
+ skipped a call fails with `network-error` under both settings.
110
+
111
+ Session modes:
112
+
113
+ - `"per-call"` (default): every call has its own TLS session and `Connection: close`; no resumption, no 0-RTT, a
114
+ fresh key share each time. `tlsSpares` keeps handshaken, unused sessions for hosts the wallet called, opened in
115
+ the background (only after the first wallet call settled) on a random tunnel exit after an exponential delay
116
+ (mean 2 s), each serving one call. Each call travels in its own session; at low traffic the timing of a spare's
117
+ open can still chain sequential calls of one wallet to one host, which weakens as traffic grows.
118
+ - `"keep-alive"`: calls to one host share a session on one exit for `tlsKeepAliveMs`, up to 4 sessions per host.
119
+ The exit links the calls of a session, and the provider sees them on one TLS connection.
120
+
121
+ Latency: a call on a ready session (a spare, or a warm keep-alive session) costs what the exit `HttpRequest` path
122
+ costs. The first call to a host, and calls beyond the ready spares, add one mixnet round trip for the handshake.
123
+
124
+ Certificates are checked against the Mozilla root store compiled into the bundle (webpki-roots; the version and its
125
+ release date are in the provenance, refreshed with every worker release), with the host name and the device clock;
126
+ a wrong clock fails with a message that names it. Each worker release ships its root store, so wallets keep their
127
+ roots current by moving to new worker releases. Resends of a tunnel exchange go to the same exit through another
128
+ entry and mix and carry identical bytes, which the exit writes once; after a lost tunnel only reads that may be resent
129
+ run again on a new tunnel.
130
+
131
+ `tls` governs the wallet's calls. The chain check's registry reads (discovery) use the exit `HttpRequest` path: public
132
+ chain data, accepted only when a quorum of exit and provider pairs agrees byte for byte.
133
+
134
+ Next milestones: registry reads through tunnels as well, Encrypted Client Hello for providers that publish it, and
135
+ a hybrid post-quantum key exchange (X25519MLKEM768).
136
+
137
+ Errors: `permission-denied` (the exit refuses the destination), `network-error` (no tunnel exit, a certificate
138
+ failure, the exit cannot reach the provider), `protocol-error` (TLS or HTTP checks failed, a truncated response),
139
+ `too-large`, `timeout`. Log events: `boot.tls`, `tls.open` (debug: handshake time and CPU), `tls.fallback{reason}`,
140
+ `tls.reject{code}`, `tls.exit.skipped`, `tls.retry`; none carries bytes or a host name.
141
+
66
142
  ## Discovery (identity from chain, location at run time)
67
143
 
68
144
  The bundle pins `snapshot/nox-bootstrap.json` (`nox-anon-rpc-bootstrap/1`) next to the snapshot, both covered by
@@ -124,19 +200,42 @@ were read at, and when each member outside the snapshot was first seen. At most
124
200
  days. A cache that fails any check (another registry, too many records, one malformed record) is ignored whole, and
125
201
  a learned address is used only after the node's `/metadata.json` names the same member.
126
202
 
203
+ ## Release 0.3.0
204
+
205
+ | | |
206
+ |---|---|
207
+ | `workerHash` (keccak-256 of `dist/anon-rpc-worker.js`) | `0x24604525d220bcc7e39f2dbc22966a814600a63ada51baafad5dd0bcc1d28549` |
208
+ | Size | 961,799 bytes; 279,515 bytes with `gzip -9 -n` |
209
+ | sha256 | `0458b19258dffcfb9ce23d584096f3df06532080390d34ee71b201b6b78dbbac` |
210
+ | Specifier | Ethereum Sepolia `0x29B51ca9Ad80E9c0B0D111C8748E6a7908b82eDB` (`ImmutableWorkerSpecifier`, resolvers in [specifier/README.md](./specifier/README.md#deployments)) |
211
+ | Snapshot | NoxRegistry at Arbitrum Sepolia block 316,207,920, keccak-256 `0xa351960e…94fbb2` |
212
+ | nox-wasm module | 188,708 bytes, sha256 `5171416972ea4499fd4b4ea8bcadf002f4c10f10ed42cafb0d5a426121da9c4f` |
213
+ | Built from | `nox-sdk` commit `6bddd851c5b1f4e5508f4718e2ea9b4db626b17f`, rustc 1.95.0, wasm-pack 0.13.1, wasm-bindgen 0.2.114, binaryen 117, esbuild 0.27.3 |
214
+
215
+ The npm package carries the bundle and provenance file of the first pinned container build.
216
+
217
+ Release 0.2.0: `workerHash` `0x0a58f9915f686950072a4786249d396ecbf2194a39ac835effe8ea1a7c76f324` (924,847 bytes, commit
218
+ `ef019a7`), specifier `0x29b4a6A8Cc11769531854d87f9F33EC63Efe8fe6`.
219
+
220
+ Two builds in fresh containers of the pinned images (at two different paths) give these exact bytes
221
+ (`scripts/verify-reproducible.sh --ref 6bddd85`). Run it from the repository root as
222
+ `packages/anon-rpc-worker/scripts/verify-reproducible.sh` (or `pnpm verify:reproducible` inside the package); host builds (`--native`,
223
+ `--local`) use rustc 1.95.0, so set `RUSTUP_TOOLCHAIN=1.95.0` when the host default differs.
224
+
127
225
  ## What the bundle contains
128
226
 
129
227
  - `dist/anon-rpc-worker.js`: a single IIFE built by esbuild (target es2022, not minified, so it can be read
130
228
  and audited). Every module is inlined: the worker source, the `@hisoka-io/nox-client` sources, the
131
- `nox-wasm` WebAssembly module (base64), the pinned registry snapshot and the discovery bootstrap, each embedded
132
- byte for byte as one string.
229
+ `nox-wasm` and `nox-tls` WebAssembly modules (base64), the pinned registry snapshot and the discovery bootstrap,
230
+ each embedded byte for byte as one string. The TLS module is compiled at boot, off the first call's path.
133
231
  The bundle has no imports and never loads code or WebAssembly by URL. The SDK's ambient `fetch` and
134
232
  `WebSocket` defaults are replaced at build time by a stand-in that fails closed, and the build checks the
135
233
  output for any other network API.
136
234
  - `dist/anon-rpc-worker.js.keccak256`: the Ethereum keccak-256 of those exact bytes, the value a worker
137
235
  specifier pins as `workerHash()`.
138
236
  - `dist/anon-rpc-worker.provenance.json`: source commit, toolchain versions, input digests (snapshot, bootstrap,
139
- WASM, lockfile) and output digests.
237
+ both WASM modules, lockfile), the compiled-in root store (webpki-roots version and release date, and
238
+ `extraRoots`, 0 in every release) and output digests.
140
239
 
141
240
  ## Pinned registry snapshot
142
241
 
@@ -155,13 +254,16 @@ The committed release snapshot is of block 316207920 (finalized, read and re-ver
155
254
  providers of different organisations). It carries the KPS addresses nox-1, nox-2 and nox-8 publish, so the worker
156
255
  finds its entries from the snapshot alone, and the default anchors and the chain check add any later moves.
157
256
  The release gate names the seven mix and exit nodes, which serve the mixnet without a KPS listener, with
158
- `--allow-missing-kps`.
257
+ `--allow-missing-kps`, and requires an exit whose capability hints carry `tunnel_v1`: the hints in
258
+ `snapshot/capabilities.json` come from the seed topology of 2026-10-10 (fleet on nox 0.4.0-rc.9), where the three
259
+ exits advertise `tunnel_v1`. The registry state at that block is the same as in 0.3.0 (fingerprint
260
+ `0x14ca3b69…7c75`); with the new hints the snapshot's keccak-256 is `0x1090f071…014681`.
159
261
 
160
262
  ```bash
161
263
  pnpm --filter @hisoka-io/nox-client build
162
264
  node scripts/make-snapshot.mjs --rpc <arbitrum-sepolia-rpc> --rpc <second-rpc> [--block <n>]
163
265
  node scripts/verify-snapshot.mjs --offline # schema, hashes, SDK-derived fields
164
- node scripts/verify-snapshot.mjs --rpc <rpc> --rpc <rpc> --release # chain re-read + every member publishes a KPS address
266
+ node scripts/verify-snapshot.mjs --rpc <rpc> --rpc <rpc> --release # chain re-read, KPS addresses, a tunnel exit
165
267
  ```
166
268
 
167
269
  ## Building and checking the hash
@@ -173,10 +275,11 @@ node packages/anon-rpc-worker/scripts/hash.mjs packages/anon-rpc-worker/dist/ano
173
275
  ```
174
276
 
175
277
  `scripts/toolchain.env` pins every tool that shapes the bytes: the Node and Rust container images by digest,
176
- rustc, wasm-pack, wasm-bindgen and binaryen (with sha256 digests of their release archives); pnpm comes from the
177
- repository's `packageManager` field and esbuild from the lockfile. `scripts/build-wasm.sh` follows the
178
- tor-js recipe: path remapping, a `RUSTC_WRAPPER` that makes cargo's metadata host-independent, `--locked`, and
179
- a pinned `wasm-opt` on `PATH`.
278
+ rustc, wasm-pack, wasm-bindgen and binaryen (with sha256 digests of their release archives), and clang with llvm-ar
279
+ for ring's C sources in `nox-tls` (Debian bookworm packages at a snapshot.debian.org timestamp, the same on x86_64
280
+ and aarch64); pnpm comes from the repository's `packageManager` field and esbuild from the lockfile.
281
+ `scripts/build-wasm.sh` follows the tor-js recipe: path remapping, a `RUSTC_WRAPPER` that makes cargo's metadata
282
+ host-independent, `--locked`, and a pinned `wasm-opt` on `PATH`.
180
283
 
181
284
  `scripts/verify-reproducible.sh` builds a commit twice from clean `git archive` exports in fresh containers
182
285
  of the pinned images, at two different paths, and compares the WASM, bundle and provenance digests
@@ -185,12 +288,14 @@ container builds and requires all three to match; `--local --bwrap` runs the two
185
288
  namespaces at the container paths, for hosts without a container runtime.
186
289
 
187
290
  For a test bed, `node scripts/build.mjs --snapshot <file>` embeds another canonical snapshot (for example a
188
- local mesh); the provenance records which snapshot a bundle carries.
291
+ local mesh) and `--extra-root <ca.der>` a test CA next to the Mozilla roots; the provenance records which snapshot a
292
+ bundle carries and how many extra roots. The release build (`build-worker.sh --release`) refuses a bundle with an
293
+ extra root (`provenance.mjs --release`).
189
294
 
190
295
  ## Tests
191
296
 
192
297
  ```bash
193
- pnpm --filter @hisoka-io/anon-rpc-worker test # worker, SDK integration over an in-memory KPS network, build
298
+ pnpm --filter @hisoka-io/anon-rpc-worker test # worker, SDK integration over an in-memory KPS network, build, e2e bed unit tests
194
299
  pnpm --filter @hisoka-io/anon-rpc-specifier test # Foundry and harness-path tests (forge, anvil)
195
300
  ```
196
301