Skip to content

Commit 5c16a14

Browse files
stackdumpclaude
andcommitted
Move WASM prover to Web Worker to prevent UI freezing
The Groth16 proof generation blocks the main thread for several seconds, causing "page unresponsive" dialogs. Fix: run the WASM prover in a dedicated Web Worker via prover-worker.js. - prover.js: dispatches prove/verify/loadKeys to worker via postMessage - prover-worker.js: loads Go WASM runtime, handles messages - Falls back to main-thread execution if Workers unavailable - poll.js: imports from prover.js module instead of window.bitwrapProver Note: voteCast witness size mismatch (48 vs 46) is a pre-existing circuit key issue on the server, not related to this change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 954eb64 commit 5c16a14

3 files changed

Lines changed: 167 additions & 35 deletions

File tree

public/poll.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { mimcHash } from './mimc.js';
44
import { MerkleTree } from './merkle.js';
55
import { buildVoteCastWitness } from './witness-builder.js';
6+
import { prove as workerProve, loadKeys, initProver } from './prover.js';
67

78
// Current poll context
89
window.currentPollId = null;
@@ -317,16 +318,15 @@ window.castVote = async function() {
317318

318319
btn.innerHTML = '<span class="spinner"></span>Generating proof...';
319320

320-
// Try WASM prover first (privacy-preserving), fall back to server
321+
// Try WASM prover in Web Worker first (non-blocking), fall back to server
321322
let proofData;
322323
let clientSideProved = false;
323-
if (window.bitwrapProver && window.bitwrapProver.prove) {
324-
try {
325-
proofData = await window.bitwrapProver.prove(witnessResult.circuit, witnessResult.witness);
326-
clientSideProved = true;
327-
} catch (e) {
328-
console.warn('Client-side proving failed, falling back to server:', e);
329-
}
324+
try {
325+
await initProver();
326+
proofData = await workerProve(witnessResult.circuit, witnessResult.witness);
327+
clientSideProved = true;
328+
} catch (e) {
329+
console.warn('Client-side proving failed, falling back to server:', e);
330330
}
331331
if (!clientSideProved) {
332332
const resp = await fetch('/api/prove', {

public/prover-worker.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Web Worker for WASM Groth16 prover — runs off the main thread.
2+
// The main thread communicates via postMessage.
3+
4+
let proverReady = false;
5+
6+
// Load Go WASM runtime inside the worker
7+
importScripts('./wasm_exec.js');
8+
9+
async function initWasm() {
10+
const go = new Go();
11+
const result = await WebAssembly.instantiateStreaming(fetch('./prover.wasm'), go.importObject);
12+
go.run(result.instance);
13+
14+
// Wait for bitwrapProver global
15+
for (let i = 0; i < 100; i++) {
16+
if (typeof bitwrapProver !== 'undefined') {
17+
proverReady = true;
18+
return;
19+
}
20+
await new Promise(r => setTimeout(r, 50));
21+
}
22+
throw new Error('WASM prover did not initialize');
23+
}
24+
25+
const initPromise = initWasm().catch(e => {
26+
postMessage({ type: 'error', error: `WASM init failed: ${e.message}` });
27+
});
28+
29+
onmessage = async function(e) {
30+
const { id, type, payload } = e.data;
31+
32+
try {
33+
await initPromise;
34+
if (!proverReady) throw new Error('Prover not ready');
35+
36+
let result;
37+
switch (type) {
38+
case 'loadKeys': {
39+
const { name, csBytes, pkBytes, vkBytes } = payload;
40+
result = bitwrapProver.loadKeys(name, csBytes, pkBytes, vkBytes);
41+
if (result.error) throw new Error(result.error);
42+
postMessage({ id, type: 'loadKeys', result });
43+
break;
44+
}
45+
46+
case 'prove': {
47+
const { circuit, witness } = payload;
48+
result = bitwrapProver.prove(circuit, JSON.stringify(witness));
49+
if (result.error) throw new Error(result.error);
50+
postMessage({ id, type: 'prove', result: {
51+
proof: result.proof,
52+
publicWitness: result.publicWitness,
53+
}});
54+
break;
55+
}
56+
57+
case 'verify': {
58+
const { circuit, proof, publicWitness } = payload;
59+
result = bitwrapProver.verify(circuit, proof, publicWitness);
60+
postMessage({ id, type: 'verify', result });
61+
break;
62+
}
63+
64+
case 'mimcHash': {
65+
const { args } = payload;
66+
result = bitwrapProver.mimcHash(...args.map(String));
67+
if (typeof result === 'object' && result.error) throw new Error(result.error);
68+
postMessage({ id, type: 'mimcHash', result: String(result) });
69+
break;
70+
}
71+
72+
case 'listCircuits': {
73+
result = bitwrapProver.listCircuits();
74+
postMessage({ id, type: 'listCircuits', result });
75+
break;
76+
}
77+
78+
default:
79+
throw new Error(`Unknown message type: ${type}`);
80+
}
81+
} catch (err) {
82+
postMessage({ id, type: 'error', error: err.message });
83+
}
84+
};

public/prover.js

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,89 @@
1-
// bitwrap WASM prover — client-side Groth16 proving
2-
// Loads prover.wasm and exposes async API over bitwrapProver global
1+
// bitwrap WASM prover — runs Groth16 proving in a Web Worker to avoid blocking UI.
2+
// Falls back to main-thread execution if Workers are unavailable.
33

4-
let _ready = null;
4+
let _worker = null;
5+
let _pending = {};
6+
let _msgId = 0;
57
let _keyCache = {};
8+
let _initPromise = null;
9+
10+
function sendWorkerMessage(type, payload) {
11+
return new Promise((resolve, reject) => {
12+
const id = ++_msgId;
13+
_pending[id] = { resolve, reject };
14+
_worker.postMessage({ id, type, payload });
15+
});
16+
}
617

7-
// Initialize the WASM prover. Call once, awaits loading.
8-
export async function initProver(wasmUrl = './prover.wasm', execUrl = './wasm_exec.js') {
9-
if (_ready) return _ready;
18+
// Initialize the prover (Web Worker with WASM).
19+
export async function initProver() {
20+
if (_initPromise) return _initPromise;
21+
22+
_initPromise = (async () => {
23+
if (typeof Worker !== 'undefined') {
24+
try {
25+
_worker = new Worker('./prover-worker.js');
26+
_worker.onmessage = (e) => {
27+
const { id, type, result, error } = e.data;
28+
if (id && _pending[id]) {
29+
if (type === 'error') {
30+
_pending[id].reject(new Error(error));
31+
} else {
32+
_pending[id].resolve(result);
33+
}
34+
delete _pending[id];
35+
}
36+
};
37+
_worker.onerror = (e) => {
38+
console.warn('Prover worker error, falling back to main thread:', e.message);
39+
_worker = null;
40+
};
41+
// Give the worker a moment to initialize
42+
await new Promise(r => setTimeout(r, 100));
43+
return;
44+
} catch (e) {
45+
console.warn('Worker creation failed, falling back to main thread:', e);
46+
_worker = null;
47+
}
48+
}
1049

11-
_ready = (async () => {
12-
// Load wasm_exec.js if Go runtime not present
50+
// Fallback: load on main thread (blocks UI during prove)
1351
if (typeof Go === 'undefined') {
1452
await new Promise((resolve, reject) => {
1553
const script = document.createElement('script');
16-
script.src = execUrl;
54+
script.src = './wasm_exec.js';
1755
script.onload = resolve;
1856
script.onerror = () => reject(new Error('Failed to load wasm_exec.js'));
1957
document.head.appendChild(script);
2058
});
2159
}
2260

2361
const go = new Go();
24-
const result = await WebAssembly.instantiateStreaming(fetch(wasmUrl), go.importObject);
25-
go.run(result.instance); // starts the Go main() goroutine
62+
const result = await WebAssembly.instantiateStreaming(fetch('./prover.wasm'), go.importObject);
63+
go.run(result.instance);
2664

27-
// Wait for bitwrapProver to be set
2865
for (let i = 0; i < 100; i++) {
2966
if (typeof bitwrapProver !== 'undefined') return;
3067
await new Promise(r => setTimeout(r, 50));
3168
}
3269
throw new Error('WASM prover did not initialize');
3370
})();
3471

35-
return _ready;
72+
return _initPromise;
3673
}
3774

38-
// Compile a circuit from scratch (slow — runs trusted setup in WASM).
39-
// Returns { constraints, publicVars, privateVars } or throws.
4075
export async function compileCircuit(name) {
4176
await initProver();
77+
if (_worker) {
78+
return sendWorkerMessage('compileCircuit', { name });
79+
}
4280
const result = bitwrapProver.compileCircuit(name);
4381
if (result.error) throw new Error(result.error);
4482
return result;
4583
}
4684

47-
// Load pre-compiled keys from the server (fast — skips compilation).
48-
// Fetches .cs, .pk, .vk from keyUrl/{name}.cs etc.
4985
export async function loadKeys(name, keyUrl) {
5086
await initProver();
51-
5287
if (_keyCache[name]) return _keyCache[name];
5388

5489
const [csResp, pkResp, vkResp] = await Promise.all([
@@ -67,40 +102,53 @@ export async function loadKeys(name, keyUrl) {
67102
vkResp.arrayBuffer().then(b => new Uint8Array(b)),
68103
]);
69104

105+
if (_worker) {
106+
const result = await sendWorkerMessage('loadKeys', { name, csBytes, pkBytes, vkBytes });
107+
_keyCache[name] = result;
108+
return result;
109+
}
110+
70111
const result = bitwrapProver.loadKeys(name, csBytes, pkBytes, vkBytes);
71112
if (result.error) throw new Error(result.error);
72113
_keyCache[name] = result;
73114
return result;
74115
}
75116

76-
// Generate a Groth16 proof. Returns { proof: Uint8Array, publicWitness: Uint8Array }.
77-
// witness is an object with string keys and string values (decimal field elements).
117+
// Generate a Groth16 proof — runs in Web Worker (non-blocking).
78118
export async function prove(circuitName, witness) {
79119
await initProver();
120+
if (_worker) {
121+
return sendWorkerMessage('prove', { circuit: circuitName, witness });
122+
}
123+
// Fallback: main thread (will block UI)
80124
const result = bitwrapProver.prove(circuitName, JSON.stringify(witness));
81125
if (result.error) throw new Error(result.error);
82-
return {
83-
proof: result.proof,
84-
publicWitness: result.publicWitness,
85-
};
126+
return { proof: result.proof, publicWitness: result.publicWitness };
86127
}
87128

88-
// Verify a proof. Returns { valid: boolean, error?: string }.
89129
export async function verify(circuitName, proof, publicWitness) {
90130
await initProver();
131+
if (_worker) {
132+
return sendWorkerMessage('verify', { circuit: circuitName, proof, publicWitness });
133+
}
91134
return bitwrapProver.verify(circuitName, proof, publicWitness);
92135
}
93136

94-
// Compute MiMC hash (convenience — also available in mimc.js without WASM).
95137
export async function mimcHash(...args) {
96138
await initProver();
139+
if (_worker) {
140+
const result = await sendWorkerMessage('mimcHash', { args: args.map(String) });
141+
return BigInt(result);
142+
}
97143
const result = bitwrapProver.mimcHash(...args.map(String));
98144
if (typeof result === 'object' && result.error) throw new Error(result.error);
99145
return BigInt(result);
100146
}
101147

102-
// List loaded circuits.
103148
export async function listCircuits() {
104149
await initProver();
150+
if (_worker) {
151+
return sendWorkerMessage('listCircuits', {});
152+
}
105153
return bitwrapProver.listCircuits();
106154
}

0 commit comments

Comments
 (0)