mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 11:52:25 +00:00
Profiling the live app showed the single biggest CPU cost was the relay pool verifying every incoming event's schnorr signature synchronously on the main thread: ~1.2s of secp256k1 work ([email protected] 855ms) during feed load and tab switches, dropping to ~16ms when skipped. The pool was constructed as `new SimplePool()` with no options, so nostr-tools used its default verifying verifyEvent; the `verifyEvent: () => true` that looked like it disabled this only existed on a mock tools object and was never wired to the real pool. Move verification off the main thread instead of dropping it: - Construct the pool with `verifyEvent: () => true` so it never blocks. - Add signatureVerifyWorker (+ client) that verifies event batches in a Web Worker and returns the valid ids; falls back to main-thread verification only when Workers are unavailable (never silently trusts). - VideoEventBuffer verifies each batch via the worker before committing, and drops events that fail (fails closed). The verifier is injectable so tests can virtualize the worker; subscribeVideos passes it through and returns the flush promise from EOSE so commits are awaitable. Tests: new signatureVerifyWorkerClient.test.mjs (worker protocol + drop-invalid + empty/no-id handling); videoEventBuffer.test.mjs and client.test.mjs adapted to the async, verified commit path with a passthrough verifier. Note: tests/nostr-delete-flow.test.mjs has a PRE-EXISTING failure on `should not trigger subscription callbacks` (fails on clean unstable, and is a standalone script not run by the unit-test runner); its edits here only adapt it to the now-async flush. Co-Authored-By: Claude Opus 4.8 <[email protected]>
119 lines
3.9 KiB
JavaScript
119 lines
3.9 KiB
JavaScript
// Client for signatureVerifyWorker. Verifies event batches off the main thread
|
|
// and resolves with the Set of valid event ids. Falls back to main-thread
|
|
// verification only when Web Workers are unavailable, so signatures are still
|
|
// checked (never silently trusted) in that degraded case.
|
|
|
|
import { devLogger } from "../utils/logger.js";
|
|
import { ensureNostrTools, getCachedNostrTools } from "./toolkit.js";
|
|
|
|
const DEFAULT_TIMEOUT_MS = 10000;
|
|
|
|
let workerInstance = null;
|
|
let workerReady = false;
|
|
let requestId = 0;
|
|
const pending = new Map();
|
|
|
|
function attachWorkerListeners(worker) {
|
|
if (!worker || workerReady) return;
|
|
|
|
worker.addEventListener("message", (event) => {
|
|
const data = event?.data || {};
|
|
const entry = pending.get(data.id);
|
|
if (!entry) return;
|
|
pending.delete(data.id);
|
|
clearTimeout(entry.timeoutId);
|
|
if (data.ok && Array.isArray(data.validIds)) {
|
|
entry.resolve(new Set(data.validIds));
|
|
} else {
|
|
entry.reject(new Error(data?.error?.message || "verify-worker-error"));
|
|
}
|
|
});
|
|
|
|
worker.addEventListener("error", (error) => {
|
|
devLogger.warn("[signatureVerifyWorkerClient] Worker error", error);
|
|
for (const entry of pending.values()) {
|
|
clearTimeout(entry.timeoutId);
|
|
entry.reject(error instanceof Error ? error : new Error("verify-worker-error"));
|
|
}
|
|
pending.clear();
|
|
});
|
|
|
|
workerReady = true;
|
|
}
|
|
|
|
function ensureWorker() {
|
|
if (workerInstance) return workerInstance;
|
|
if (typeof Worker === "undefined") return null;
|
|
try {
|
|
workerInstance = new Worker(new URL("./signatureVerifyWorker.js", import.meta.url), {
|
|
type: "module",
|
|
});
|
|
attachWorkerListeners(workerInstance);
|
|
} catch (error) {
|
|
devLogger.warn("[signatureVerifyWorkerClient] Failed to create worker", error);
|
|
workerInstance = null;
|
|
}
|
|
return workerInstance;
|
|
}
|
|
|
|
// Main-thread fallback used only when Workers are unavailable. Preserves the
|
|
// security guarantee (still verifies) at the cost of main-thread time.
|
|
async function verifyOnMainThread(events) {
|
|
const tools = getCachedNostrTools() || (await ensureNostrTools());
|
|
const valid = new Set();
|
|
if (!tools || typeof tools.verifyEvent !== "function") {
|
|
// Cannot verify at all — fail closed by returning no valid ids.
|
|
return valid;
|
|
}
|
|
for (const event of events) {
|
|
try {
|
|
if (!event || !event.id) continue;
|
|
if (typeof tools.validateEvent === "function" && !tools.validateEvent(event)) continue;
|
|
if (tools.verifyEvent(event) === true) valid.add(event.id);
|
|
} catch (_) {
|
|
// skip
|
|
}
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
/**
|
|
* Verify a batch of events. Resolves with a Set of valid event ids.
|
|
* @param {Array<object>} events
|
|
* @param {{ timeoutMs?: number }} [options]
|
|
* @returns {Promise<Set<string>>}
|
|
*/
|
|
export function verifyEventsInWorker(events, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
const list = Array.isArray(events) ? events.filter((e) => e && e.id) : [];
|
|
if (!list.length) return Promise.resolve(new Set());
|
|
|
|
const worker = ensureWorker();
|
|
if (!worker) {
|
|
return verifyOnMainThread(list);
|
|
}
|
|
|
|
const id = ++requestId;
|
|
return new Promise((resolve, reject) => {
|
|
const timeoutId = setTimeout(() => {
|
|
pending.delete(id);
|
|
reject(new Error("verify-worker-timeout"));
|
|
}, timeoutMs);
|
|
pending.set(id, { resolve, reject, timeoutId });
|
|
try {
|
|
worker.postMessage({ id, events: list });
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
pending.delete(id);
|
|
reject(error instanceof Error ? error : new Error("verify-worker-post-failed"));
|
|
}
|
|
}).catch(async (error) => {
|
|
// On worker failure/timeout, fall back to main-thread verification rather
|
|
// than dropping events or trusting them blindly.
|
|
devLogger.warn("[signatureVerifyWorkerClient] Falling back to main-thread verify:", error?.message || error);
|
|
return verifyOnMainThread(list);
|
|
});
|
|
}
|
|
|
|
// Test/diagnostic helpers.
|
|
export const __signatureVerifyInternals = { verifyOnMainThread };
|