Skip to content

Commit e3c9869

Browse files
stackdumpclaude
andcommitted
Add WASM prover for client-side Groth16 proving
cmd/prover-wasm/ — Go WASM binary exposing prove/verify to JS public/prover.js — async JS wrapper with key loading from server public/wasm_exec.js — Go WASM runtime Two modes: - loadKeys(name, keyUrl) — fetch pre-compiled .cs/.pk/.vk (fast) - compileCircuit(name) — compile from scratch in WASM (slow) Build: make wasm (22MB raw, ~4.6MB gzipped) .wasm is gitignored — built on demand. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 166cb52 commit e3c9869

5 files changed

Lines changed: 1006 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/bitwrap
22
data/
33
*.exe
4+
*.wasm
45
.DS_Store
56
.claude/

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
.PHONY: build run test clean
1+
.PHONY: build run test clean wasm
22

33
PORT ?= 8088
44

55
build:
66
go build -o bitwrap ./cmd/bitwrap
77

8+
wasm:
9+
GOOS=js GOARCH=wasm go build -o public/prover.wasm ./cmd/prover-wasm
10+
cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" public/wasm_exec.js
11+
812
run: build
913
./bitwrap -port $(PORT)
1014

1115
test:
1216
go test ./...
1317

1418
clean:
15-
rm -f bitwrap
19+
rm -f bitwrap public/prover.wasm public/wasm_exec.js

cmd/prover-wasm/main.go

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
//go:build js && wasm
2+
3+
package main
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
"math/big"
9+
"syscall/js"
10+
11+
"github.com/consensys/gnark-crypto/ecc"
12+
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
13+
"github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc"
14+
"github.com/consensys/gnark/backend/groth16"
15+
"github.com/consensys/gnark/constraint"
16+
"github.com/consensys/gnark/frontend"
17+
"github.com/consensys/gnark/frontend/cs/r1cs"
18+
19+
circuits "github.com/stackdump/bitwrap-io/prover"
20+
)
21+
22+
// compiled holds loaded circuit keys
23+
var compiled = map[string]*compiledCircuit{}
24+
25+
type compiledCircuit struct {
26+
cs constraint.ConstraintSystem
27+
pk groth16.ProvingKey
28+
vk groth16.VerifyingKey
29+
}
30+
31+
func main() {
32+
fmt.Println("bitwrap-prover WASM loaded")
33+
34+
api := map[string]interface{}{
35+
"version": js.FuncOf(version),
36+
"compileCircuit": js.FuncOf(compileCircuit),
37+
"loadKeys": js.FuncOf(loadKeys),
38+
"prove": js.FuncOf(prove),
39+
"verify": js.FuncOf(verify),
40+
"mimcHash": js.FuncOf(mimcHashJS),
41+
"listCircuits": js.FuncOf(listCircuits),
42+
}
43+
js.Global().Set("bitwrapProver", js.ValueOf(api))
44+
45+
// Block forever
46+
select {}
47+
}
48+
49+
func version(_ js.Value, _ []js.Value) interface{} {
50+
return "0.1.0"
51+
}
52+
53+
// compileCircuit("transfer") — compiles a circuit from scratch (slow, ~seconds)
54+
// Returns: {constraints: N, publicVars: N, privateVars: N}
55+
func compileCircuit(_ js.Value, args []js.Value) interface{} {
56+
if len(args) < 1 {
57+
return jsError("usage: compileCircuit(name)")
58+
}
59+
name := args[0].String()
60+
61+
circuit := circuitByName(name)
62+
if circuit == nil {
63+
return jsError(fmt.Sprintf("unknown circuit: %s", name))
64+
}
65+
66+
cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, circuit)
67+
if err != nil {
68+
return jsError(fmt.Sprintf("compile failed: %v", err))
69+
}
70+
71+
pk, vk, err := groth16.Setup(cs)
72+
if err != nil {
73+
return jsError(fmt.Sprintf("setup failed: %v", err))
74+
}
75+
76+
compiled[name] = &compiledCircuit{cs: cs, pk: pk, vk: vk}
77+
78+
return js.ValueOf(map[string]interface{}{
79+
"constraints": cs.GetNbConstraints(),
80+
"publicVars": cs.GetNbPublicVariables(),
81+
"privateVars": cs.GetNbSecretVariables(),
82+
})
83+
}
84+
85+
// loadKeys("transfer", csBytes, pkBytes, vkBytes) — load pre-compiled keys
86+
// csBytes/pkBytes/vkBytes are Uint8Array from fetched .cs/.pk/.vk files
87+
func loadKeys(_ js.Value, args []js.Value) interface{} {
88+
if len(args) < 4 {
89+
return jsError("usage: loadKeys(name, csBytes, pkBytes, vkBytes)")
90+
}
91+
name := args[0].String()
92+
93+
csData := jsToBytes(args[1])
94+
pkData := jsToBytes(args[2])
95+
vkData := jsToBytes(args[3])
96+
97+
cs := groth16.NewCS(ecc.BN254)
98+
if _, err := cs.ReadFrom(newBytesReader(csData)); err != nil {
99+
return jsError(fmt.Sprintf("load cs: %v", err))
100+
}
101+
102+
pk := groth16.NewProvingKey(ecc.BN254)
103+
if _, err := pk.ReadFrom(newBytesReader(pkData)); err != nil {
104+
return jsError(fmt.Sprintf("load pk: %v", err))
105+
}
106+
107+
vk := groth16.NewVerifyingKey(ecc.BN254)
108+
if _, err := vk.ReadFrom(newBytesReader(vkData)); err != nil {
109+
return jsError(fmt.Sprintf("load vk: %v", err))
110+
}
111+
112+
compiled[name] = &compiledCircuit{cs: cs, pk: pk, vk: vk}
113+
114+
return js.ValueOf(map[string]interface{}{
115+
"constraints": cs.GetNbConstraints(),
116+
"publicVars": cs.GetNbPublicVariables(),
117+
"privateVars": cs.GetNbSecretVariables(),
118+
})
119+
}
120+
121+
// prove("transfer", {from: "1", to: "2", ...}) — generate Groth16 proof
122+
// Returns: {proof: "...", publicInputs: [...]}
123+
func prove(_ js.Value, args []js.Value) interface{} {
124+
if len(args) < 2 {
125+
return jsError("usage: prove(circuit, witnessJSON)")
126+
}
127+
name := args[0].String()
128+
129+
cc, ok := compiled[name]
130+
if !ok {
131+
return jsError(fmt.Sprintf("circuit %q not loaded — call compileCircuit or loadKeys first", name))
132+
}
133+
134+
// Parse witness from JS object or JSON string
135+
var witnessMap map[string]string
136+
if args[1].Type() == js.TypeString {
137+
if err := json.Unmarshal([]byte(args[1].String()), &witnessMap); err != nil {
138+
return jsError(fmt.Sprintf("invalid witness JSON: %v", err))
139+
}
140+
} else {
141+
witnessMap = jsObjectToStringMap(args[1])
142+
}
143+
144+
// Build assignment using the witness factory
145+
factory := &circuits.ArcnetWitnessFactory{}
146+
assignment, err := factory.CreateAssignment(name, witnessMap)
147+
if err != nil {
148+
return jsError(fmt.Sprintf("witness creation failed: %v", err))
149+
}
150+
151+
witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField())
152+
if err != nil {
153+
return jsError(fmt.Sprintf("witness build failed: %v", err))
154+
}
155+
156+
proof, err := groth16.Prove(cc.cs, cc.pk, witness)
157+
if err != nil {
158+
return jsError(fmt.Sprintf("prove failed: %v", err))
159+
}
160+
161+
// Serialize proof
162+
var proofBuf bytesBuffer
163+
proof.WriteTo(&proofBuf)
164+
165+
// Get public witness
166+
pubWitness, _ := witness.Public()
167+
var pubBuf bytesBuffer
168+
pubWitness.WriteTo(&pubBuf)
169+
170+
return js.ValueOf(map[string]interface{}{
171+
"proof": bytesToJS(proofBuf.data),
172+
"publicWitness": bytesToJS(pubBuf.data),
173+
})
174+
}
175+
176+
// verify("transfer", proofBytes, publicWitnessBytes) — verify a proof
177+
func verify(_ js.Value, args []js.Value) interface{} {
178+
if len(args) < 3 {
179+
return jsError("usage: verify(circuit, proofBytes, publicWitnessBytes)")
180+
}
181+
name := args[0].String()
182+
183+
cc, ok := compiled[name]
184+
if !ok {
185+
return jsError(fmt.Sprintf("circuit %q not loaded", name))
186+
}
187+
188+
proofData := jsToBytes(args[1])
189+
pubData := jsToBytes(args[2])
190+
191+
proof := groth16.NewProof(ecc.BN254)
192+
if _, err := proof.ReadFrom(newBytesReader(proofData)); err != nil {
193+
return jsError(fmt.Sprintf("invalid proof: %v", err))
194+
}
195+
196+
pubWitness, err := frontend.NewWitness(nil, ecc.BN254.ScalarField(), frontend.PublicOnly())
197+
if err != nil {
198+
return jsError(fmt.Sprintf("create witness: %v", err))
199+
}
200+
if _, err := pubWitness.ReadFrom(newBytesReader(pubData)); err != nil {
201+
return jsError(fmt.Sprintf("invalid public witness: %v", err))
202+
}
203+
204+
err = groth16.Verify(proof, cc.vk, pubWitness)
205+
if err != nil {
206+
return js.ValueOf(map[string]interface{}{"valid": false, "error": err.Error()})
207+
}
208+
return js.ValueOf(map[string]interface{}{"valid": true})
209+
}
210+
211+
// mimcHash("42", "100") — compute MiMC hash (for building witnesses client-side)
212+
func mimcHashJS(_ js.Value, args []js.Value) interface{} {
213+
if len(args) < 2 {
214+
return jsError("usage: mimcHash(a, b)")
215+
}
216+
h := mimc.NewMiMC()
217+
for _, arg := range args {
218+
var e fr.Element
219+
b := new(big.Int)
220+
b.SetString(arg.String(), 10)
221+
e.SetBigInt(b)
222+
eb := e.Bytes()
223+
h.Write(eb[:])
224+
}
225+
result := h.Sum(nil)
226+
var r fr.Element
227+
r.SetBytes(result)
228+
var rBig big.Int
229+
r.BigInt(&rBig)
230+
return rBig.String()
231+
}
232+
233+
// listCircuits() — list loaded circuits
234+
func listCircuits(_ js.Value, _ []js.Value) interface{} {
235+
names := make([]interface{}, 0, len(compiled))
236+
for name := range compiled {
237+
names = append(names, name)
238+
}
239+
return js.ValueOf(names)
240+
}
241+
242+
// circuitByName returns a zero-valued circuit struct for compilation
243+
func circuitByName(name string) frontend.Circuit {
244+
switch name {
245+
case "transfer":
246+
return &circuits.TransferCircuit{}
247+
case "transferFrom":
248+
return &circuits.TransferFromCircuit{}
249+
case "mint":
250+
return &circuits.MintCircuit{}
251+
case "burn":
252+
return &circuits.BurnCircuit{}
253+
case "approve":
254+
return &circuits.ApproveCircuit{}
255+
case "vestClaim":
256+
return &circuits.VestingClaimCircuit{}
257+
default:
258+
return nil
259+
}
260+
}
261+
262+
// Helpers
263+
264+
func jsError(msg string) interface{} {
265+
return js.ValueOf(map[string]interface{}{"error": msg})
266+
}
267+
268+
func jsToBytes(v js.Value) []byte {
269+
length := v.Get("length").Int()
270+
buf := make([]byte, length)
271+
js.CopyBytesToGo(buf, v)
272+
return buf
273+
}
274+
275+
func bytesToJS(data []byte) js.Value {
276+
arr := js.Global().Get("Uint8Array").New(len(data))
277+
js.CopyBytesToJS(arr, data)
278+
return arr
279+
}
280+
281+
func jsObjectToStringMap(v js.Value) map[string]string {
282+
m := make(map[string]string)
283+
keys := js.Global().Get("Object").Call("keys", v)
284+
for i := 0; i < keys.Length(); i++ {
285+
key := keys.Index(i).String()
286+
m[key] = v.Get(key).String()
287+
}
288+
return m
289+
}
290+
291+
// bytesBuffer wraps a byte slice as an io.Writer
292+
type bytesBuffer struct {
293+
data []byte
294+
}
295+
296+
func (b *bytesBuffer) Write(p []byte) (int, error) {
297+
b.data = append(b.data, p...)
298+
return len(p), nil
299+
}
300+
301+
// bytesReader wraps a byte slice as an io.Reader
302+
type bytesReader struct {
303+
data []byte
304+
pos int
305+
}
306+
307+
func newBytesReader(data []byte) *bytesReader {
308+
return &bytesReader{data: data}
309+
}
310+
311+
func (r *bytesReader) Read(p []byte) (int, error) {
312+
if r.pos >= len(r.data) {
313+
return 0, fmt.Errorf("EOF")
314+
}
315+
n := copy(p, r.data[r.pos:])
316+
r.pos += n
317+
return n, nil
318+
}

0 commit comments

Comments
 (0)