Skip to content

Commit 82cf116

Browse files
stackdumpclaude
andcommitted
Add persistent circuit key storage and verifying key endpoints
- prover/keystore.go: save/load pk, vk, cs to disk; export Solidity verifier - -key-dir flag enables persistence (keys cached after first compile) - GET /api/vk/{circuit} — download raw verifying key - GET /api/vk/{circuit}/solidity — download Solidity verifier contract - Second startup with -key-dir loads cached keys instantly (skips compilation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3569e88 commit 82cf116

5 files changed

Lines changed: 308 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ make test # Run tests
1717
-data ./data Data directory for CID storage
1818
-no-prover Disable ZK prover (faster startup)
1919
-no-solgen Disable Solidity generation endpoints
20+
-key-dir ./keys Persist circuit keys for fast restarts
2021
-compile path/to/file.btw Compile .btw file to JSON schema on stdout
2122
```
2223

@@ -54,6 +55,8 @@ make test # Run tests
5455
- `POST /api/compile` — compile .btw DSL source to schema JSON
5556
- `GET /api/circuits` — list available ZK circuits
5657
- `POST /api/prove` — submit witness for ZK proof generation
58+
- `GET /api/vk/{circuit}` — download verifying key (binary)
59+
- `GET /api/vk/{circuit}/solidity` — download Solidity verifier contract
5760

5861
## Dependencies
5962

cmd/bitwrap/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func main() {
1919
dataDir := flag.String("data", "./data", "Data directory for storage")
2020
noProver := flag.Bool("no-prover", false, "Disable ZK prover (faster startup)")
2121
noSolgen := flag.Bool("no-solgen", false, "Disable Solidity generation endpoints")
22+
keyDir := flag.String("key-dir", "", "Directory for persistent circuit keys (enables fast restarts)")
2223
compile := flag.String("compile", "", "Compile a .btw file and output JSON schema to stdout")
2324
flag.Parse()
2425

@@ -53,6 +54,7 @@ func main() {
5354
srv := server.New(storage, publicFS, server.Options{
5455
EnableProver: !*noProver,
5556
EnableSolidity: !*noSolgen,
57+
KeyDir: *keyDir,
5658
})
5759

5860
addr := fmt.Sprintf(":%d", *port)

internal/server/server.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
type Options struct {
2525
EnableProver bool
2626
EnableSolidity bool
27+
KeyDir string // directory for persistent circuit keys (empty = no persistence)
2728
}
2829

2930
// Server is the bitwrap HTTP server.
@@ -32,6 +33,7 @@ type Server struct {
3233
publicFS fs.FS
3334
opts Options
3435
proverSvc *prover.Service
36+
keyStore *prover.KeyStore
3537
}
3638

3739
// New creates a new server.
@@ -40,11 +42,12 @@ func New(s *store.FSStore, publicFS fs.FS, opts Options) *Server {
4042
if opts.EnableProver {
4143
log.Printf("Initializing ZK prover (compiling circuits)...")
4244
start := time.Now()
43-
svc, err := prover.NewArcnetService()
45+
svc, ks, err := prover.NewArcnetService(opts.KeyDir)
4446
if err != nil {
4547
log.Printf("WARNING: ZK prover initialization failed: %v", err)
4648
} else {
4749
srv.proverSvc = svc
50+
srv.keyStore = ks
4851
log.Printf("ZK prover ready (%d circuits compiled in %v)", len(svc.Prover().ListCircuits()), time.Since(start))
4952
}
5053
}
@@ -97,6 +100,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
97100
s.handleProve(w, r)
98101
case r.URL.Path == "/api/circuits":
99102
s.handleCircuits(w, r)
103+
case strings.HasPrefix(r.URL.Path, "/api/vk/"):
104+
s.handleVK(w, r)
100105
case r.URL.Path == "/api/compile" && r.Method == http.MethodPost:
101106
s.handleCompile(w, r)
102107

@@ -598,6 +603,48 @@ func (s *Server) handleCircuits(w http.ResponseWriter, r *http.Request) {
598603
json.NewEncoder(w).Encode(map[string]interface{}{"circuits": circuits})
599604
}
600605

606+
// handleVK serves verifying key data for a circuit.
607+
// GET /api/vk/{circuit} — raw verifying key bytes
608+
// GET /api/vk/{circuit}/solidity — Solidity verifier contract
609+
func (s *Server) handleVK(w http.ResponseWriter, r *http.Request) {
610+
if s.keyStore == nil {
611+
http.Error(w, "Key store not enabled (start with -key-dir flag)", http.StatusServiceUnavailable)
612+
return
613+
}
614+
615+
path := strings.TrimPrefix(r.URL.Path, "/api/vk/")
616+
parts := strings.SplitN(path, "/", 2)
617+
circuit := parts[0]
618+
619+
if !s.keyStore.Has(circuit) {
620+
http.Error(w, fmt.Sprintf("circuit %q not found", circuit), http.StatusNotFound)
621+
return
622+
}
623+
624+
// GET /api/vk/{circuit}/solidity
625+
if len(parts) == 2 && parts[1] == "solidity" {
626+
sol, err := s.keyStore.ExportSolidityVerifier(circuit)
627+
if err != nil {
628+
http.Error(w, err.Error(), http.StatusInternalServerError)
629+
return
630+
}
631+
w.Header().Set("Content-Type", "text/plain")
632+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=Verifier_%s.sol", circuit))
633+
w.Write(sol)
634+
return
635+
}
636+
637+
// GET /api/vk/{circuit} — raw binary key
638+
vkBytes, err := s.keyStore.ExportVerifyingKey(circuit)
639+
if err != nil {
640+
http.Error(w, err.Error(), http.StatusInternalServerError)
641+
return
642+
}
643+
w.Header().Set("Content-Type", "application/octet-stream")
644+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.vk", circuit))
645+
w.Write(vkBytes)
646+
}
647+
601648
// handleCompile compiles a .btw DSL source to metamodel schema JSON.
602649
func (s *Server) handleCompile(w http.ResponseWriter, r *http.Request) {
603650
src, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))

prover/keystore.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package prover
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/consensys/gnark-crypto/ecc"
10+
"github.com/consensys/gnark/backend/groth16"
11+
"github.com/consensys/gnark/frontend"
12+
"github.com/rs/zerolog/log"
13+
)
14+
15+
// KeyStore manages persistent storage of compiled circuit keys.
16+
type KeyStore struct {
17+
dir string
18+
}
19+
20+
// NewKeyStore creates a key store at the given directory.
21+
func NewKeyStore(dir string) (*KeyStore, error) {
22+
if err := os.MkdirAll(dir, 0755); err != nil {
23+
return nil, fmt.Errorf("create keystore dir: %w", err)
24+
}
25+
return &KeyStore{dir: dir}, nil
26+
}
27+
28+
// Has returns true if keys exist for the named circuit.
29+
func (ks *KeyStore) Has(name string) bool {
30+
_, err := os.Stat(ks.pkPath(name))
31+
return err == nil
32+
}
33+
34+
// Save writes a compiled circuit's keys to disk.
35+
func (ks *KeyStore) Save(name string, cc *CompiledCircuit) error {
36+
// Save constraint system
37+
{
38+
f, err := os.Create(ks.csPath(name))
39+
if err != nil {
40+
return fmt.Errorf("save cs %s: %w", name, err)
41+
}
42+
if _, err := cc.CS.WriteTo(f); err != nil {
43+
f.Close()
44+
return fmt.Errorf("write cs %s: %w", name, err)
45+
}
46+
f.Close()
47+
}
48+
49+
// Save proving key
50+
{
51+
f, err := os.Create(ks.pkPath(name))
52+
if err != nil {
53+
return fmt.Errorf("save pk %s: %w", name, err)
54+
}
55+
if _, err := cc.ProvingKey.WriteTo(f); err != nil {
56+
f.Close()
57+
return fmt.Errorf("write pk %s: %w", name, err)
58+
}
59+
f.Close()
60+
}
61+
62+
// Save verifying key
63+
{
64+
f, err := os.Create(ks.vkPath(name))
65+
if err != nil {
66+
return fmt.Errorf("save vk %s: %w", name, err)
67+
}
68+
if _, err := cc.VerifyingKey.WriteTo(f); err != nil {
69+
f.Close()
70+
return fmt.Errorf("write vk %s: %w", name, err)
71+
}
72+
f.Close()
73+
}
74+
75+
log.Debug().Str("circuit", name).Str("dir", ks.dir).Msg("Keys saved")
76+
return nil
77+
}
78+
79+
// Load reads a compiled circuit's keys from disk.
80+
func (ks *KeyStore) Load(name string) (*CompiledCircuit, error) {
81+
cs := groth16.NewCS(ecc.BN254)
82+
{
83+
f, err := os.Open(ks.csPath(name))
84+
if err != nil {
85+
return nil, fmt.Errorf("load cs %s: %w", name, err)
86+
}
87+
if _, err := cs.ReadFrom(f); err != nil {
88+
f.Close()
89+
return nil, fmt.Errorf("read cs %s: %w", name, err)
90+
}
91+
f.Close()
92+
}
93+
94+
pk := groth16.NewProvingKey(ecc.BN254)
95+
{
96+
f, err := os.Open(ks.pkPath(name))
97+
if err != nil {
98+
return nil, fmt.Errorf("load pk %s: %w", name, err)
99+
}
100+
if _, err := pk.ReadFrom(f); err != nil {
101+
f.Close()
102+
return nil, fmt.Errorf("read pk %s: %w", name, err)
103+
}
104+
f.Close()
105+
}
106+
107+
vk := groth16.NewVerifyingKey(ecc.BN254)
108+
{
109+
f, err := os.Open(ks.vkPath(name))
110+
if err != nil {
111+
return nil, fmt.Errorf("load vk %s: %w", name, err)
112+
}
113+
if _, err := vk.ReadFrom(f); err != nil {
114+
f.Close()
115+
return nil, fmt.Errorf("read vk %s: %w", name, err)
116+
}
117+
f.Close()
118+
}
119+
120+
return &CompiledCircuit{
121+
Name: name,
122+
CS: cs,
123+
ProvingKey: pk,
124+
VerifyingKey: vk,
125+
Constraints: cs.GetNbConstraints(),
126+
PublicVars: cs.GetNbPublicVariables(),
127+
PrivateVars: cs.GetNbSecretVariables(),
128+
}, nil
129+
}
130+
131+
// CompileAndSave compiles a circuit and persists the keys.
132+
// If keys already exist on disk, loads them instead (skipping compilation).
133+
func (ks *KeyStore) CompileAndSave(p *Prover, name string, circuit frontend.Circuit) (*CompiledCircuit, error) {
134+
if ks.Has(name) {
135+
log.Debug().Str("circuit", name).Msg("Loading cached keys")
136+
return ks.Load(name)
137+
}
138+
139+
log.Debug().Str("circuit", name).Msg("Compiling circuit (no cached keys)")
140+
cc, err := p.CompileCircuit(name, circuit)
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
if err := ks.Save(name, cc); err != nil {
146+
log.Warn().Err(err).Str("circuit", name).Msg("Failed to save keys (will recompile next time)")
147+
}
148+
149+
return cc, nil
150+
}
151+
152+
// ExportVerifyingKey returns the raw bytes of a verifying key.
153+
func (ks *KeyStore) ExportVerifyingKey(name string) ([]byte, error) {
154+
return os.ReadFile(ks.vkPath(name))
155+
}
156+
157+
// ExportSolidityVerifier generates a Solidity verifier contract for a circuit.
158+
func (ks *KeyStore) ExportSolidityVerifier(name string) ([]byte, error) {
159+
vk := groth16.NewVerifyingKey(ecc.BN254)
160+
f, err := os.Open(ks.vkPath(name))
161+
if err != nil {
162+
return nil, fmt.Errorf("load vk %s: %w", name, err)
163+
}
164+
if _, err := vk.ReadFrom(f); err != nil {
165+
f.Close()
166+
return nil, fmt.Errorf("read vk %s: %w", name, err)
167+
}
168+
f.Close()
169+
170+
var buf bytes.Buffer
171+
if err := vk.ExportSolidity(&buf); err != nil {
172+
return nil, fmt.Errorf("export solidity verifier %s: %w", name, err)
173+
}
174+
return buf.Bytes(), nil
175+
}
176+
177+
// Purge removes all cached keys for a circuit (forces recompilation).
178+
func (ks *KeyStore) Purge(name string) error {
179+
for _, path := range []string{ks.csPath(name), ks.pkPath(name), ks.vkPath(name)} {
180+
os.Remove(path)
181+
}
182+
log.Debug().Str("circuit", name).Msg("Keys purged")
183+
return nil
184+
}
185+
186+
// PurgeAll removes all cached keys.
187+
func (ks *KeyStore) PurgeAll() error {
188+
entries, err := os.ReadDir(ks.dir)
189+
if err != nil {
190+
return err
191+
}
192+
for _, e := range entries {
193+
os.Remove(filepath.Join(ks.dir, e.Name()))
194+
}
195+
log.Debug().Str("dir", ks.dir).Msg("All keys purged")
196+
return nil
197+
}
198+
199+
// RegisterWithKeyStore compiles circuits using the key store for caching.
200+
func RegisterWithKeyStore(p *Prover, ks *KeyStore, circuits map[string]frontend.Circuit) error {
201+
for name, circuit := range circuits {
202+
cc, err := ks.CompileAndSave(p, name, circuit)
203+
if err != nil {
204+
return fmt.Errorf("circuit %s: %w", name, err)
205+
}
206+
p.StoreCircuit(name, cc)
207+
}
208+
return nil
209+
}
210+
211+
// Path helpers
212+
213+
func (ks *KeyStore) csPath(name string) string {
214+
return filepath.Join(ks.dir, name+".cs")
215+
}
216+
217+
func (ks *KeyStore) pkPath(name string) string {
218+
return filepath.Join(ks.dir, name+".pk")
219+
}
220+
221+
func (ks *KeyStore) vkPath(name string) string {
222+
return filepath.Join(ks.dir, name+".vk")
223+
}

prover/service.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,20 +230,48 @@ func (f *ArcnetWitnessFactory) CreateAssignment(circuitName string, witness map[
230230
}
231231

232232
// NewArcnetService creates a new prover service with arcnet's circuits and witness factory.
233-
func NewArcnetService() (*Service, error) {
233+
// If keyDir is non-empty, keys are persisted to disk for fast restarts.
234+
func NewArcnetService(keyDir string) (*Service, *KeyStore, error) {
234235
p := NewProver()
235236

236237
log.Info().Msg("Registering standard circuits...")
237238
start := time.Now()
238239

239-
if err := RegisterStandardCircuits(p); err != nil {
240-
return nil, fmt.Errorf("failed to register circuits: %w", err)
240+
var ks *KeyStore
241+
if keyDir != "" {
242+
var err error
243+
ks, err = NewKeyStore(keyDir)
244+
if err != nil {
245+
return nil, nil, fmt.Errorf("failed to create keystore: %w", err)
246+
}
247+
248+
circuits := standardCircuits()
249+
if err := RegisterWithKeyStore(p, ks, circuits); err != nil {
250+
return nil, nil, fmt.Errorf("failed to register circuits: %w", err)
251+
}
252+
} else {
253+
if err := RegisterStandardCircuits(p); err != nil {
254+
return nil, nil, fmt.Errorf("failed to register circuits: %w", err)
255+
}
241256
}
242257

243258
log.Info().
244259
Dur("elapsed", time.Since(start)).
245260
Int("circuits", len(p.ListCircuits())).
261+
Bool("cached", ks != nil).
246262
Msg("Circuits registered")
247263

248-
return goprover.NewService(p, &ArcnetWitnessFactory{}), nil
264+
return goprover.NewService(p, &ArcnetWitnessFactory{}), ks, nil
265+
}
266+
267+
// standardCircuits returns the circuit definitions (without compiling them).
268+
func standardCircuits() map[string]frontend.Circuit {
269+
return map[string]frontend.Circuit{
270+
"transfer": &TransferCircuit{},
271+
"transferFrom": &TransferFromCircuit{},
272+
"mint": &MintCircuit{},
273+
"burn": &BurnCircuit{},
274+
"approve": &ApproveCircuit{},
275+
"vestClaim": &VestingClaimCircuit{},
276+
}
249277
}

0 commit comments

Comments
 (0)