Skip to content

Commit 46830e4

Browse files
Copilotclaude
andauthored
Phase B / v3.x: confirm v3 verifier export + Foundry bundle, gate on-chain test (#9)
Confirms `/api/vk/{circuit}/solidity` returns valid Solidity source for `voteCastHomomorphic_8` and `tallyDecrypt_8`, adds `/api/bundle/vote-v3` producing a complete Foundry project (verifier contracts + governance contract + deploy script + tests), and adds the harness scaffolding for on-chain proof verification. The on-chain verification test is gated with `t.Skip` referencing issue #6: gnark's auto-generated Solidity verifiers compile cleanly for these BabyJubJub-flavored Groth16 circuits but reject proofs that `groth16.Verify` accepts in Go. README has a "Known limitations" note pointing at the same issue. Pairing-equation parity is pending dedicated follow-up work — likely either an upstream gnark template fix or a custom verifier path in `solidity/`. Refs #6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b8fd66a commit 46830e4

4 files changed

Lines changed: 693 additions & 1 deletion

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Poll creation requires an EIP-191 `personal_sign` signature from MetaMask or any
5858
- **Visual editor** — draw places, transitions, and arcs in the browser. Models are stored as content-addressed JSON-LD.
5959
- **Solidity generation** — produce deployable contracts and Foundry test suites from any template.
6060
- **ZK circuits** — Groth16 circuits for transfer, mint, burn, approve, transferFrom, vestClaim, and voteCast, all **generated from the Petri net schema** (no hand-written gnark). One `.btw` source produces Solidity + Foundry tests + ZK circuits + witness builders.
61-
- **Deploy bundle** — download a complete Foundry project (`BitwrapZKPoll.sol` + `Verifier.sol` + tests + deploy script) from `GET /api/bundle/vote`.
61+
- **Deploy bundle** — download complete Foundry projects for v1/v2 (`GET /api/bundle/vote`) and v3 homomorphic settlement (`GET /api/bundle/vote-v3`).
6262
- **ERC templates** — start from ERC-20, ERC-721, ERC-1155, or a Vote template. Each is a complete Petri net with guards, arcs, and events.
6363
- **`.btw` DSL** — a compact schema language for defining Petri net models.
6464
- **Remix IDE plugin** — generate and deploy contracts inside Remix at [solver.bitwrap.io](https://solver.bitwrap.io).
@@ -90,6 +90,8 @@ POST /api/solgen Generate Solidity from template
9090
POST /api/testgen Generate Foundry tests from template
9191
POST /api/compile Compile .btw DSL to schema JSON
9292
GET /api/bundle/{template} Download Foundry project (ZIP)
93+
- /api/bundle/vote: v1/v2 poll + voteCast verifier
94+
- /api/bundle/vote-v3: BitwrapZKPollV3 + voteCastHomomorphic_8 + tallyDecrypt_8 verifiers
9395
```
9496

9597
### Operator tools
@@ -145,6 +147,12 @@ GET /api/vk/{circuit} Download verifying key (binary)
145147
GET /api/vk/{circuit}/solidity Download Solidity verifier contract
146148
```
147149

150+
For v3, the bundle includes generated Solidity verifiers and `BitwrapZKPollV3` wiring for close-time settlement with the aggregate artifact (`data/polls/{id}/tally.json`).
151+
152+
## Known limitations
153+
154+
- Exported Solidity verifiers for the current BabyJubJub-flavored v3 circuits (`voteCastHomomorphic_8`, `tallyDecrypt_8`) compile, but can reject proofs that verify successfully in Go (`groth16.Verify`). This upstream limitation is tracked in issue #6; on-chain pairing-equation parity is pending dedicated follow-up work.
155+
148156
## CDN
149157

150158
Client-side modules are available via jsDelivr:

internal/server/bundle_v3.go

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package server
2+
3+
import (
4+
"archive/zip"
5+
"bytes"
6+
"fmt"
7+
"log"
8+
"net/http"
9+
"strings"
10+
)
11+
12+
func (s *Server) handleBundleVoteV3(w http.ResponseWriter, _ *http.Request) {
13+
if s.keyStore == nil {
14+
http.Error(w, "Key store not enabled (start with -key-dir flag)", http.StatusServiceUnavailable)
15+
return
16+
}
17+
18+
voteCircuit := "voteCastHomomorphic_8"
19+
tallyCircuit := "tallyDecrypt_8"
20+
if !s.keyStore.Has(voteCircuit) || !s.keyStore.Has(tallyCircuit) {
21+
http.Error(w, "v3 verifier keys not available (need voteCastHomomorphic_8 + tallyDecrypt_8)", http.StatusServiceUnavailable)
22+
return
23+
}
24+
25+
voteVerifier, err := s.keyStore.ExportSolidityVerifier(voteCircuit)
26+
if err != nil {
27+
http.Error(w, fmt.Sprintf("export %s verifier: %v", voteCircuit, err), http.StatusInternalServerError)
28+
return
29+
}
30+
tallyVerifier, err := s.keyStore.ExportSolidityVerifier(tallyCircuit)
31+
if err != nil {
32+
http.Error(w, fmt.Sprintf("export %s verifier: %v", tallyCircuit, err), http.StatusInternalServerError)
33+
return
34+
}
35+
voteVerifierSol, err := renameVerifierContract(string(voteVerifier), "Verifier_voteCastHomomorphic_8")
36+
if err != nil {
37+
http.Error(w, err.Error(), http.StatusInternalServerError)
38+
return
39+
}
40+
tallyVerifierSol, err := renameVerifierContract(string(tallyVerifier), "Verifier_tallyDecrypt_8")
41+
if err != nil {
42+
http.Error(w, err.Error(), http.StatusInternalServerError)
43+
return
44+
}
45+
46+
files := map[string]string{
47+
"foundry.toml": v3FoundryToml,
48+
"src/BitwrapZKPollV3.sol": v3PollContract,
49+
"src/Verifier_voteCastHomomorphic_8.sol": voteVerifierSol,
50+
"src/Verifier_tallyDecrypt_8.sol": tallyVerifierSol,
51+
"test/BitwrapZKPollV3.t.sol": v3FoundryTest,
52+
"script/DeployV3.s.sol": v3DeployScript,
53+
"README.md": v3BundleREADME,
54+
}
55+
56+
var zipBuf bytes.Buffer
57+
zw := zip.NewWriter(&zipBuf)
58+
for name, content := range files {
59+
fw, err := zw.Create(name)
60+
if err != nil {
61+
log.Printf("bundle-v3: failed to create zip entry %s: %v", name, err)
62+
http.Error(w, "failed to create bundle entry", http.StatusInternalServerError)
63+
return
64+
}
65+
if _, err := fw.Write([]byte(content)); err != nil {
66+
log.Printf("bundle-v3: failed to write zip entry %s: %v", name, err)
67+
http.Error(w, "failed to write bundle entry", http.StatusInternalServerError)
68+
return
69+
}
70+
}
71+
if err := zw.Close(); err != nil {
72+
log.Printf("bundle-v3: failed to finalize zip: %v", err)
73+
http.Error(w, "failed to finalize bundle", http.StatusInternalServerError)
74+
return
75+
}
76+
77+
w.Header().Set("Content-Type", "application/zip")
78+
w.Header().Set("Content-Disposition", "attachment; filename=BitwrapZKPollV3.zip")
79+
_, _ = w.Write(zipBuf.Bytes())
80+
}
81+
82+
func renameVerifierContract(soliditySource, contractName string) (string, error) {
83+
const oldDecl = "contract Verifier {"
84+
if !strings.Contains(soliditySource, oldDecl) {
85+
return "", fmt.Errorf("unexpected verifier format: contract declaration not found")
86+
}
87+
return strings.Replace(soliditySource, oldDecl, "contract "+contractName+" {", 1), nil
88+
}
89+
90+
const v3FoundryToml = `[profile.default]
91+
src = "src"
92+
out = "out"
93+
libs = ["lib"]
94+
solc_version = "0.8.20"
95+
96+
[fmt]
97+
line_length = 120
98+
`
99+
100+
const v3PollContract = `// SPDX-License-Identifier: MIT
101+
pragma solidity ^0.8.20;
102+
103+
interface IVerifierVoteCastHomomorphic8 {
104+
function verifyProof(uint256[8] calldata proof, uint256[38] calldata input) external view;
105+
}
106+
107+
interface IVerifierTallyDecrypt8 {
108+
function verifyProof(uint256[8] calldata proof, uint256[42] calldata input) external view;
109+
}
110+
111+
contract BitwrapZKPollV3 {
112+
struct Ciphertext {
113+
uint256 ax;
114+
uint256 ay;
115+
uint256 bx;
116+
uint256 by;
117+
}
118+
119+
address public immutable owner;
120+
uint256[2] public pkCreator;
121+
IVerifierVoteCastHomomorphic8 public voteVerifier;
122+
IVerifierTallyDecrypt8 public tallyVerifier;
123+
124+
Ciphertext[8] public aggregateCiphertexts;
125+
uint256[8] public settledTallies;
126+
bool public tallyArtifactSet;
127+
bool public settled;
128+
129+
event AggregateStored(uint256 indexed ballotCount);
130+
event PollSettled(uint256[8] tallies);
131+
132+
modifier onlyOwner() {
133+
require(msg.sender == owner, "only owner");
134+
_;
135+
}
136+
137+
constructor(
138+
uint256[2] memory _pkCreator,
139+
address _voteVerifier,
140+
address _tallyVerifier
141+
) {
142+
owner = msg.sender;
143+
pkCreator = _pkCreator;
144+
voteVerifier = IVerifierVoteCastHomomorphic8(_voteVerifier);
145+
tallyVerifier = IVerifierTallyDecrypt8(_tallyVerifier);
146+
}
147+
148+
function setAggregateCiphertexts(Ciphertext[8] calldata aggregate, uint256 ballotCount) external onlyOwner {
149+
require(!settled, "already settled");
150+
aggregateCiphertexts = aggregate;
151+
tallyArtifactSet = true;
152+
emit AggregateStored(ballotCount);
153+
}
154+
155+
function verifyCastVoteProof(
156+
uint256[8] calldata proof,
157+
uint256[38] calldata input
158+
) external view returns (bool) {
159+
try voteVerifier.verifyProof(proof, input) {
160+
return true;
161+
} catch {
162+
return false;
163+
}
164+
}
165+
166+
function verifyTallyDecryptProof(
167+
uint256[8] calldata proof,
168+
uint256[8] calldata tallies
169+
) public view returns (bool) {
170+
if (!tallyArtifactSet) {
171+
return false;
172+
}
173+
174+
uint256[42] memory input;
175+
input[0] = pkCreator[0];
176+
input[1] = pkCreator[1];
177+
178+
uint256 idx = 2;
179+
for (uint256 i = 0; i < 8; i++) {
180+
Ciphertext memory ct = aggregateCiphertexts[i];
181+
input[idx++] = ct.ax;
182+
input[idx++] = ct.ay;
183+
input[idx++] = ct.bx;
184+
input[idx++] = ct.by;
185+
}
186+
for (uint256 i = 0; i < 8; i++) {
187+
input[idx++] = tallies[i];
188+
}
189+
190+
try tallyVerifier.verifyProof(proof, input) {
191+
return true;
192+
} catch {
193+
return false;
194+
}
195+
}
196+
197+
function closePollV3(uint256[8] calldata decryptProof, uint256[8] calldata tallies) external onlyOwner {
198+
require(!settled, "already settled");
199+
require(tallyArtifactSet, "aggregate not set");
200+
require(verifyTallyDecryptProof(decryptProof, tallies), "invalid decrypt proof");
201+
202+
settledTallies = tallies;
203+
settled = true;
204+
205+
emit PollSettled(tallies);
206+
}
207+
}
208+
`
209+
210+
const v3FoundryTest = `// SPDX-License-Identifier: MIT
211+
pragma solidity ^0.8.20;
212+
213+
import {Test} from "forge-std/Test.sol";
214+
import {BitwrapZKPollV3} from "../src/BitwrapZKPollV3.sol";
215+
216+
contract MockVoteVerifier {
217+
bool internal _valid = true;
218+
219+
function setValid(bool valid) external {
220+
_valid = valid;
221+
}
222+
223+
function verifyProof(uint256[8] calldata, uint256[38] calldata) external view {
224+
require(_valid, "invalid vote proof");
225+
}
226+
}
227+
228+
contract MockTallyVerifier {
229+
bool internal _valid = true;
230+
231+
function setValid(bool valid) external {
232+
_valid = valid;
233+
}
234+
235+
function verifyProof(uint256[8] calldata, uint256[42] calldata) external view {
236+
require(_valid, "invalid tally proof");
237+
}
238+
}
239+
240+
contract BitwrapZKPollV3Test is Test {
241+
BitwrapZKPollV3 poll;
242+
MockVoteVerifier voteVerifier;
243+
MockTallyVerifier tallyVerifier;
244+
245+
function setUp() public {
246+
voteVerifier = new MockVoteVerifier();
247+
tallyVerifier = new MockTallyVerifier();
248+
poll = new BitwrapZKPollV3([uint256(1), uint256(2)], address(voteVerifier), address(tallyVerifier));
249+
}
250+
251+
function testVerifyCastVoteProof() public {
252+
uint256[8] memory proof;
253+
uint256[38] memory input;
254+
assertTrue(poll.verifyCastVoteProof(proof, input));
255+
}
256+
257+
function testClosePollV3() public {
258+
BitwrapZKPollV3.Ciphertext[8] memory aggregate;
259+
uint256[8] memory proof;
260+
uint256[8] memory tallies;
261+
tallies[0] = 2;
262+
tallies[1] = 1;
263+
264+
poll.setAggregateCiphertexts(aggregate, 3);
265+
poll.closePollV3(proof, tallies);
266+
267+
assertTrue(poll.settled());
268+
assertEq(poll.settledTallies(0), 2);
269+
assertEq(poll.settledTallies(1), 1);
270+
}
271+
}
272+
`
273+
274+
const v3DeployScript = `// SPDX-License-Identifier: MIT
275+
pragma solidity ^0.8.20;
276+
277+
import {Script} from "forge-std/Script.sol";
278+
import {BitwrapZKPollV3} from "../src/BitwrapZKPollV3.sol";
279+
import {Verifier_voteCastHomomorphic_8} from "../src/Verifier_voteCastHomomorphic_8.sol";
280+
import {Verifier_tallyDecrypt_8} from "../src/Verifier_tallyDecrypt_8.sol";
281+
282+
contract DeployV3 is Script {
283+
function run() external {
284+
vm.startBroadcast();
285+
286+
Verifier_voteCastHomomorphic_8 voteVerifier = new Verifier_voteCastHomomorphic_8();
287+
Verifier_tallyDecrypt_8 tallyVerifier = new Verifier_tallyDecrypt_8();
288+
289+
// Replace with the real creator public key coordinates before broadcast.
290+
uint256[2] memory pkCreator = [uint256(0), uint256(0)];
291+
new BitwrapZKPollV3(pkCreator, address(voteVerifier), address(tallyVerifier));
292+
293+
vm.stopBroadcast();
294+
}
295+
}
296+
`
297+
298+
const v3BundleREADME = `# BitwrapZKPollV3 Foundry Bundle
299+
300+
This bundle contains an on-chain settlement loop for vote schema v3 (homomorphic tally):
301+
302+
- src/BitwrapZKPollV3.sol — v3 governance contract with settlement flow.
303+
- src/Verifier_voteCastHomomorphic_8.sol — Groth16 verifier for per-vote proofs.
304+
- src/Verifier_tallyDecrypt_8.sol — Groth16 verifier for close-time decrypt proofs.
305+
- test/BitwrapZKPollV3.t.sol — harness test for proof verification wiring + settlement.
306+
- script/DeployV3.s.sol — deployment script for local/anvil or production chains.
307+
308+
## Quick start
309+
310+
` + "```bash" + `
311+
git init
312+
forge install foundry-rs/forge-std
313+
forge build
314+
forge test -vv
315+
` + "```" + `
316+
`

internal/server/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,10 @@ func (s *Server) handleBundle(w http.ResponseWriter, r *http.Request) {
810810
http.Error(w, "Template ID required", http.StatusBadRequest)
811811
return
812812
}
813+
if templateID == "vote-v3" {
814+
s.handleBundleVoteV3(w, r)
815+
return
816+
}
813817

814818
tmpl := s.getTemplate(templateID)
815819
if tmpl == nil {

0 commit comments

Comments
 (0)