Skip to content

Commit a4e55b9

Browse files
stackdumpclaude
andcommitted
Phase B / B5.7a: register v3 circuits in WASM prover bundle
cmd/prover-wasm now dispatches voteCastHomomorphic_8 and tallyDecrypt_8 through circuitByName, so the browser-side workerProve('voteCastHomomorphic_8', ...) call lands at the right gnark struct. Without this, the next slice (voter-flow v3 branch in poll.js) would fail at the WASM boundary with "unknown circuit". Bundles a fast Node smoke test that loads public/prover.wasm and verifies both new names dispatch through to VK deserialization (rather than getting rejected at the dispatch step), wired into `go test ./cmd/prover-wasm/...` so any drift between the wasm switch and the JS dispatch surfaces in CI before reaching a browser. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 67867fc commit a4e55b9

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

cmd/prover-wasm/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@ func circuitByName(name string) frontend.Circuit {
295295
return &circuits.TallyProofCircuit64{}
296296
case "tallyProof_256":
297297
return &circuits.TallyProofCircuit256{}
298+
case "voteCastHomomorphic_8":
299+
// Phase B / vote schema v3 — per-voter homomorphic ballot. ~70k
300+
// constraints; first call in a session pays the compile cost.
301+
return &circuits.VoteCastHomomorphicCircuit_8{}
302+
case "tallyDecrypt_8":
303+
// Phase B / vote schema v3 — creator's decrypt proof at close. ~40k
304+
// constraints; lazy-compiled like tallyProof_256.
305+
return &circuits.TallyDecryptCircuit_8{}
298306
default:
299307
return nil
300308
}

cmd/prover-wasm/wasm_smoke_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//go:build !js && !wasm
2+
3+
// This test runs on the *host* toolchain (not the wasm one) and shells
4+
// out to `node` to load public/prover.wasm and verify that the v3
5+
// circuits — voteCastHomomorphic_8 and tallyDecrypt_8 — are registered
6+
// in circuitByName. A divergence between cmd/prover-wasm/main.go's
7+
// switch and the JS-side dispatch surfaces as a test failure here
8+
// before reaching a browser.
9+
10+
package main_test
11+
12+
import (
13+
"os"
14+
"os/exec"
15+
"path/filepath"
16+
"strings"
17+
"testing"
18+
)
19+
20+
func TestProverWasmCircuitsSmoke(t *testing.T) {
21+
if _, err := exec.LookPath("node"); err != nil {
22+
t.Skip("node not installed; skipping wasm smoke test")
23+
}
24+
root := repoRoot(t)
25+
wasmPath := filepath.Join(root, "public", "prover.wasm")
26+
if _, err := os.Stat(wasmPath); err != nil {
27+
t.Skipf("public/prover.wasm not built yet (run `make wasm`): %v", err)
28+
}
29+
30+
cmd := exec.Command("node", "public/prover_wasm_circuits_smoke.mjs")
31+
cmd.Dir = root
32+
out, err := cmd.CombinedOutput()
33+
if err != nil {
34+
t.Fatalf("smoke test failed:\n%s\n%v", out, err)
35+
}
36+
if !strings.Contains(string(out), "All prover.wasm circuit dispatches OK") {
37+
t.Fatalf("smoke test output unexpected:\n%s", out)
38+
}
39+
}
40+
41+
func repoRoot(t *testing.T) string {
42+
t.Helper()
43+
dir, _ := os.Getwd()
44+
for {
45+
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
46+
return dir
47+
}
48+
parent := filepath.Dir(dir)
49+
if parent == dir {
50+
t.Fatal("could not find repo root (no go.mod ancestor)")
51+
}
52+
dir = parent
53+
}
54+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Cheap smoke test: load public/prover.wasm in Node and confirm the
2+
// new v3 circuit names route through circuitByName. We invoke
3+
// loadVerifyOnly with throwaway VK bytes — the deserializer will
4+
// always fail, but the "unknown circuit: X" branch fires *before*
5+
// deserialization, so a circuit that's not registered surfaces as a
6+
// distinguishable error.
7+
//
8+
// Run: node public/prover_wasm_circuits_smoke.mjs
9+
10+
import { readFileSync } from 'node:fs';
11+
import { fileURLToPath } from 'node:url';
12+
import { dirname, join } from 'node:path';
13+
14+
const here = dirname(fileURLToPath(import.meta.url));
15+
16+
// Load wasm_exec.js into the global scope (it expects classic-script
17+
// globals like `globalThis.Go`).
18+
const execText = readFileSync(join(here, 'wasm_exec.js'), 'utf8');
19+
new Function(execText)();
20+
21+
const go = new globalThis.Go();
22+
const wasmBytes = readFileSync(join(here, 'prover.wasm'));
23+
const { instance } = await WebAssembly.instantiate(wasmBytes, go.importObject);
24+
go.run(instance); // fires-and-forgets; the bitwrapProver global is set synchronously during run
25+
26+
// Wait one tick so the global gets set (Go's main writes it during init).
27+
await new Promise((r) => setImmediate(r));
28+
29+
const api = globalThis.bitwrapProver;
30+
if (!api) {
31+
console.error('bitwrapProver not exported from wasm');
32+
process.exit(1);
33+
}
34+
35+
let failures = 0;
36+
function check(name) {
37+
const r = api.loadVerifyOnly(name, new Uint8Array([0, 0]));
38+
const errStr = r && r.error ? String(r.error) : '';
39+
if (errStr.includes('unknown circuit')) {
40+
console.error(`FAIL: ${name} not registered (error: ${errStr})`);
41+
failures++;
42+
return;
43+
}
44+
// Any other error is fine — it means the dispatch reached the
45+
// VK-deserialization step, proving the name was recognized.
46+
console.log(`OK: ${name} dispatches (${errStr || 'no error?'})`);
47+
}
48+
49+
check('voteCastHomomorphic_8');
50+
check('tallyDecrypt_8');
51+
52+
// Sanity: pre-existing names still dispatch.
53+
check('voteCast');
54+
check('tallyProof_16');
55+
56+
if (failures > 0) {
57+
console.error(`\n${failures} smoke check(s) FAILED`);
58+
process.exit(1);
59+
}
60+
console.log('\nAll prover.wasm circuit dispatches OK.');
61+
process.exit(0);

0 commit comments

Comments
 (0)