diff --git a/app/scripts/sync-circuit.mjs b/app/scripts/sync-circuit.mjs index 2ca50ca..4ea1ab8 100644 --- a/app/scripts/sync-circuit.mjs +++ b/app/scripts/sync-circuit.mjs @@ -7,12 +7,14 @@ // node scripts/sync-circuit.mjs --watch # watch & auto-sync on change import { copyFileSync, existsSync, mkdirSync, watch } from "node:fs"; import path from "node:path"; +import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const appDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const repoRoot = path.join(appDir, ".."); const buildDir = path.join(repoRoot, "circuits", "build"); const outDir = path.join(appDir, "public", "circuits"); +const verifierPath = path.join(repoRoot, "circuits", "scripts", "verify-artifacts.mjs"); const files = [ { from: path.join(buildDir, "membership_js", "membership.wasm"), to: "membership.wasm" }, @@ -23,6 +25,16 @@ const files = [ const isWatch = process.argv.includes("--watch"); function sync() { + const verify = spawnSync(process.execPath, [verifierPath], { + cwd: repoRoot, + stdio: "inherit", + }); + + if (verify.status !== 0) { + if (!isWatch) process.exit(verify.status ?? 1); + return; + } + mkdirSync(outDir, { recursive: true }); let missing = false; diff --git a/circuits/package.json b/circuits/package.json index 636e563..c258141 100644 --- a/circuits/package.json +++ b/circuits/package.json @@ -21,6 +21,7 @@ "check-constants": "node scripts/check-poseidon-constants.mjs", "compile": "npm run check-constants && bash scripts/compile.sh", "setup": "bash scripts/setup.sh", + "verify-artifacts": "node scripts/verify-artifacts.mjs", "prove": "bash scripts/prove.sh", "test": "mocha --require tsx/cjs test/**/*.test.js --timeout 120000" }, diff --git a/circuits/scripts/setup.sh b/circuits/scripts/setup.sh index 399a511..7089049 100755 --- a/circuits/scripts/setup.sh +++ b/circuits/scripts/setup.sh @@ -38,12 +38,6 @@ npx --yes snarkjs zkey contribute "$BUILD/membership_0000.zkey" "$BUILD/membersh --name="Sharibo circuit key contribution" -v -e="$(head -c 64 /dev/urandom | base64)" npx --yes snarkjs zkey export verificationkey "$BUILD/membership_final.zkey" verification_key.json -echo "Setup complete -> build/membership_final.zkey, verification_key.json" - -# ── Transcript fingerprinting ──────────────────────────────────────────────── -SNARKJS_VERSION="$(npx --yes snarkjs --version 2>&1 | head -n1 | sed 's/snarkjs@//')" -DATE_NOW="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" - hash_file() { # Portable SHA-256: prefer sha256sum (Linux/WSL), fall back to shasum (macOS). if command -v sha256sum >/dev/null 2>&1; then @@ -55,6 +49,16 @@ hash_file() { fi } +hash_file verification_key.json > verification_key.json.sha256 +hash_file "$BUILD/membership_js/membership.wasm" > membership.wasm.sha256 +hash_file "$BUILD/membership_final.zkey" > membership_final.zkey.sha256 + +echo "Setup complete -> build/membership_final.zkey, verification_key.json" + +# ── Transcript fingerprinting ──────────────────────────────────────────────── +SNARKJS_VERSION="$(npx --yes snarkjs --version 2>&1 | head -n1 | sed 's/snarkjs@//')" +DATE_NOW="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + HASH_VK="$(hash_file verification_key.json)" HASH_ZKEY="$(hash_file "$BUILD/membership_final.zkey")" HASH_PTAU="$(hash_file "$PTAU_FINAL")" diff --git a/circuits/scripts/verify-artifacts.mjs b/circuits/scripts/verify-artifacts.mjs new file mode 100644 index 0000000..20aa33b --- /dev/null +++ b/circuits/scripts/verify-artifacts.mjs @@ -0,0 +1,82 @@ +import { createHash } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const circuitsDir = path.resolve(scriptDir, ".."); +const fixHint = "run `npm run compile && npm run setup` in `circuits/`"; + +const artifacts = [ + { + name: "verification_key.json", + filePath: path.join(circuitsDir, "verification_key.json"), + hashPath: path.join(circuitsDir, "verification_key.json.sha256"), + }, + { + name: "membership.wasm", + filePath: path.join(circuitsDir, "build", "membership_js", "membership.wasm"), + hashPath: path.join(circuitsDir, "membership.wasm.sha256"), + }, + { + name: "membership_final.zkey", + filePath: path.join(circuitsDir, "build", "membership_final.zkey"), + hashPath: path.join(circuitsDir, "membership_final.zkey.sha256"), + }, +]; + +function hashFile(filePath) { + return createHash("sha256").update(readFileSync(filePath)).digest("hex"); +} + +function readExpectedHash(hashPath) { + if (!existsSync(hashPath)) return null; + + const raw = readFileSync(hashPath, "utf8").trim(); + const match = raw.match(/[A-Fa-f0-9]{64}/); + if (match) return match[0].toLowerCase(); + + try { + const parsed = JSON.parse(raw); + const candidates = [parsed.sha256, parsed.hash, parsed["verification_key.json"], parsed["membership.wasm"], parsed["membership_final.zkey"]]; + for (const candidate of candidates) { + if (typeof candidate === "string") return candidate.toLowerCase(); + } + } catch { + // ignore malformed hash manifests and fail below with a clearer message + } + + return null; +} + +function fail(message) { + console.error(`Circuit artifact verification failed: ${message}`); + console.error(`Fix: ${fixHint}`); + process.exit(1); +} + +const manifestPath = path.join(circuitsDir, "artifact-hashes.json"); +const manifest = existsSync(manifestPath) + ? JSON.parse(readFileSync(manifestPath, "utf8")) + : {}; + +for (const artifact of artifacts) { + if (!existsSync(artifact.filePath)) { + fail(`${artifact.name} is missing. ${fixHint}`); + } + + const expected = readExpectedHash(artifact.hashPath) ?? manifest[artifact.name]; + + if (!expected) { + fail(`No committed SHA-256 hash found for ${artifact.name}. ${fixHint}`); + } + + const actual = hashFile(artifact.filePath); + if (actual !== expected.toLowerCase()) { + fail( + `${artifact.name} hash mismatch. Expected ${expected.toLowerCase()} but found ${actual}. ${fixHint}`, + ); + } +} + +console.log("Circuit artifacts verified."); diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 43994f2..078c70e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -245,6 +245,36 @@ Then hard-refresh the browser tab. If it still misbehaves, delete --- +## Local circuit artifacts are stale and fail verification before the app copies them + +**Symptom** + +The browser throws `InvalidProof`, but the real problem is that the local `circuits/build/` +artifacts no longer match the committed circuit setup. This often happens after deleting and +rebuilding the circuit without re-running the trusted setup. + +**Cause** + +`app/scripts/sync-circuit.mjs` used to copy whatever existed in `circuits/build/` without checking +whether the `.wasm` and `.zkey` still match the committed `verification_key.json`. + +**Fix** + +```bash +cd circuits +npm run verify-artifacts +``` + +If the hashes differ, the script aborts with: + +```bash +run `npm run compile && npm run setup` in `circuits/` +``` + +This is the safe recovery path: rebuild the circuit and re-run setup, then re-sync the app. + +--- + **Still stuck?** Re-read [`CONTRIBUTING.md`](../CONTRIBUTING.md) for the dev loop and the [README "Run it" section](../README.md#run-it) for the step order; open an issue if your symptom isn't here. \ No newline at end of file