From 7bdc0fb00f8f77075a449732971f4689d892e921 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 4 Sep 2026 08:23:50 -0700 Subject: Move nip44 decryption off the main thread — the remaining boot secp256k1 after verification moved off (chunk-60dfb4nt in profiles). The verify worker grows two ops: 'openWraps' opens community (Concord) wraps given the symmetric stream conversation key — nip44 decrypt + the seal's schnorr verify + hash checks per wrap, no secret crossing the thread boundary since stream keys are group material; 'unwrapDMs' runs the full NIP-17 unwrap (two ECDH ops + seal verify) when the wallet holds the raw identity key. Remote signers can't export their key, so their DM path stays on the main thread, exactly as before. The time-sliced main-thread queues survive as fallbacks — a dead or stalled worker only changes WHERE the crypto runs (workerCrypto resolves null and callers re-enqueue), never whether chat catches up. Bonus: subChannel derives its stream key once per channel instead of once per wrap (groupKey does hkdf + ECDH per call — real curve work on a 200-wrap backfill) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz --- src/verify-worker.js | 66 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 15 deletions(-) (limited to 'src/verify-worker.js') diff --git a/src/verify-worker.js b/src/verify-worker.js index 679c1a8..1f5203f 100644 --- a/src/verify-worker.js +++ b/src/verify-worker.js @@ -1,19 +1,55 @@ -// Off-main-thread nostr event verification. Schnorr signature checks (real -// secp256k1) on a relay backlog blocked the UI on boot / when a community -// opens — the carousel-stutter cause a CPU profile pinned to verifyEvent. This -// worker does that work on another thread; the main thread posts events and -// gets back a boolean per event, so the page never freezes on a burst. +// Off-main-thread nostr crypto. Schnorr signature checks (real secp256k1) on +// a relay backlog blocked the UI on boot / when a community opens — the +// carousel-stutter cause a CPU profile pinned to verifyEvent. This worker +// does that work on another thread; the main thread posts events and gets +// back a boolean per event, so the page never freezes on a burst. // -// Bundled to dist/verify-worker.js (see build.js) and loaded as a classic -// Worker. Same verifyEvent nostr-tools uses on the main thread, so semantics -// are identical — this only moves WHERE it runs. +// It also carries the other boot secp256k1 the profiler kept pinning: +// - 'openWraps': community (Concord) wraps — nip44 decrypt under the +// symmetric stream key + the seal's schnorr verify + hash checks. No +// secret crosses the thread boundary: the stream conversation key is +// group material every member derives. +// - 'unwrapDMs': NIP-17 gift wraps when the wallet holds the raw identity +// key (seed-derived / pasted nsec) — two ECDH ops + a seal verify each. +// Remote signers (bunker/extension) can't export their key, so their +// wraps never come here; the main thread keeps that path. +// +// Bundled to dist/verify-worker.js (see build.js). Same openWrap/unwrapDM/ +// verifyEvent the main thread uses, so semantics are identical — this only +// moves WHERE they run. import { verifyEvent } from 'nostr-tools/pure'; +import { hexToBytes } from '@noble/hashes/utils'; +import { openWrap } from './concord.js'; +import { unwrapDM } from './dm.js'; -self.onmessage = (e) => { - const { reqId, events } = e.data || {}; - if (reqId == null || !Array.isArray(events)) return; - const results = events.map((ev) => { - try { return verifyEvent(ev); } catch { return false; } - }); - self.postMessage({ reqId, results }); +self.onmessage = async (e) => { + const { reqId, events, op, wraps, convKey, sk } = e.data || {}; + if (reqId == null) return; + if (Array.isArray(events)) { + const results = events.map((ev) => { + try { return verifyEvent(ev); } catch { return false; } + }); + self.postMessage({ reqId, results }); + return; + } + if (op === 'openWraps' && Array.isArray(wraps)) { + let stream = null; + try { stream = { convKey: hexToBytes(convKey) }; } catch {} + const results = wraps.map((w) => { + try { return stream ? openWrap(w, stream) : null; } catch { return null; } + }); + self.postMessage({ reqId, results }); + return; + } + if (op === 'unwrapDMs' && Array.isArray(wraps)) { + let key = null; + try { key = hexToBytes(sk); } catch {} + const results = []; + for (const w of wraps) { + results.push(key ? await unwrapDM(w, key).catch(() => null) : null); + } + self.postMessage({ reqId, results }); + return; + } + self.postMessage({ reqId, results: null }); }; -- cgit v1.2.3