Skip to content

perf: verify documents in the preload with Web Crypto instead of JS SHA-256 - #322

Merged
lidonggui merged 2 commits into
mainfrom
perf/preload-native-digest
Oct 11, 2026
Merged

lidonggui merged 2 commits into
mainfrom
perf/preload-native-digest

Conversation

@lidonggui

Copy link
Copy Markdown
Collaborator

What

The preload still verifies every document it receives against the envelope hash. The integrity check is unchanged; what changed is how the hash is computed.

  • The validators are synchronous, the sandboxed preload has no node:crypto, and Web Crypto only answers with a promise. So the check used to run a plain-JS SHA-256 on the renderer thread.
  • New collectDigestClaims(run) runs any validator with the envelope-hash checks collected instead of computed. Every other check runs as before.
  • New verifyDigestClaims(claims) then checks each claim with crypto.subtle.digest('SHA-256'), falling back to the portable sha256Hex if Web Crypto is missing. A payload passes only if the validator passes and every claim verifies, which is the same verdict as the synchronous path. No document validator branches on the hash result: unions are discriminated by status / exact keys, so collecting can't change which branch accepts.
  • Outside collectDigestClaims, isNotoDocumentWire still hashes synchronously. Tests and any other caller are unchanged.
  • In the preload, invoke, invokeFileTruth, invokeWorkspace and subscribe hand payloads on through one arrival-ordered chain. A later event is never delivered before an earlier document, whether or not it carries one itself.

Measured (huge 8 MB, packaged, Linux box, interleaved with f25d1fe)

before after
preload document check on the renderer thread (profiled) ~258 ms (compressSha256Block ~201) ~52 ms (UTF-8 encode + structure; the digest runs off-thread, ~9 ms to await)
profiled open 1,721 / 1,758 ms 1,605 / 1,514 ms
bench open huge, BENCH_RUNS=5 1,623 / 1,673 ms 1,522 / 1,465 ms
bench open large 661 / 658 ms 590 / 587 ms

Tests

  • New preload-native-digest.test.ts:
    • The Web Crypto digest equals the JS SHA-256 (and node:crypto) on empty input, every padding boundary (55–128 bytes), CJK + emoji + lone-surrogate text, a BOM, and 3 MB of random bytes.
    • Collected claims accept a genuine wire and refuse a same-length forged text, with and without the native digest, matching the sync verdict.
    • The BOM is restored before hashing.
    • Collection is scoped to the call.
  • pnpm typecheck and pnpm test pass.

…HA-256

The preload checks every document it receives against its envelope hash.
The validators are synchronous and the sandboxed preload has no
node:crypto, so the check ran a plain-JavaScript SHA-256: ~200 ms of
renderer time for the 8 MB bench document, on every open.

Validators now run with the hash checks collected (collectDigestClaims);
the preload awaits them through crypto.subtle.digest (verifyDigestClaims)
before handing the payload on. Same checks, same verdict, and outside that
call isNotoDocumentWire still hashes synchronously. Deliveries that can
carry a document go through one arrival-ordered chain, so a later event is
never handed on before an earlier document.

Renderer-thread preload check on huge ~258 -> ~52 ms; packaged Linux open
~1.65 -> ~1.49 s (interleaved).
…igest

# Conflicts:
#	docs/performance/measurements.md
@lidonggui
lidonggui merged commit a6ed28f into main Oct 11, 2026
7 checks passed
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