Skip to content

perf: fast digests for user-signed actions, leaner ws/http per-request paths - #86

Merged
joeblau merged 1 commit into
mainfrom
perf/fast-digests-and-transport
Jul 28, 2026
Merged

perf: fast digests for user-signed actions, leaner ws/http per-request paths#86
joeblau merged 1 commit into
mainfrom
perf/fast-digests-and-transport

Conversation

@joeblau

@joeblau joeblau commented Jul 28, 2026

Copy link
Copy Markdown

Everything on main that had not yet reached origin. This was committed locally as bbfc7ec and is being routed through a PR so it gets CI before any release is cut from it.

Contents

Signing. User-signed actions (approveAgent, usdSend, and the other 15) no longer pay viem's generic hashTypedData. Typehash plans compile once per types identity and domain separators once per chainId, following the _fastDigest pattern already used for Agent/SendMultiSig. Digests are byte-identical to viem's across all 17 types × 5 chain IDs (userSignedDigest.test.ts). Measured 2.5 µs vs 56 µs per digest; multisig_user_signed_3_signers −22%.

Order path — the lock hoist. executeWithShell now holds the nonce lock across only nonce issuance and a dispatch-chain claim; signing moved outside it, so concurrent callers on one wallet sign simultaneously. Wire order is preserved by a per-(wallet × network) dispatch chain rather than by the lock.

Measured with a remote wallet, concurrent approveAgent:

latency eth_accounts eth_chainId signTypedData
N=1 116 ms 1 1 1
N=8 116 ms 1 1 8
N=20 116 ms 1 1 20

Latency is constant in N, bounded by the data-dependency floor. Previously N=20 cost 892–3760 ms with 20 eth_chainId calls. The hoist also incidentally collapses the redundant chain-id and multi-sig leader-address round trips, because build now runs outside the lock and their dedupe caches can finally fire.

Ordering is covered by tests/api/exchange/_dispatchOrder.test.ts: signatures completing in reverse order, jittered latency, and signing failures at the first/middle/last position — a burned nonce must leave a gap without stalling later requests or letting them overtake an earlier nonce still being signed. The suite was verified to have teeth by re-injecting that exact bug.

Websocket / HTTP. Dispatcher timeouts moved to the shared abort.TimeoutWheel (ws_request_round_trip −10%); routing stops allocating per frame — toLowerCase() gated behind an uppercase check, routed event-type strings interned per channel+key (webData3_frame_dispatch_e2e −38%, l2book_dispatch_50_coins −28%).

Perf harness. subscribe_user_trio stabilised (absorbed from perf/stabilize-subscribe-user-trio, which #85 tracked): run-to-run spread 33% → ~3%, so it stops flipping the gate on unrelated PRs. Baseline re-recorded — the committed one was stale since 0.1.4 and failed the local gate closed.

Full suite vs old baseline: 11 faster, 0 regressed, perf:gate PASS.

Note

The perf check will likely go red on the suite-fingerprint guard, since this adds scenarios and re-records the baseline — the same fail-closed path as #83, which the gate's own message sanctions for an intentional suite change.

🤖 Generated with Claude Code

…t paths

User-signed actions (approveAgent, usdSend, and the other 15) no longer pay
viem's generic hashTypedData. The EIP-712 domain is fixed and every shipped
types object is a module constant, so typehash plans compile once per types
identity (WeakMap) and domain separators once per chainId, following the
_fastDigest pattern used for Agent/SendMultiSig. Signing routes through the
raw-digest capability (WASM when present); remote and ledger wallets fall
back to signTypedData unchanged. Digests are byte-identical to viem's
hashTypedData across all 17 types x 5 chain IDs (userSignedDigest.test.ts).
Measured: 2.5 us vs 56 us per digest; multisig_user_signed_3_signers -22%.
Multi-sig computes the shared digest once per call and only after a
capability check, so stub/remote signers never pay for a digest they
cannot use.

Websocket transport:

- The dispatcher schedules request timeouts on the shared abort.TimeoutWheel
  (as HttpTransport already did) instead of a native setTimeout per request:
  ws_request_round_trip -10%.
- Routing stops allocating per frame: .toLowerCase() is gated behind an
  uppercase check (the server sends lowercase hex), and routed event-type
  strings are interned per channel+key instead of re-concatenated:
  webData3_frame_dispatch_e2e -38%, l2book_dispatch_50_coins -28%.
- WebSocketTransport.request drops a pointless async/return await.

Subscriptions and utils:

- fastAssetCtxs decodes the node:zlib Buffer via Buffer.toString("utf8")
  instead of a shared TextDecoder on the hottest decode path; the
  DecompressionStream fallback fuses its two .then hops into one.
- floatToWire renders the double once (toFixed(9)) and derives the 8-decimal
  wire string by digit-9 rounding with carry: -20%, fuzz-verified
  byte-identical over 1.8M values.

Http transport:

- Error paths skip the redaction walk for signature-free payloads (all info
  requests), gated on a wire-string check.
- No AbortController or TimeoutWheel entry is allocated when timeout is null
  and no signal exists; the wheel hands out a shared frozen null-handle.
- Explorer requests skip the pre-send JSON.parse (flat weight needs no
  parsed form); billing snapshots materialize lazily for surcharge/error
  paths only. Info/exchange billing still parses the wire form — a pinned
  test contract requires billing to derive from the serialized payload.

Exchange shell:

- Signing moves outside the per-wallet nonce lock; wire order is preserved
  by a per-(wallet x network) dispatch chain instead, so concurrent callers
  on one wallet (where signing is a network round trip for any remote
  wallet) sign in parallel while the server still sees strictly increasing
  nonces. Covered by the new _dispatchOrder tests.
- extractNonceFieldName and static signatureChainId validation are memoized
  per types/config identity; the multi-sig inner hash no longer round-trips
  through hex; msgpack uint64 writes integers without BigInt boxing.

Perf harness:

- subscribe_user_trio stabilised (from the abandoned
  perf/stabilize-subscribe-user-trio branch): 20 iterations per sample
  instead of 1, and MockWebSocket keeps only the latest instance instead of
  retaining every socket. Run-to-run spread falls from 33% to ~3%, stopping
  the scenario flipping the gate on unrelated PRs.
- New scenarios: eip712_user_signed_digest (+viem oracle pair),
  approve_agent_e2e_no_ecdsa. Baseline re-recorded (the committed one was
  stale since 0.1.4 and failed the local gate closed).

Full suite vs old baseline: 11 faster, 0 regressed. perf:gate PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joeblau
joeblau merged commit 3930596 into main Jul 28, 2026
2 of 3 checks passed
@joeblau
joeblau deleted the perf/fast-digests-and-transport branch July 28, 2026 05:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant