v0.7.0 — Circuit synthesis from Petri net schema
Headline: Hand-written gnark circuits are gone. Every ZK circuit for every ERC template is now generated from metamodel.Schema, closing the "Petri net as single source of truth" loop for ZK proofs alongside the existing Solidity + Foundry pipelines. See #1.
Highlights
Phase 2 — Circuit synthesis (gh#1)
-
prover/synth/package: emits Go source files containing gnark circuit struct +Define()methods directly frommetamodel.Schema. -
All 7 circuits synthesized with byte-identical constraint shapes (parity verified via gnark compile):
Circuit Constraints Public Private mint 662 6 2 approve 662 6 2 burn 14,647 5 41 voteCast 15,261 6 43 transfer 15,967 6 42 vestClaim 15,968 6 43 transferFrom 18,012 7 42 -
prover/circuits.godropped — the hand-written version (478 lines) retired;prover/*_gen.goare the sole source of circuit definitions. -
Schema metadata extensions (
metamodel.Schema):State.MerkleDepth,State.HashFunc,Action.Roles,Action.ZKOps. Allomitempty— existing schema CIDs unchanged. -
New ZKOp primitives:
NullifierBind,CommitmentBind,RangeCheck. Express cryptographic obligations that aren't arc-derivable (nullifier hashes, vote-commitment binding). -
Guard expression compiler:
<state>[<key>] >= <amount>patterns automatically becomeapi.ToBinaryrange checks; non-ZK conjuncts (zero-address checks etc.) are correctly ignored. -
Deterministic output:
make gen-circuits && git diff --exit-codeis a CI gate.
Phase 1 — Vote security + UX
- Fixed registration-bypass vulnerability: unregistered voters could previously cast votes because
if poll.RegistryRoot == ""guards skipped validation. Server now rejects votes with an empty registry (400) and counts registration slots (409 when exhausted). - Runtime
rt.Enabled()gate (slice 2.7): the vote handler now replays events through the Petri net Runtime and checkscastVoteenablement, replacing the earlier manual event-count counter. IntroducedregistrySlotsTokenStatesoregisterVoter → registrySlots → castVoteis a proper token flow. - Sparse Merkle tree in
public/merkle.js: fixes a ~2-minute main-thread hang on vote. Depth-20 tree build drops from ~2M mimcHash ops to O(N·D) (~6ms for a single-voter tree). modInversesign bug inpublic/dev-wallet.jssecp256k1: JS BigInt division truncates toward zero, breaking extended Euclidean for negative inputs. This caused wrong public keys → invalid signatures rejected by the backend. One-line fix.- EIP-6963 provider discovery in
public/poll.js: Trust Wallet and MetaMask coexisting no longer collide onwindow.ethereumlast-writer-wins. - Client-side signing test fixture:
e2e/wallet-fixture.jsinjects awindow.ethereummock that performs real secp256k1 via the pure-JS path. 26 Playwright tests (up from 19).
Tooling & infrastructure
bitwrap -synthesize <template> -output <path>: new CLI for regenerating circuits.make gen-circuits: regenerates all ERC templates.make test-e2e-wallet: headless e2e with the real-signing fixture.- WASM prover moved to Web Worker (no main-thread freeze during proving).
Known gaps (follow-up work)
.btwDSL → synthesizer naming convention (gh#1 residue): the DSL produces uppercase state names (BALANCES), the generators expect lowercase (balances). Runningexamples/erc20.btwthrough-synthesizecurrently returns "schema has no synthesizable actions". Wiring a case-insensitive lookup (or normalizing IDs atdsl.Build) is a small follow-up.- Slice 2.6 synth parity tests are now self-comparisons (no hand-written twin to compare against). Harmless but could be simplified to pure smoke tests.
Upgrade notes
- No config / migration changes. Deploy is
git pull && make build && restart. - Circuit names are stable (
mint,transfer, etc.) so no prover-service client changes needed. prover/circuits.gois gone; if external consumers importedMintCircuitetc. from that file, they now come fromprover/erc020_gen.goand friends — same package, same names.
Full commit history: 55 commits from v0.6.3..v0.7.0. git log v0.6.3..v0.7.0 --oneline for the full list.
🤖 Generated with Claude Code