Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/scripts/sync-circuit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions circuits/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
16 changes: 10 additions & 6 deletions circuits/scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")"
Expand Down
82 changes: 82 additions & 0 deletions circuits/scripts/verify-artifacts.mjs
Original file line number Diff line number Diff line change
@@ -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.");
30 changes: 30 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.