From a183b12c9135fd3431b67e9782e4910b0ad67088 Mon Sep 17 00:00:00 2001
From: erhant
Date: Sun, 28 Jun 2026 20:35:49 +0300
Subject: [PATCH 01/21] initial commit with alternate provers (via
`circomkit-ffi` repo)
---
.eslintrc.json | 67 -
.github/workflows/publish.yml | 34 -
.github/workflows/tests.yml | 59 -
.gitignore | 123 +-
.vscode/extensions.json | 3 -
.vscode/settings.json | 20 -
Cargo.lock | 4658 +++++++++
Cargo.toml | 81 +
LICENSE | 21 -
README.md | 361 -
circomkit.json | 12 -
crates/circomkit-cli/Cargo.toml | 26 +
crates/circomkit-cli/src/main.rs | 289 +
crates/circomkit-core/Cargo.toml | 24 +
crates/circomkit-core/src/config/circomkit.rs | 202 +
crates/circomkit-core/src/config/circuit.rs | 69 +
crates/circomkit-core/src/config/compiler.rs | 60 +
crates/circomkit-core/src/config/legacy.rs | 245 +
crates/circomkit-core/src/config/mod.rs | 13 +
crates/circomkit-core/src/config/overrides.rs | 34 +
crates/circomkit-core/src/config/prover.rs | 56 +
crates/circomkit-core/src/config/witness.rs | 12 +
crates/circomkit-core/src/enums.rs | 97 +
crates/circomkit-core/src/error.rs | 52 +
.../circomkit-core/src/functions/calldata.rs | 182 +
.../circomkit-core/src/functions/compile.rs | 76 +
.../src/functions/instantiate.rs | 114 +
crates/circomkit-core/src/functions/mod.rs | 11 +
.../circomkit-core/src/functions/symbols.rs | 180 +
crates/circomkit-core/src/lib.rs | 7 +
crates/circomkit-core/src/pathing.rs | 198 +
crates/circomkit-core/src/types/circuit.rs | 160 +
crates/circomkit-core/src/types/mod.rs | 7 +
crates/circomkit-core/src/types/r1cs.rs | 52 +
crates/circomkit-core/src/types/symbols.rs | 12 +
crates/circomkit-core/src/utils/mod.rs | 11 +
crates/circomkit-core/src/utils/primes.rs | 84 +
crates/circomkit-core/src/utils/ptau.rs | 88 +
crates/circomkit-core/src/utils/r1cs.rs | 245 +
crates/circomkit-core/src/utils/witness.rs | 157 +
crates/circomkit-prove/Cargo.toml | 40 +
.../circomkit-prove/src/arkworks/convert.rs | 24 +
crates/circomkit-prove/src/arkworks/core.rs | 68 +
crates/circomkit-prove/src/arkworks/mod.rs | 97 +
crates/circomkit-prove/src/capabilities.rs | 163 +
crates/circomkit-prove/src/error.rs | 48 +
.../src/lambdaworks/convert.rs | 55 +
crates/circomkit-prove/src/lambdaworks/mod.rs | 143 +
crates/circomkit-prove/src/lib.rs | 73 +
crates/circomkit-prove/src/snarkjs/mod.rs | 261 +
crates/circomkit-prove/src/traits.rs | 62 +
crates/circomkit-prove/src/types.rs | 11 +
.../tests/arkworks_integration.rs | 64 +
.../circomkit-prove/tests/backend_matrix.rs | 226 +
.../tests/lambdaworks_integration.rs | 73 +
.../tests/snarkjs_integration.rs | 76 +
crates/circomkit-test/Cargo.toml | 16 +
crates/circomkit-test/src/error.rs | 43 +
crates/circomkit-test/src/lib.rs | 7 +
crates/circomkit-test/src/proof_tester.rs | 80 +
crates/circomkit-test/src/witness_tester.rs | 176 +
crates/circomkit-witness/Cargo.toml | 24 +
crates/circomkit-witness/src/error.rs | 35 +
crates/circomkit-witness/src/factory.rs | 34 +
crates/circomkit-witness/src/lib.rs | 13 +
crates/circomkit-witness/src/traits.rs | 9 +
crates/circomkit-witness/src/wasm.rs | 481 +
.../tests/wasm_integration.rs | 62 +
crates/circomkit/Cargo.toml | 23 +
crates/circomkit/src/circomkit/compile.rs | 100 +
crates/circomkit/src/circomkit/mod.rs | 56 +
crates/circomkit/src/circomkit/prove.rs | 215 +
crates/circomkit/src/circomkit/testing.rs | 46 +
crates/circomkit/src/lib.rs | 22 +
.../tests/circuits/main/arrays_2_3.circom | 5 +
.../tests/circuits/main/errors.circom | 3 +-
.../tests/circuits/main}/multiplier_3.circom | 3 +-
crates/circomkit/tests/e2e/common.rs | 51 +
crates/circomkit/tests/e2e/compile.rs | 151 +
crates/circomkit/tests/e2e/config.rs | 54 +
crates/circomkit/tests/e2e/main.rs | 13 +
crates/circomkit/tests/e2e/prove.rs | 111 +
crates/circomkit/tests/e2e/witness.rs | 174 +
examples/bun-sha256/.gitignore | 122 -
examples/bun-sha256/.vscode/extensions.json | 3 -
examples/bun-sha256/.vscode/settings.json | 3 -
examples/bun-sha256/README.md | 37 -
examples/bun-sha256/bun.lockb | Bin 38543 -> 0 bytes
examples/bun-sha256/circomkit.json | 6 -
examples/bun-sha256/circuits.json | 7 -
.../bun-sha256/circuits/main/sha256_32.circom | 6 -
examples/bun-sha256/circuits/sha256.circom | 42 -
.../bun-sha256/inputs/sha256_32/default.json | 6 -
examples/bun-sha256/package.json | 19 -
examples/bun-sha256/src/index.ts | 37 -
examples/bun-sha256/tests/sha256.test.ts | 54 -
examples/bun-sha256/tsconfig.json | 26 -
hardhat.config.ts | 8 -
jest.config.js | 19 -
package.json | 97 -
pnpm-lock.yaml | 8374 -----------------
schema.json | 451 +
src/cli.ts | 297 -
src/configs/index.ts | 85 -
src/core/index.ts | 536 --
src/core/pathing.ts | 87 -
src/functions/calldata.ts | 122 -
src/functions/circuit.ts | 82 -
src/functions/index.ts | 3 -
src/functions/r1cs.ts | 115 -
src/functions/symbols.ts | 1 -
src/index.ts | 5 -
src/testers/index.ts | 2 -
src/testers/proofTester.ts | 81 -
src/testers/witnessTester.ts | 368 -
src/types/index.ts | 68 -
src/utils/index.ts | 41 -
src/utils/ptau.ts | 51 -
src/utils/teardown.ts | 15 -
tests/circomkit.json | 46 +
tests/circomkit.test.ts | 196 -
tests/circuits.json | 17 -
tests/circuits/custom.circom | 20 +
tests/circuits/custom_templates.circom | 15 -
tests/circuits/fibonacci/recursive.circom | 17 -
.../fibonacci/test/fibo_vanilla.circom | 6 -
tests/circuits/fibonacci/vanilla.circom | 17 -
tests/circuits/main/multiplier_3.circom | 1 -
tests/circuits/main/someArrays_2_3.circom | 6 -
.../main/uses_custom_templates.circom | 7 -
tests/circuits/multiout.circom | 24 -
tests/circuits/tags.circom | 33 +
tests/circuits/test/arrays.circom | 6 -
tests/circuits/test/checkin.circom | 6 -
tests/circuits/test/error_ct.circom | 6 -
tests/circuits/test/fibo_recursive.circom | 6 -
tests/circuits/test/fibo_vanilla.circom | 6 -
tests/circuits/test/multiplier_10.circom | 6 -
tests/circuits/test/multiplier_100.circom | 6 -
tests/circuits/test/multiplier_20.circom | 6 -
tests/circuits/test/multiplier_4.circom | 6 -
tests/circuits/test/multiplier_50.circom | 6 -
tests/common/circuits.ts | 68 -
tests/common/index.ts | 4 -
tests/configs.test.ts | 161 -
tests/data/multiplier_30/groth16_pkey.zkey | Bin 0 -> 25165 bytes
tests/data/multiplier_30/groth16_vkey.json | 94 +
tests/data/multiplier_30/input.json | 3 +
tests/data/multiplier_30/multiplier_30.r1cs | Bin 0 -> 4072 bytes
.../multiplier_30/multiplier_30.r1cs.json | 340 +
tests/data/multiplier_30/multiplier_30.sym | 118 +
tests/data/multiplier_30/multiplier_30.wasm | Bin 0 -> 35985 bytes
tests/data/multiplier_30/witness.wtns | Bin 0 -> 1996 bytes
tests/errors.test.ts | 47 -
tests/hooks/teardown.js | 6 -
tests/inputs/arrays_2_3/default.json | 5 +-
tests/inputs/errors/default.json | 4 +
tests/proofTester.test.ts | 38 -
tests/witnessTester.test.ts | 112 -
tsconfig.json | 21 -
tsconfig.test.json | 9 -
161 files changed, 12107 insertions(+), 12357 deletions(-)
delete mode 100644 .eslintrc.json
delete mode 100644 .github/workflows/publish.yml
delete mode 100644 .github/workflows/tests.yml
delete mode 100644 .vscode/extensions.json
delete mode 100644 .vscode/settings.json
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
delete mode 100644 LICENSE
delete mode 100644 README.md
delete mode 100644 circomkit.json
create mode 100644 crates/circomkit-cli/Cargo.toml
create mode 100644 crates/circomkit-cli/src/main.rs
create mode 100644 crates/circomkit-core/Cargo.toml
create mode 100644 crates/circomkit-core/src/config/circomkit.rs
create mode 100644 crates/circomkit-core/src/config/circuit.rs
create mode 100644 crates/circomkit-core/src/config/compiler.rs
create mode 100644 crates/circomkit-core/src/config/legacy.rs
create mode 100644 crates/circomkit-core/src/config/mod.rs
create mode 100644 crates/circomkit-core/src/config/overrides.rs
create mode 100644 crates/circomkit-core/src/config/prover.rs
create mode 100644 crates/circomkit-core/src/config/witness.rs
create mode 100644 crates/circomkit-core/src/enums.rs
create mode 100644 crates/circomkit-core/src/error.rs
create mode 100644 crates/circomkit-core/src/functions/calldata.rs
create mode 100644 crates/circomkit-core/src/functions/compile.rs
create mode 100644 crates/circomkit-core/src/functions/instantiate.rs
create mode 100644 crates/circomkit-core/src/functions/mod.rs
create mode 100644 crates/circomkit-core/src/functions/symbols.rs
create mode 100644 crates/circomkit-core/src/lib.rs
create mode 100644 crates/circomkit-core/src/pathing.rs
create mode 100644 crates/circomkit-core/src/types/circuit.rs
create mode 100644 crates/circomkit-core/src/types/mod.rs
create mode 100644 crates/circomkit-core/src/types/r1cs.rs
create mode 100644 crates/circomkit-core/src/types/symbols.rs
create mode 100644 crates/circomkit-core/src/utils/mod.rs
create mode 100644 crates/circomkit-core/src/utils/primes.rs
create mode 100644 crates/circomkit-core/src/utils/ptau.rs
create mode 100644 crates/circomkit-core/src/utils/r1cs.rs
create mode 100644 crates/circomkit-core/src/utils/witness.rs
create mode 100644 crates/circomkit-prove/Cargo.toml
create mode 100644 crates/circomkit-prove/src/arkworks/convert.rs
create mode 100644 crates/circomkit-prove/src/arkworks/core.rs
create mode 100644 crates/circomkit-prove/src/arkworks/mod.rs
create mode 100644 crates/circomkit-prove/src/capabilities.rs
create mode 100644 crates/circomkit-prove/src/error.rs
create mode 100644 crates/circomkit-prove/src/lambdaworks/convert.rs
create mode 100644 crates/circomkit-prove/src/lambdaworks/mod.rs
create mode 100644 crates/circomkit-prove/src/lib.rs
create mode 100644 crates/circomkit-prove/src/snarkjs/mod.rs
create mode 100644 crates/circomkit-prove/src/traits.rs
create mode 100644 crates/circomkit-prove/src/types.rs
create mode 100644 crates/circomkit-prove/tests/arkworks_integration.rs
create mode 100644 crates/circomkit-prove/tests/backend_matrix.rs
create mode 100644 crates/circomkit-prove/tests/lambdaworks_integration.rs
create mode 100644 crates/circomkit-prove/tests/snarkjs_integration.rs
create mode 100644 crates/circomkit-test/Cargo.toml
create mode 100644 crates/circomkit-test/src/error.rs
create mode 100644 crates/circomkit-test/src/lib.rs
create mode 100644 crates/circomkit-test/src/proof_tester.rs
create mode 100644 crates/circomkit-test/src/witness_tester.rs
create mode 100644 crates/circomkit-witness/Cargo.toml
create mode 100644 crates/circomkit-witness/src/error.rs
create mode 100644 crates/circomkit-witness/src/factory.rs
create mode 100644 crates/circomkit-witness/src/lib.rs
create mode 100644 crates/circomkit-witness/src/traits.rs
create mode 100644 crates/circomkit-witness/src/wasm.rs
create mode 100644 crates/circomkit-witness/tests/wasm_integration.rs
create mode 100644 crates/circomkit/Cargo.toml
create mode 100644 crates/circomkit/src/circomkit/compile.rs
create mode 100644 crates/circomkit/src/circomkit/mod.rs
create mode 100644 crates/circomkit/src/circomkit/prove.rs
create mode 100644 crates/circomkit/src/circomkit/testing.rs
create mode 100644 crates/circomkit/src/lib.rs
create mode 100644 crates/circomkit/tests/circuits/main/arrays_2_3.circom
rename tests/circuits/test/error_rt.circom => crates/circomkit/tests/circuits/main/errors.circom (79%)
rename {tests/circuits/test => crates/circomkit/tests/circuits/main}/multiplier_3.circom (81%)
create mode 100644 crates/circomkit/tests/e2e/common.rs
create mode 100644 crates/circomkit/tests/e2e/compile.rs
create mode 100644 crates/circomkit/tests/e2e/config.rs
create mode 100644 crates/circomkit/tests/e2e/main.rs
create mode 100644 crates/circomkit/tests/e2e/prove.rs
create mode 100644 crates/circomkit/tests/e2e/witness.rs
delete mode 100644 examples/bun-sha256/.gitignore
delete mode 100644 examples/bun-sha256/.vscode/extensions.json
delete mode 100644 examples/bun-sha256/.vscode/settings.json
delete mode 100644 examples/bun-sha256/README.md
delete mode 100755 examples/bun-sha256/bun.lockb
delete mode 100644 examples/bun-sha256/circomkit.json
delete mode 100644 examples/bun-sha256/circuits.json
delete mode 100644 examples/bun-sha256/circuits/main/sha256_32.circom
delete mode 100644 examples/bun-sha256/circuits/sha256.circom
delete mode 100644 examples/bun-sha256/inputs/sha256_32/default.json
delete mode 100644 examples/bun-sha256/package.json
delete mode 100644 examples/bun-sha256/src/index.ts
delete mode 100644 examples/bun-sha256/tests/sha256.test.ts
delete mode 100644 examples/bun-sha256/tsconfig.json
delete mode 100644 hardhat.config.ts
delete mode 100644 jest.config.js
delete mode 100644 package.json
delete mode 100644 pnpm-lock.yaml
create mode 100644 schema.json
delete mode 100644 src/cli.ts
delete mode 100644 src/configs/index.ts
delete mode 100644 src/core/index.ts
delete mode 100644 src/core/pathing.ts
delete mode 100644 src/functions/calldata.ts
delete mode 100644 src/functions/circuit.ts
delete mode 100644 src/functions/index.ts
delete mode 100644 src/functions/r1cs.ts
delete mode 100644 src/functions/symbols.ts
delete mode 100644 src/index.ts
delete mode 100644 src/testers/index.ts
delete mode 100644 src/testers/proofTester.ts
delete mode 100644 src/testers/witnessTester.ts
delete mode 100644 src/types/index.ts
delete mode 100644 src/utils/index.ts
delete mode 100644 src/utils/ptau.ts
delete mode 100644 src/utils/teardown.ts
create mode 100644 tests/circomkit.json
delete mode 100644 tests/circomkit.test.ts
delete mode 100644 tests/circuits.json
create mode 100644 tests/circuits/custom.circom
delete mode 100644 tests/circuits/custom_templates.circom
delete mode 100644 tests/circuits/fibonacci/recursive.circom
delete mode 100644 tests/circuits/fibonacci/test/fibo_vanilla.circom
delete mode 100644 tests/circuits/fibonacci/vanilla.circom
delete mode 100644 tests/circuits/main/someArrays_2_3.circom
delete mode 100644 tests/circuits/main/uses_custom_templates.circom
delete mode 100644 tests/circuits/multiout.circom
create mode 100644 tests/circuits/tags.circom
delete mode 100644 tests/circuits/test/arrays.circom
delete mode 100644 tests/circuits/test/checkin.circom
delete mode 100644 tests/circuits/test/error_ct.circom
delete mode 100644 tests/circuits/test/fibo_recursive.circom
delete mode 100644 tests/circuits/test/fibo_vanilla.circom
delete mode 100644 tests/circuits/test/multiplier_10.circom
delete mode 100644 tests/circuits/test/multiplier_100.circom
delete mode 100644 tests/circuits/test/multiplier_20.circom
delete mode 100644 tests/circuits/test/multiplier_4.circom
delete mode 100644 tests/circuits/test/multiplier_50.circom
delete mode 100644 tests/common/circuits.ts
delete mode 100644 tests/common/index.ts
delete mode 100644 tests/configs.test.ts
create mode 100644 tests/data/multiplier_30/groth16_pkey.zkey
create mode 100644 tests/data/multiplier_30/groth16_vkey.json
create mode 100644 tests/data/multiplier_30/input.json
create mode 100644 tests/data/multiplier_30/multiplier_30.r1cs
create mode 100644 tests/data/multiplier_30/multiplier_30.r1cs.json
create mode 100644 tests/data/multiplier_30/multiplier_30.sym
create mode 100644 tests/data/multiplier_30/multiplier_30.wasm
create mode 100644 tests/data/multiplier_30/witness.wtns
delete mode 100644 tests/errors.test.ts
delete mode 100644 tests/hooks/teardown.js
create mode 100644 tests/inputs/errors/default.json
delete mode 100644 tests/proofTester.test.ts
delete mode 100644 tests/witnessTester.test.ts
delete mode 100644 tsconfig.json
delete mode 100644 tsconfig.test.json
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 875f9eb..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,67 +0,0 @@
-{
- "extends": ["eslint:recommended", "plugin:node/recommended", "prettier"],
- "plugins": ["node", "prettier"],
- "rules": {
- "prettier/prettier": "error",
- "block-scoped-var": "error",
- "eqeqeq": "error",
- "no-var": "error",
- "prefer-const": "error",
- "eol-last": "error",
- "prefer-arrow-callback": "error",
- "no-trailing-spaces": "error",
- "quotes": ["warn", "single", {"avoidEscape": true}],
- "no-restricted-properties": [
- "error",
- {
- "object": "describe",
- "property": "only"
- },
- {
- "object": "it",
- "property": "only"
- }
- ]
- },
- "ignorePatterns": [
- "build",
- "node_modules",
- "ptau",
- "circuits",
- "inputs",
- "dist",
- ".vscode",
- ".github",
- "jest.config.js",
- "examples"
- ],
- "overrides": [
- {
- "files": ["**/*.ts", "**/*.tsx"],
- "parser": "@typescript-eslint/parser",
- "extends": ["plugin:@typescript-eslint/recommended"],
- "rules": {
- "@typescript-eslint/no-non-null-assertion": "off",
- "@typescript-eslint/no-use-before-define": "off",
- "@typescript-eslint/no-warning-comments": "off",
- "@typescript-eslint/no-empty-function": "off",
- "@typescript-eslint/no-var-requires": "off",
- "@typescript-eslint/explicit-function-return-type": "off",
- "@typescript-eslint/explicit-module-boundary-types": "off",
- "@typescript-eslint/ban-types": "off",
- "@typescript-eslint/camelcase": "off",
- "node/no-missing-import": "off",
- "node/no-empty-function": "off",
- "node/no-unsupported-features/es-syntax": "off",
- "node/no-missing-require": "off",
- "node/shebang": "off",
- "no-dupe-class-members": "off",
- "require-atomic-updates": "off"
- }
- }
- ],
- "parserOptions": {
- "ecmaVersion": "latest",
- "sourceType": "module"
- }
-}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index 49b8ff4..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: npm
-
-on:
- release:
- types: [published]
-
-jobs:
- publish:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v3
- with:
- version: 9
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: 'pnpm'
-
- - name: Install dependencies
- run: pnpm install --no-frozen-lockfile
-
- - name: Build package
- run: pnpm build
-
- - uses: JS-DevTools/npm-publish@v3
- id: publish
- with:
- token: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index eaa2d0c..0000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-name: tests
-
-on:
- push:
- branches:
- - main
- paths:
- # Source files
- - 'src/**'
- - 'tests/**'
- # Configurations
- - 'circomkit.json'
- - 'hardhat.config.ts'
- - 'jest.config.js'
- # workflow itself
- - '.github/workflows/tests.yml'
-
-jobs:
- test:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Install dependencies
- run: |
- sudo apt-get update
- sudo apt-get install --yes \
- build-essential \
- libgmp-dev \
- libsodium-dev \
- nasm \
- nlohmann-json3-dev
-
- - name: Download Circom Binary v2.1.5
- run: |
- wget -qO /home/runner/work/circom https://github.com/iden3/circom/releases/download/v2.1.5/circom-linux-amd64
- chmod +x /home/runner/work/circom
- sudo mv /home/runner/work/circom /bin/circom
-
- - name: Print Circom version
- run: circom --version
-
- - name: Install pnpm
- uses: pnpm/action-setup@v3
- with:
- version: 9
-
- - name: Use Node.js 20
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: 'pnpm'
-
- - name: Install dependencies
- run: pnpm install --no-frozen-lockfile
-
- - name: Run tests
- run: pnpm test
diff --git a/.gitignore b/.gitignore
index 3f8c3b4..7da90dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,118 +1,9 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
+/target
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+# Editor config
+/.vscode/
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
-
-# builds
-build
-dist
-
-# is this still a thing lol
-.DS_Store
-
-# Hardhat files
-/cache
-/artifacts
-
-# test inputs
-input.test.json
+# Generated test artifacts
+/tests/build/
+/tests/circuits/main/
+/tests/ptau/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index dd0e89c..0000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "recommendations": ["iden3.circom", "firsttris.vscode-jest-runner"]
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 69a0202..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- // https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/images/folderIcons.png
- "material-icon-theme.folders.associations": {
- "circuits": "Stack",
- "main": "Controller",
- "inputs": "Json",
- "ptau": "Secure",
- "zkey": "Functions",
- "ejs": "JavaScript",
- "testers": "Test"
- },
- // https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/images/fileIcons.png
- "material-icon-theme.files.associations": {
- "*.circom": "Verilog",
- "*.wtns": "Authors",
- "*.zkey": "Lock",
- "*.r1cs": "Diff",
- "*.sym": "Edge"
- }
-}
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..3029d5d
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,4658 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli 0.31.1",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
+dependencies = [
+ "gimli 0.32.3",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "ar_archive_writer"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
+dependencies = [
+ "object 0.37.3",
+]
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
+[[package]]
+name = "ark-bn254"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bc66f96ebe2a17a499475b4f94791d379817592ef494171586967ffdc6f95db"
+dependencies = [
+ "ark-ec",
+ "ark-ff",
+ "ark-std",
+]
+
+[[package]]
+name = "ark-circom"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7289925883497fad86462c39a34e836baefe372b081381215258887f82736a"
+dependencies = [
+ "ark-bn254",
+ "ark-crypto-primitives",
+ "ark-ec",
+ "ark-ff",
+ "ark-groth16",
+ "ark-poly",
+ "ark-relations",
+ "ark-serialize",
+ "ark-std",
+ "byteorder",
+ "cfg-if",
+ "color-eyre",
+ "fnv",
+ "hex",
+ "num",
+ "num-bigint",
+ "num-traits",
+ "rayon",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "wasmer",
+]
+
+[[package]]
+name = "ark-crypto-primitives"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b3409b1846fe459d19c95df039481575ac6d5842ae63858ad75cc31219bfc1"
+dependencies = [
+ "ahash",
+ "ark-crypto-primitives-macros",
+ "ark-ec",
+ "ark-ff",
+ "ark-r1cs-std",
+ "ark-relations",
+ "ark-serialize",
+ "ark-snark",
+ "ark-std",
+ "blake2",
+ "blake3",
+ "derivative",
+ "digest",
+ "fnv",
+ "merlin",
+ "num-bigint",
+ "rayon",
+ "sha2",
+]
+
+[[package]]
+name = "ark-crypto-primitives-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ark-ec"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8352a2b2aedf6ba2cc38f7520fc51191d518dde96175c729af19f2d059f191c4"
+dependencies = [
+ "ahash",
+ "ark-ff",
+ "ark-poly",
+ "ark-serialize",
+ "ark-std",
+ "educe",
+ "fnv",
+ "hashbrown 0.17.1",
+ "itertools 0.14.0",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "rayon",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af"
+dependencies = [
+ "ark-ff-asm",
+ "ark-ff-macros",
+ "ark-serialize",
+ "ark-std",
+ "digest",
+ "educe",
+ "num-bigint",
+ "num-traits",
+ "rayon",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ark-groth16"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a293328aa422e65527e285614ce5d1dceb0bd7b8b18d18b1b63191ee1f74cb41"
+dependencies = [
+ "ark-crypto-primitives",
+ "ark-ec",
+ "ark-ff",
+ "ark-poly",
+ "ark-relations",
+ "ark-serialize",
+ "ark-snark",
+ "ark-std",
+ "rayon",
+]
+
+[[package]]
+name = "ark-poly"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f55af10b672002b8d953e230282c51206842e20e5791a94432219b4201de5c"
+dependencies = [
+ "ahash",
+ "ark-ff",
+ "ark-serialize",
+ "ark-std",
+ "educe",
+ "fnv",
+ "hashbrown 0.17.1",
+ "rayon",
+]
+
+[[package]]
+name = "ark-r1cs-std"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291f1c6628bfcac79b0dc2adbe401aa9100e2e96daa971645e0b18fc94de9a98"
+dependencies = [
+ "ark-ec",
+ "ark-ff",
+ "ark-relations",
+ "ark-std",
+ "educe",
+ "itertools 0.14.0",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "tracing",
+]
+
+[[package]]
+name = "ark-relations"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4c11c797a64b8a23e22bf4e77bf582ac27bb21395e3183a9a506ba2561e9f9"
+dependencies = [
+ "ark-ff",
+ "ark-poly",
+ "ark-serialize",
+ "ark-std",
+ "foldhash",
+ "indexmap 2.13.1",
+ "rayon",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b"
+dependencies = [
+ "ark-serialize-derive",
+ "ark-std",
+ "digest",
+ "num-bigint",
+ "rayon",
+ "serde_with",
+]
+
+[[package]]
+name = "ark-serialize-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ark-snark"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bdb461d2be9b2bd6f303c79fffc89f5858790a7b4d33257bca3178e2c071fb9"
+dependencies = [
+ "ark-ff",
+ "ark-relations",
+ "ark-serialize",
+ "ark-std",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155"
+dependencies = [
+ "num-traits",
+ "rand",
+ "rayon",
+]
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "backtrace"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
+dependencies = [
+ "addr2line 0.25.1",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object 0.37.3",
+ "rustc-demangle",
+ "windows-link",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bindgen"
+version = "0.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
+dependencies = [
+ "bitflags 2.11.0",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 1.1.0",
+ "shlex",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "blake3"
+version = "1.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.0",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive 0.6.12",
+ "ptr_meta 0.1.4",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b"
+dependencies = [
+ "bytecheck_derive 0.8.2",
+ "ptr_meta 0.3.1",
+ "rancor",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+
+[[package]]
+name = "cc"
+version = "1.2.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "circomkit"
+version = "0.1.0"
+dependencies = [
+ "circomkit-core",
+ "circomkit-prove",
+ "circomkit-test",
+ "circomkit-witness",
+ "log",
+ "num-bigint",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "circomkit-cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "circomkit",
+ "clap",
+ "env_logger",
+ "log",
+ "regex",
+ "serde_json",
+]
+
+[[package]]
+name = "circomkit-core"
+version = "0.1.0"
+dependencies = [
+ "log",
+ "num-bigint",
+ "num-traits",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "ureq",
+]
+
+[[package]]
+name = "circomkit-prove"
+version = "0.1.0"
+dependencies = [
+ "ark-bn254",
+ "ark-circom",
+ "ark-ec",
+ "ark-ff",
+ "ark-groth16",
+ "ark-relations",
+ "ark-serialize",
+ "ark-std",
+ "circomkit-core",
+ "lambdaworks-circom-adapter",
+ "lambdaworks-groth16",
+ "lambdaworks-math",
+ "log",
+ "num-bigint",
+ "rand",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "circomkit-test"
+version = "0.1.0"
+dependencies = [
+ "circomkit-core",
+ "circomkit-prove",
+ "circomkit-witness",
+ "log",
+ "num-bigint",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "circomkit-witness"
+version = "0.1.0"
+dependencies = [
+ "circomkit-core",
+ "log",
+ "num-bigint",
+ "num-traits",
+ "serde_json",
+ "thiserror 2.0.18",
+ "wasmtime",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "color-eyre"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
+dependencies = [
+ "backtrace",
+ "color-spantrace",
+ "eyre",
+ "indenter",
+ "once_cell",
+ "owo-colors",
+ "tracing-error",
+]
+
+[[package]]
+name = "color-spantrace"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
+dependencies = [
+ "once_cell",
+ "owo-colors",
+ "tracing-core",
+ "tracing-error",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "convert_case"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "corosensei"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6886a0c0f263965933c438626e7179139a62b978a33aa18281cbf0cd5a975f34"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "libc",
+ "scopeguard",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "cpp_demangle"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cranelift-assembler-x64"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e4b56ebe316895d3fa37775d0a87b0c889cc933f5c8b253dbcc7c7bcb7fe7e4"
+dependencies = [
+ "cranelift-assembler-x64-meta",
+]
+
+[[package]]
+name = "cranelift-assembler-x64-meta"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95cabbc01dfbd7dcd6c329ca44f0212910309c221797ac736a67a5bc8857fe1b"
+
+[[package]]
+name = "cranelift-bforest"
+version = "0.110.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "305d51c180ebdc46ef61bc60c54ae6512db3bc9a05842a1f1e762e45977019ab"
+dependencies = [
+ "cranelift-entity 0.110.2",
+]
+
+[[package]]
+name = "cranelift-bforest"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ffe46df300a45f1dc6f609dc808ce963f0e3a2e971682c479a2d13e3b9b8ef"
+dependencies = [
+ "cranelift-entity 0.118.0",
+]
+
+[[package]]
+name = "cranelift-bitset"
+version = "0.110.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "690d8ae6c73748e5ce3d8fe59034dceadb8823e6c8994ba324141c5eae909b0e"
+
+[[package]]
+name = "cranelift-bitset"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b265bed7c51e1921fdae6419791d31af77d33662ee56d7b0fa0704dc8d231cab"
+dependencies = [
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "cranelift-codegen"
+version = "0.110.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd7ca95e831c18d1356da783765c344207cbdffea91e13e47fa9327dbb2e0719"
+dependencies = [
+ "bumpalo",
+ "cranelift-bforest 0.110.2",
+ "cranelift-bitset 0.110.3",
+ "cranelift-codegen-meta 0.110.3",
+ "cranelift-codegen-shared 0.110.3",
+ "cranelift-control 0.110.3",
+ "cranelift-entity 0.110.2",
+ "cranelift-isle 0.110.2",
+ "gimli 0.28.1",
+ "hashbrown 0.14.5",
+ "log",
+ "regalloc2 0.9.3",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "target-lexicon 0.12.16",
+]
+
+[[package]]
+name = "cranelift-codegen"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e606230a7e3a6897d603761baee0d19f88d077f17b996bb5089488a29ae96e41"
+dependencies = [
+ "bumpalo",
+ "cranelift-assembler-x64",
+ "cranelift-bforest 0.118.0",
+ "cranelift-bitset 0.118.0",
+ "cranelift-codegen-meta 0.118.0",
+ "cranelift-codegen-shared 0.118.0",
+ "cranelift-control 0.118.0",
+ "cranelift-entity 0.118.0",
+ "cranelift-isle 0.118.0",
+ "gimli 0.31.1",
+ "hashbrown 0.15.5",
+ "log",
+ "pulley-interpreter",
+ "regalloc2 0.11.2",
+ "rustc-hash 2.1.2",
+ "serde",
+ "smallvec",
+ "target-lexicon 0.13.5",
+]
+
+[[package]]
+name = "cranelift-codegen-meta"
+version = "0.110.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0a2d2ab65e6cbf91f81781d8da65ec2005510f18300eff21a99526ed6785863"
+dependencies = [
+ "cranelift-codegen-shared 0.110.3",
+]
+
+[[package]]
+name = "cranelift-codegen-meta"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a63bffafc23bc60969ad528e138788495999d935f0adcfd6543cb151ca8637d"
+dependencies = [
+ "cranelift-assembler-x64",
+ "cranelift-codegen-shared 0.118.0",
+ "pulley-interpreter",
+]
+
+[[package]]
+name = "cranelift-codegen-shared"
+version = "0.110.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efcff860573cf3db9ae98fbd949240d78b319df686cc306872e7fab60e9c84d7"
+
+[[package]]
+name = "cranelift-codegen-shared"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af50281b67324b58e843170a6a5943cf6d387c06f7eeacc9f5696e4ab7ae7d7e"
+
+[[package]]
+name = "cranelift-control"
+version = "0.110.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d70e5b75c2d5541ef80a99966ccd97aaa54d2a6af19ea31759a28538e1685a"
+dependencies = [
+ "arbitrary",
+]
+
+[[package]]
+name = "cranelift-control"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c20c1b38d1abfbcebb0032e497e71156c0e3b8dcb3f0a92b9863b7bcaec290c"
+dependencies = [
+ "arbitrary",
+]
+
+[[package]]
+name = "cranelift-entity"
+version = "0.110.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a48cb0a194c9ba82fec35a1e492055388d89b2e3c03dee9dcf2488892be8004d"
+dependencies = [
+ "cranelift-bitset 0.110.3",
+]
+
+[[package]]
+name = "cranelift-entity"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c2c67d95507c51b4a1ff3f3555fe4bfec36b9e13c1b684ccc602736f5d5f4a2"
+dependencies = [
+ "cranelift-bitset 0.118.0",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "cranelift-frontend"
+version = "0.110.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8327afc6c1c05f4be62fefce5b439fa83521c65363a322e86ea32c85e7ceaf64"
+dependencies = [
+ "cranelift-codegen 0.110.2",
+ "log",
+ "smallvec",
+ "target-lexicon 0.12.16",
+]
+
+[[package]]
+name = "cranelift-frontend"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e002691cc69c38b54fc7ec93e5be5b744f627d027031d991cc845d1d512d0ce"
+dependencies = [
+ "cranelift-codegen 0.118.0",
+ "log",
+ "smallvec",
+ "target-lexicon 0.13.5",
+]
+
+[[package]]
+name = "cranelift-isle"
+version = "0.110.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56b08621c00321efcfa3eee6a3179adc009e21ea8d24ca7adc3c326184bc3f48"
+
+[[package]]
+name = "cranelift-isle"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93588ed1796cbcb0e2ad160403509e2c5d330d80dd6e0014ac6774c7ebac496"
+
+[[package]]
+name = "cranelift-native"
+version = "0.118.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5b09bdd6407bf5d89661b80cf926ce731c9e8cc184bf49102267a2369a8358e"
+dependencies = [
+ "cranelift-codegen 0.118.0",
+ "libc",
+ "target-lexicon 0.13.5",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dashmap"
+version = "6.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "debugid"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
+dependencies = [
+ "uuid",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.117",
+ "unicode-xid",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "directories-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "enum-iterator"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6"
+dependencies = [
+ "enum-iterator-derive",
+]
+
+[[package]]
+name = "enum-iterator-derive"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "enumset"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d"
+dependencies = [
+ "enumset_derive",
+]
+
+[[package]]
+name = "enumset_derive"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "env_filter"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "eyre"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
+dependencies = [
+ "indenter",
+ "once_cell",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fastrand"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f"
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "fxprof-processed-profile"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd"
+dependencies = [
+ "bitflags 2.11.0",
+ "debugid",
+ "fxhash",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+dependencies = [
+ "fallible-iterator",
+ "indexmap 2.13.1",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+dependencies = [
+ "fallible-iterator",
+ "indexmap 2.13.1",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "gimli"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+ "serde",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indenter"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "ittapi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1"
+dependencies = [
+ "anyhow",
+ "ittapi-sys",
+ "log",
+]
+
+[[package]]
+name = "ittapi-sys"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "jiff"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+dependencies = [
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "lambdaworks-circom-adapter"
+version = "0.11.0"
+source = "git+https://github.com/erhant/lambdaworks?branch=erhant%2Ffix-circom-adapter#14f46222a52943dbb98fca8a9713e05718419dcf"
+dependencies = [
+ "lambdaworks-groth16",
+ "lambdaworks-math",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "lambdaworks-crypto"
+version = "0.11.0"
+source = "git+https://github.com/erhant/lambdaworks?branch=erhant%2Ffix-circom-adapter#14f46222a52943dbb98fca8a9713e05718419dcf"
+dependencies = [
+ "lambdaworks-math",
+ "sha2",
+ "sha3",
+]
+
+[[package]]
+name = "lambdaworks-groth16"
+version = "0.11.0"
+source = "git+https://github.com/erhant/lambdaworks?branch=erhant%2Ffix-circom-adapter#14f46222a52943dbb98fca8a9713e05718419dcf"
+dependencies = [
+ "lambdaworks-crypto",
+ "lambdaworks-math",
+ "rand",
+ "rand_chacha",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "lambdaworks-math"
+version = "0.11.0"
+source = "git+https://github.com/erhant/lambdaworks?branch=erhant%2Ffix-circom-adapter#14f46222a52943dbb98fca8a9713e05718419dcf"
+dependencies = [
+ "rayon",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.184"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libunwind"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "mach2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "macho-unwind-info"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149"
+dependencies = [
+ "thiserror 2.0.18",
+ "zerocopy",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memfd"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227"
+dependencies = [
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "memmap2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "merlin"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d"
+dependencies = [
+ "byteorder",
+ "keccak",
+ "rand_core",
+ "zeroize",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "more-asserts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389"
+
+[[package]]
+name = "munge"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c"
+dependencies = [
+ "munge_macro",
+]
+
+[[package]]
+name = "munge_macro"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+ "rand",
+ "serde",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "crc32fast",
+ "flate2",
+ "hashbrown 0.14.5",
+ "indexmap 2.13.1",
+ "memchr",
+ "ruzstd",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "crc32fast",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.1",
+ "memchr",
+]
+
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "owo-colors"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "psm"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
+dependencies = [
+ "ar_archive_writer",
+ "cc",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive 0.1.4",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79"
+dependencies = [
+ "ptr_meta_derive 0.3.1",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "pulley-interpreter"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3325791708ad50580aeacfcce06cb5e462c9ba7a2368e109cb2012b944b70e"
+dependencies = [
+ "cranelift-bitset 0.118.0",
+ "log",
+ "wasmtime-math",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rancor"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee"
+dependencies = [
+ "ptr_meta 0.3.1",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.11.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "regalloc2"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6"
+dependencies = [
+ "hashbrown 0.13.2",
+ "log",
+ "rustc-hash 1.1.0",
+ "slice-group-by",
+ "smallvec",
+]
+
+[[package]]
+name = "regalloc2"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a"
+dependencies = [
+ "allocator-api2",
+ "bumpalo",
+ "hashbrown 0.15.5",
+ "log",
+ "rustc-hash 2.1.2",
+ "smallvec",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "region"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+ "mach2",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rend"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6"
+dependencies = [
+ "bytecheck 0.8.2",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3"
+dependencies = [
+ "bytecheck 0.8.2",
+ "bytes",
+ "hashbrown 0.17.1",
+ "indexmap 2.13.1",
+ "munge",
+ "ptr_meta 0.3.1",
+ "rancor",
+ "rend",
+ "rkyv_derive",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.11.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.11.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ruzstd"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d"
+dependencies = [
+ "byteorder",
+ "derive_more 0.99.20",
+ "twox-hash",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "self_cell"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
+dependencies = [
+ "base64 0.22.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.13.1",
+ "schemars 0.9.0",
+ "schemars 1.2.1",
+ "serde_core",
+ "serde_json",
+ "time",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shared-buffer"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16"
+dependencies = [
+ "bytes",
+ "memmap2",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "slice-group-by"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "sptr"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "target-lexicon"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap 2.13.1",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-error"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
+dependencies = [
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "trait-variant"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "twox-hash"
+version = "1.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
+dependencies = [
+ "cfg-if",
+ "static_assertions",
+]
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
+dependencies = [
+ "base64 0.22.1",
+ "flate2",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-pki-types",
+ "url",
+ "webpki-roots 0.26.11",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.226.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d81b727619aec227dce83e7f7420d4e56c79acd044642a356ea045b98d4e13"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.226.0",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.244.0",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.246.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.246.2",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.13.1",
+ "wasm-encoder 0.244.0",
+ "wasmparser 0.244.0",
+]
+
+[[package]]
+name = "wasmer"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d85671948f8886a1cc946141c0b688a5617603c103699a5fceeebeb4e75b0b6"
+dependencies = [
+ "bindgen",
+ "bytes",
+ "cfg-if",
+ "cmake",
+ "derive_more 2.1.1",
+ "indexmap 2.13.1",
+ "js-sys",
+ "more-asserts",
+ "paste",
+ "rustc-demangle",
+ "serde",
+ "serde-wasm-bindgen",
+ "shared-buffer",
+ "tar",
+ "target-lexicon 0.12.16",
+ "thiserror 1.0.69",
+ "tracing",
+ "wasm-bindgen",
+ "wasmer-compiler",
+ "wasmer-compiler-cranelift",
+ "wasmer-derive",
+ "wasmer-types",
+ "wasmer-vm",
+ "wasmparser 0.224.1",
+ "wat",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wasmer-compiler"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4946475adc0af265af8f10aadf4d4a3c64845bcd3801c655bdd81ce5e3ee869b"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "cfg-if",
+ "enum-iterator",
+ "enumset",
+ "leb128",
+ "libc",
+ "macho-unwind-info",
+ "memmap2",
+ "more-asserts",
+ "object 0.32.2",
+ "region",
+ "rkyv",
+ "self_cell",
+ "shared-buffer",
+ "smallvec",
+ "target-lexicon 0.12.16",
+ "thiserror 1.0.69",
+ "wasmer-types",
+ "wasmer-vm",
+ "wasmparser 0.224.1",
+ "windows-sys 0.59.0",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "wasmer-compiler-cranelift"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780f9c2050941b4e3f6bb82d32bd796a6c1750d2f97cbd892f07b384bb6af6f8"
+dependencies = [
+ "cranelift-codegen 0.110.2",
+ "cranelift-entity 0.110.2",
+ "cranelift-frontend 0.110.2",
+ "gimli 0.28.1",
+ "itertools 0.12.1",
+ "more-asserts",
+ "rayon",
+ "smallvec",
+ "target-lexicon 0.12.16",
+ "tracing",
+ "wasmer-compiler",
+ "wasmer-types",
+]
+
+[[package]]
+name = "wasmer-derive"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c546f3380840cd63fdcc390f04cd19002f2dfa19b4691b77ecbd27642bd93452"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "wasmer-types"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94a4027ce165e8dc776dc5e2a3231a96983e6dc7330efd97b793cfc4e973ad0c"
+dependencies = [
+ "bytecheck 0.6.12",
+ "enum-iterator",
+ "enumset",
+ "getrandom 0.2.17",
+ "hex",
+ "indexmap 2.13.1",
+ "more-asserts",
+ "rkyv",
+ "sha2",
+ "target-lexicon 0.12.16",
+ "thiserror 1.0.69",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "wasmer-vm"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c37d5be291eea00a00d077ce3a427bb3074709ee386ec358f18f0b7da33be01"
+dependencies = [
+ "backtrace",
+ "cc",
+ "cfg-if",
+ "corosensei",
+ "crossbeam-queue",
+ "dashmap",
+ "enum-iterator",
+ "fnv",
+ "indexmap 2.13.1",
+ "libc",
+ "libunwind",
+ "mach2",
+ "memoffset",
+ "more-asserts",
+ "region",
+ "rustversion",
+ "scopeguard",
+ "thiserror 1.0.69",
+ "wasmer-types",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.224.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11"
+dependencies = [
+ "bitflags 2.11.0",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.226.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc28600dcb2ba68d7e5f1c3ba4195c2bddc918c0243fd702d0b6dbd05689b681"
+dependencies = [
+ "bitflags 2.11.0",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.1",
+ "semver",
+ "serde",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.0",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.1",
+ "semver",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.246.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d"
+dependencies = [
+ "bitflags 2.11.0",
+ "indexmap 2.13.1",
+ "semver",
+]
+
+[[package]]
+name = "wasmprinter"
+version = "0.226.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "753a0516fa6c01756ee861f36878dfd9875f273aea9409d9ea390a333c5bcdc2"
+dependencies = [
+ "anyhow",
+ "termcolor",
+ "wasmparser 0.226.0",
+]
+
+[[package]]
+name = "wasmtime"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9fe78033c72da8741e724d763daf1375c93a38bfcea99c873ee4415f6098c3f"
+dependencies = [
+ "addr2line 0.24.2",
+ "anyhow",
+ "async-trait",
+ "bitflags 2.11.0",
+ "bumpalo",
+ "cc",
+ "cfg-if",
+ "encoding_rs",
+ "fxprof-processed-profile",
+ "gimli 0.31.1",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.1",
+ "ittapi",
+ "libc",
+ "log",
+ "mach2",
+ "memfd",
+ "object 0.36.7",
+ "once_cell",
+ "paste",
+ "postcard",
+ "psm",
+ "pulley-interpreter",
+ "rayon",
+ "rustix 0.38.44",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smallvec",
+ "sptr",
+ "target-lexicon 0.13.5",
+ "trait-variant",
+ "wasm-encoder 0.226.0",
+ "wasmparser 0.226.0",
+ "wasmtime-asm-macros",
+ "wasmtime-cache",
+ "wasmtime-component-macro",
+ "wasmtime-component-util",
+ "wasmtime-cranelift",
+ "wasmtime-environ",
+ "wasmtime-fiber",
+ "wasmtime-jit-debug",
+ "wasmtime-jit-icache-coherence",
+ "wasmtime-math",
+ "wasmtime-slab",
+ "wasmtime-versioned-export-macros",
+ "wasmtime-winch",
+ "wat",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wasmtime-asm-macros"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47f3d44ae977d70ccf80938b371d5ec60b6adedf60800b9e8dd1223bb69f4cbc"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "wasmtime-cache"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e209505770c7f38725513dba37246265fa6f724c30969de1e9d2a9e6c8f55099"
+dependencies = [
+ "anyhow",
+ "base64 0.21.7",
+ "directories-next",
+ "log",
+ "postcard",
+ "rustix 0.38.44",
+ "serde",
+ "serde_derive",
+ "sha2",
+ "toml",
+ "windows-sys 0.59.0",
+ "zstd",
+]
+
+[[package]]
+name = "wasmtime-component-macro"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397e68ee29eb072d8d8741c9d2c971a284cd1bc960ebf2c1f6a33ea6ba16d6e1"
+dependencies = [
+ "anyhow",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasmtime-component-util",
+ "wasmtime-wit-bindgen",
+ "wit-parser 0.226.0",
+]
+
+[[package]]
+name = "wasmtime-component-util"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f292ef5eb2cf3d414c2bde59c7fa0feeba799c8db9a8c5a656ad1d1a1d05e10b"
+
+[[package]]
+name = "wasmtime-cranelift"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52fc12eb8ea695a30007a4849a5fd56209dd86a15579e92e0c27c27122818505"
+dependencies = [
+ "anyhow",
+ "cfg-if",
+ "cranelift-codegen 0.118.0",
+ "cranelift-control 0.118.0",
+ "cranelift-entity 0.118.0",
+ "cranelift-frontend 0.118.0",
+ "cranelift-native",
+ "gimli 0.31.1",
+ "itertools 0.12.1",
+ "log",
+ "object 0.36.7",
+ "pulley-interpreter",
+ "smallvec",
+ "target-lexicon 0.13.5",
+ "thiserror 1.0.69",
+ "wasmparser 0.226.0",
+ "wasmtime-environ",
+ "wasmtime-versioned-export-macros",
+]
+
+[[package]]
+name = "wasmtime-environ"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b6b4bf08e371edf262cccb62de10e214bd4aaafaa069f1cd49c9c1c3a5ae8e4"
+dependencies = [
+ "anyhow",
+ "cpp_demangle",
+ "cranelift-bitset 0.118.0",
+ "cranelift-entity 0.118.0",
+ "gimli 0.31.1",
+ "indexmap 2.13.1",
+ "log",
+ "object 0.36.7",
+ "postcard",
+ "rustc-demangle",
+ "semver",
+ "serde",
+ "serde_derive",
+ "smallvec",
+ "target-lexicon 0.13.5",
+ "wasm-encoder 0.226.0",
+ "wasmparser 0.226.0",
+ "wasmprinter",
+ "wasmtime-component-util",
+]
+
+[[package]]
+name = "wasmtime-fiber"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8828d7d8fbe90d087a9edea9223315caf7eb434848896667e5d27889f1173"
+dependencies = [
+ "anyhow",
+ "cc",
+ "cfg-if",
+ "rustix 0.38.44",
+ "wasmtime-asm-macros",
+ "wasmtime-versioned-export-macros",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wasmtime-jit-debug"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab9eff86dedd48b023199de2d266f5d3e37bc7c5bafdc1e3e3057214649ecf5a"
+dependencies = [
+ "cc",
+ "object 0.36.7",
+ "rustix 0.38.44",
+ "wasmtime-versioned-export-macros",
+]
+
+[[package]]
+name = "wasmtime-jit-icache-coherence"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a54f6c6c7e9d7eeee32dfcc10db7f29d505ee7dd28d00593ea241d5f70698e64"
+dependencies = [
+ "anyhow",
+ "cfg-if",
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wasmtime-math"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1108aad2e6965698f9207ea79b80eda2b3dcc57dcb69f4258296d4664ae32cd"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "wasmtime-slab"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d6a321317281b721c5530ef733e8596ecc6065035f286ccd155b3fa8e0ab2f"
+
+[[package]]
+name = "wasmtime-versioned-export-macros"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5732a5c86efce7bca121a61d8c07875f6b85c1607aa86753b40f7f8bd9d3a780"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "wasmtime-winch"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3aa4741ee66a52e2f0ec5f79040017123ba47d2dff9d994b35879cc2b7f468d4"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen 0.118.0",
+ "gimli 0.31.1",
+ "object 0.36.7",
+ "target-lexicon 0.13.5",
+ "wasmparser 0.226.0",
+ "wasmtime-cranelift",
+ "wasmtime-environ",
+ "winch-codegen",
+]
+
+[[package]]
+name = "wasmtime-wit-bindgen"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "505c13fa0cac6c43e805347acf1e916c8de54e3790f2c22873c5692964b09b62"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap 2.13.1",
+ "wit-parser 0.226.0",
+]
+
+[[package]]
+name = "wast"
+version = "246.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe3fe8e3bf88ad96d031b4181ddbd64634b17cb0d06dfc3de589ef43591a9a62"
+dependencies = [
+ "bumpalo",
+ "leb128fmt",
+ "memchr",
+ "unicode-width",
+ "wasm-encoder 0.246.2",
+]
+
+[[package]]
+name = "wat"
+version = "1.246.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c"
+dependencies = [
+ "wast",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.6",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winch-codegen"
+version = "31.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f05457f74ec3c94d5c5caac06b84fd8d9d4d7fa21419189845ed245a53477"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen 0.118.0",
+ "gimli 0.31.1",
+ "regalloc2 0.11.2",
+ "smallvec",
+ "target-lexicon 0.13.5",
+ "thiserror 1.0.69",
+ "wasmparser 0.226.0",
+ "wasmtime-cranelift",
+ "wasmtime-environ",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser 0.244.0",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap 2.13.1",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.0",
+ "indexmap 2.13.1",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder 0.244.0",
+ "wasm-metadata",
+ "wasmparser 0.244.0",
+ "wit-parser 0.244.0",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.226.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33f007722bfd43a2978c5b8b90f02c927dddf0f11c5f5b50929816b3358718cd"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.13.1",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser 0.226.0",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.13.1",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser 0.244.0",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0c168bc
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,81 @@
+[workspace]
+resolver = "3"
+members = [
+ "crates/circomkit-core",
+ "crates/circomkit-witness",
+ "crates/circomkit-prove",
+ "crates/circomkit-test",
+ "crates/circomkit",
+ "crates/circomkit-cli",
+]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2024"
+license = "MIT"
+repository = "https://github.com/erhant/circomkit"
+authors = ["Erhan Tezcan"]
+
+[workspace.dependencies]
+# Internal crates
+circomkit-core = { path = "crates/circomkit-core" }
+circomkit-witness = { path = "crates/circomkit-witness" }
+circomkit-prove = { path = "crates/circomkit-prove" }
+circomkit-test = { path = "crates/circomkit-test" }
+circomkit = { path = "crates/circomkit" }
+
+# Serialization
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+schemars = "0.8"
+
+# Numeric
+num-bigint = { version = "0.4", features = ["serde"] }
+num-traits = "0.2"
+
+# Error handling
+thiserror = "2"
+anyhow = "1"
+
+# Logging
+log = "0.4"
+env_logger = "0.11"
+
+# CLI
+clap = { version = "4", features = ["derive"] }
+
+# HTTP (PTAU download)
+ureq = "2"
+
+# Temp files
+tempfile = "3"
+
+# Regex
+regex = "1"
+
+# WASM runtime (witness calculation)
+wasmtime = "31"
+
+# Lambdaworks (proving)
+lambdaworks-math = { git = "https://github.com/erhant/lambdaworks", branch = "erhant/fix-circom-adapter" }
+lambdaworks-circom-adapter = { git = "https://github.com/erhant/lambdaworks", branch = "erhant/fix-circom-adapter" }
+lambdaworks-groth16 = { git = "https://github.com/erhant/lambdaworks", branch = "erhant/fix-circom-adapter" }
+
+# Arkworks (proving)
+ark-circom = { version = "0.6", default-features = false }
+ark-bn254 = "0.6"
+ark-groth16 = { version = "0.6", default-features = false, features = [
+ "parallel",
+] }
+ark-ff = { version = "0.6", default-features = false, features = [
+ "parallel",
+ "asm",
+] }
+ark-ec = { version = "0.6", default-features = false, features = ["parallel"] }
+ark-relations = { version = "0.6", default-features = false }
+ark-serialize = { version = "0.6", default-features = false }
+ark-std = { version = "0.6", default-features = false, features = [
+ "parallel",
+ "std",
+] }
+rand = "0.8"
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 4d2d5fb..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2023 Erhan Tezcan
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/README.md b/README.md
deleted file mode 100644
index 7db1487..0000000
--- a/README.md
+++ /dev/null
@@ -1,361 +0,0 @@
-
-
- Circomkit
-
- A simple-to-use & opinionated circuit development & testing toolkit.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-- [x] Simple CLI, abstracting away all paths with a simple config.
-- [x] Provides type-safe testing utilities to check for circuit computations & soundness errors, with minimal boilerplate code!
-- [x] Supports all protocols: `groth16`, `plonk`, and `fflonk`.
-- [x] Automatically downloads phase-1 PTAU when using `bn128`.
-- [x] Supports multiple exports such as a Solidity verifier contract and its calldata for some input, or JSON exports for R1CS and the witness file.
-
-## Installation
-
-Circomkit can be installed via:
-
-```sh
-npm install circomkit
-pnpm install circomkit
-yarn add circomkit
-bun add circomkit
-```
-
-You will also need [Circom](https://docs.circom.io), which can be installed following the instructions [here](https://docs.circom.io/getting-started/installation/).
-
-## Usage
-
-You can see available commands with:
-
-```sh
-npx circomkit help
-```
-
-You can check out examples at the [circomkit-examples](https://github.com/erhant/circomkit-examples) repository, or within the [examples](./examples/) directory here.
-
-### Command Line Interface
-
-Actions that require a circuit name can be called as follows:
-
-```sh
-# Compile the circuit
-npx circomkit compile
-
-# Create the main component
-npx circomkit instantiate
-
-# Create a Solidity verifier contract
-npx circomkit contract
-
-# Clear circuit artifacts
-npx circomkit clear
-
-# Circuit-specific setup
-npx circomkit setup [ptau-path]
-
-# Create verification key
-npx circomkit vkey [pkey-path]
-
-# Automatically download PTAU (for BN128)
-npx circomkit ptau
-```
-
-> [!NOTE]
->
-> Circuit-specific setup optionally takes the path to a PTAU file as argument. If not provided, it will automatically decide the PTAU to use with respect to constraint count, and download that for you! This feature only works for `bn128` prime, and has an upper-limit of at most $2^{28}$ constraints.
-
-Some actions such as generating a witness, generating a proof and verifying a proof require JSON inputs to provide the signal values. For that, we specifically create our input files under the `inputs` folder, and under the target circuit name there. For example, an input named `foo` for some circuit named `bar` would be at `inputs/bar/foo.json`.
-
-```sh
-# Generate a witness
-npx circomkit witness
-
-# Generate a proof
-npx circomkit prove
-
-# Verify a proof with public signals
-npx circomkit verify
-
-# Export Solidity calldata to console
-npx circomkit calldata
-```
-
-You can also list the circuit templates and compiled instances with the command:
-
-```sh
-npx circomkit list
-```
-
-### Circomkit Configurations
-
-Everything used by Circomkit can be optionally overridden by providing the selected fields in its constructor. Circomkit CLI does this automatically by checking out [`circomkit.json`](src/configs/index.ts#L56) and overriding the defaults with that. You can print the active configuration via the following command:
-
-```sh
-npx circomkit config
-```
-
-You can edit any of the fields there to fit your needs. Most importantly, you can change the protocol to be `groth16`, `plonk` or `fflonk`; and you can change the underlying prime field to `bn128`, `bls12381` and `goldilocks`.
-
-> [!NOTE]
->
-> Using a prime other than `bn128` makes things a bit harder in circuit-specific setup, as you will have to find the PTAU files yourself, whereas in `bn128` we can use [Perpetual Powers of Tau](https://github.com/privacy-scaling-explorations/perpetualpowersoftau).
-
-
-### Circuit Configurations
-
-A circuit config within `circuits.json` looks like below, where the `key` is the circuit name to be used in commands, and the value is an object that describes the filename, template name, public signals and template parameters:
-
-```js
-sudoku_9x9: {
- file: 'sudoku',
- template: 'Sudoku',
- pubs: ['puzzle'],
- params: [3], // sqrt(9)
-}
-```
-
-> [!TIP]
->
-> The `pubs` and `params` options can be omitted, in which case they will default to `[]`.
-
-### Using Circomkit in Code
-
-All CLI commands other than `init` can be used with the same name and arguments within Circomkit. Furthermore, you can provide configuration & inputs directly, instead of letting Circomkit read from `circuits.json` or from within the `inputs` folder.
-
-```ts
-import {Circomkit} from 'circomkit';
-
-const circomkit = new Circomkit({
- // custom configurations
- protocol: 'plonk',
-});
-
-// artifacts output at `build/multiplier_3` directory
-await circomkit.compile('multiplier_3', {
- file: 'multiplier',
- template: 'Multiplier',
- params: [3],
-});
-
-// proof & public signals at `build/multiplier_3/my_input` directory
-await circomkit.prove('multiplier_3', 'my_input', {in: [3, 5, 7]});
-
-// verify with proof & public signals at `build/multiplier_3/my_input`
-await circomkit.verify('multiplier_3', 'my_input');
-```
-
-## Writing Tests
-
-Circomkit provides two tester utilities that use Chai assertions within, which may be used in a test suite such as Mocha. The key point of these utilities is to help reduce boilerplate code and let you simply worry about the inputs and outputs of a circuit.
-
-### Witness Tester
-
-The Witness tester extends `require('circom_tester').wasm` tool with type-safety and few assertion functions. It provides a very simple interface:
-
-- `expectPass(input)` checks if constraints & assertions are passing for an input
-- `expectPass(input, output)` additionally checks if the outputs are matching
-- `expectFail(input)` checks if any constraints / assertions are failing
-
-See an example below:
-
-```ts
-describe('witness tester', () => {
- // input signals and output signals can be given as type parameters
- // this makes all functions type-safe!
- let circuit: WitnessTester<['in'], ['out']>;
-
- beforeAll(async () => {
- const circomkit = new Circomkit();
- circuit = await circomkit.WitnessTester(CIRCUIT_NAME, CIRCUIT_CONFIG);
- });
-
- it('should pass on correct input & output', async () => {
- await circuit.expectPass(INPUT, OUTPUT);
- });
-
- it('should fail on wrong output', async () => {
- await circuit.expectFail(INPUT, WRONG_OUTPUT);
- });
-
- it('should fail on bad input', async () => {
- await circuit.expectFail(BAD_INPUT);
- });
-});
-```
-
-You can check if the number of constraints are correct using `expectConstraintCount`, as shown below:
-
-```ts
-it('should have correct number of constraints', async () => {
- // expects at least N constraints
- await circuit.expectConstraintCount(N);
- // expects exactly N constraints
- await circuit.expectConstraintCount(N, true);
-});
-```
-
-If you want more control over the output signals, you can use the `compute` function. It takes in an input, and an array of output signal names used in the `main` component so that they can be extracted from the witness.
-
-```ts
-it('should compute correctly', async () => {
- const output = await circuit.compute(INPUT, ['out']);
- expect(output).to.haveOwnProperty('out');
- expect(output.out).to.eq(BigInt(OUTPUT.out));
-});
-```
-
-Finally, you can run tests on the witnesses too. This is most useful when you would like to check for soundness errors.
-
-- `expectConstraintPass(witness)` checks if constraints are passing for a witness
-- `expectConstraintFail(witness)` checks if constraints are failing
-
-You can compute the witness via the `calculateWitness(input)` function. To test for soundness errors, you may edit the witness and see if constraints are failing.
-
->
->
->
->
->
-> Circomkit provides a nice utility for this purpose, called `editWitness(witness, symbols)`. You simply provide a dictionary of symbols to their new values, and it will edit the witness accordingly. See the example below:
->
-> ```ts
-> it('should pass on correct witness', async () => {
-> const witness = await circuit.calculateWitness(INPUT);
-> await circuit.expectConstraintPass(witness);
-> });
->
-> it('should fail on fake witness', async () => {
-> const witness = await circuit.calculateWitness(INPUT);
-> const badWitness = await circuit.editWitness(witness, {
-> 'main.signal': BigInt(1234),
-> 'main.component.signal': BigInt('0xCAFE'),
-> 'main.foo.bar[0]': BigInt('0b0101'),
-> });
-> await circuit.expectConstraintFail(badWitness);
-> });
-> ```
-
-#### Using C Tester (Work in Progress ⌛)
-
-You can make use of the C-tester as well, which performs much better for larger circuits than the WASM alternative.
-
-There may be some prerequisites to compile, and we have an [issue on this](https://github.com/erhant/circomkit/issues/88) right now until we can have a complete setup guide.
-
-### Proof Tester
-
-As an alternative to simulate generating a proof and verifying it, you can use Proof Tester. The proof tester makes use of WASM file, prover key and verifier key in the background. It will use the underlying Circomkit configuration to look for those files, and it can generate them automatically if they do not exist. An example using Plonk protocol is given below. Notice how we create the necessary files before creating the tester, as they are required for proof generation and verification.
-
-```ts
-describe('proof tester', () => {
- // input signals and output signals can be given as type parameters
- // this makes all functions type-safe!
- let circuit: ProofTester<['in']>;
- const protocol = 'plonk';
-
- beforeAll(async () => {
- const circomkit = new Circomkit({protocol});
- circomkit.instantiate(CIRCUIT_NAME, CIRCUIT_CONFIG);
- await circomkit.setup(CIRCUIT_NAME, PTAU_PATH);
- circuit = await circomkit.ProofTester(CIRCUIT_NAME, protocol);
- });
-
- it('should verify a proof correctly', async () => {
- const {proof, publicSignals} = await circuit.prove(INPUT);
- await circuit.expectPass(proof, publicSignals);
- });
-
- it('should NOT verify a proof with invalid public signals', async () => {
- const {proof} = await circuit.prove(INPUT);
- await circuit.expectFail(proof, BAD_PUBLIC_SIGNALS);
- });
-});
-```
-
-### Type-Safety
-
-You may notice that there are optional template parameters in both testers: `WitnessTester` and `ProofTester`. These template parameters take in an array of strings corresponding to signal names. For example, if your circuit has two input signals `in1, in2` and an output `out`, you may instantiate the tester as `WitnessTester<['in1', 'in2'], ['out']>`. In doing so, you will get type-checking on all inputs and outputs required by the tester.
-
-## File Structure
-
-Circomkit with its default configuration follows an _opinionated file structure_, abstracting away the pathing and orientation behind the scenes. All of these can be customized by overriding the respective settings in `circomkit.json`.
-
-An example structure is shown below. Suppose there is a generic circuit for a Sudoku solution knowledge proof written under `circuits` folder. When instantiated, a `main` component for a 9x9 board is created under `circuits/main`. The solution along with it's puzzle is stored as a JSON object under `inputs/sudoku_9x9`. You can see the respective artifacts under `build` directory. In particular, we see `groth16` prefix on some files, indicating that Groth16 protocol was used to create them.
-
-```ml
-circomkit
-├── circuits.json - "circuit configurations"
-├── circomkit.json - "circomkit configurations"
-│
-├── circuits - "circuit codes are here"
-│ ├── main - "main components will be here"
-│ │ └── sudoku_9x9.circom - "auto-generated circuit instance"
-│ └── sudoku.circom - "circuit template"
-│
-├── inputs - "circuit inputs are here"
-│ └── sudoku_9x9 - "folder name is the circuit instance name"
-│ └── my_solution.json - "file name is the input name"
-│
-├── ptau - "PTAU files are here"
-│ └── powersOfTau28_hez_final_08.ptau
-│
-└── build - "build artifacts are stored here"
- └── sudoku_9x9 - "folder name is the circuit instance name"
- ├── sudoku_9x9_js - "WASM outputs"
- │ │── generate_witness.js
- │ │── witness_calculator.js
- │ └── sudoku_9x9.wasm
- │
- ├── my_solution - "folder name is the input name"
- │ │── groth16_proof.json - "proofs are created per protocol"
- │ │── public.json
- │ └── witness.wtns
- │
- ├── sudoku_9x9.r1cs
- ├── sudoku_9x9.sym - "symbol file, used by tests"
- │
- ├── groth16_pkey.zkey - "proving key per protocol"
- ├── groth16_vkey.json - "verification key per protocol"
- └── groth16_verifier.sol - "verifier contract"
-
-```
-
-## Testing
-
-Run all tests via:
-
-```sh
-pnpm test
-```
-
-> [!TIP]
->
-> You can also use the CLI while developing Circomkit locally via `pnpm cli` as if you are using `npx circomkit`. This is useful for hands-on testing stuff.
-
-## Styling
-
-Circomkit uses [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html).
-
-```sh
-# check the formatting
-pnpm format
-
-# lint everything
-pnpm lint
-```
-
-## Acknowledgements
-
-We wholeheartedly thank [BuidlGuild](https://buidlguidl.com/) & [Austin Griffith](https://twitter.com/austingriffith) for providing Circomkit with an [Ecosystem Impact Grant](https://grants.buidlguidl.com/)!
diff --git a/circomkit.json b/circomkit.json
deleted file mode 100644
index 57ae5b6..0000000
--- a/circomkit.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "version": "2.1.4",
- "verbose": true,
- "prime": "bn128",
- "optimization": 1,
- "protocol": "groth16",
- "circuits": "./tests/circuits.json",
- "dirPtau": "./tests/ptau",
- "dirCircuits": "./tests/circuits",
- "dirInputs": "./tests/inputs",
- "dirBuild": "./tests/build"
-}
diff --git a/crates/circomkit-cli/Cargo.toml b/crates/circomkit-cli/Cargo.toml
new file mode 100644
index 0000000..761c31d
--- /dev/null
+++ b/crates/circomkit-cli/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "circomkit-cli"
+description = "CLI for Circomkit"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[[bin]]
+name = "circomkit"
+path = "src/main.rs"
+
+[dependencies]
+circomkit.workspace = true
+clap.workspace = true
+anyhow.workspace = true
+env_logger.workspace = true
+log.workspace = true
+serde_json.workspace = true
+regex.workspace = true
+
+[features]
+default = []
+# Native proving backends (forwarded to circomkit).
+prove-arkworks = ["circomkit/prove-arkworks"]
+prove-lambdaworks = ["circomkit/prove-lambdaworks"]
diff --git a/crates/circomkit-cli/src/main.rs b/crates/circomkit-cli/src/main.rs
new file mode 100644
index 0000000..8b52a1c
--- /dev/null
+++ b/crates/circomkit-cli/src/main.rs
@@ -0,0 +1,289 @@
+use std::path::PathBuf;
+
+use anyhow::{Context, Result};
+use clap::{Parser, Subcommand};
+
+use circomkit::Circomkit;
+
+#[derive(Parser)]
+#[command(
+ name = "circomkit",
+ about = "Circom testing & development toolkit",
+ version
+)]
+struct Cli {
+ /// Path to circomkit.json config file
+ #[arg(long, default_value = "./circomkit.json")]
+ config: PathBuf,
+
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+ /// Print the effective configuration
+ Config,
+
+ /// List all configured circuits
+ List,
+
+ /// Compile circuit(s); supports regex patterns to match multiple circuits
+ Compile {
+ /// Circuit name or regex pattern (e.g. "multiplier.*")
+ #[arg(default_value = ".*")]
+ pattern: String,
+ },
+
+ /// Generate main component file
+ Instantiate {
+ /// Circuit name
+ circuit: String,
+ },
+
+ /// Print circuit info (wires, constraints, etc.)
+ Info {
+ /// Circuit name
+ circuit: String,
+ },
+
+ /// Remove build artifacts for a circuit
+ Clear {
+ /// Circuit name
+ circuit: String,
+ },
+
+ /// Run trusted setup
+ Setup {
+ /// Circuit name
+ circuit: String,
+ /// Path to PTAU file (auto-downloads if omitted)
+ #[arg(long)]
+ ptau: Option,
+ },
+
+ /// Download PTAU file for a circuit
+ Ptau {
+ /// Circuit name
+ circuit: String,
+ },
+
+ /// Export verification key
+ Vkey {
+ /// Circuit name
+ circuit: String,
+ },
+
+ /// Export Solidity verifier contract
+ Contract {
+ /// Circuit name
+ circuit: String,
+ },
+
+ /// Compute witness
+ Witness {
+ /// Circuit name
+ circuit: String,
+ /// Input name (loads from inputs/{circuit}/{input}.json)
+ input: String,
+ },
+
+ /// Generate a proof
+ Prove {
+ /// Circuit name
+ circuit: String,
+ /// Input name
+ input: String,
+ },
+
+ /// Verify a proof
+ Verify {
+ /// Circuit name
+ circuit: String,
+ /// Input name
+ input: String,
+ },
+
+ /// Export calldata for Solidity verifier
+ Calldata {
+ /// Circuit name
+ circuit: String,
+ /// Input name
+ input: String,
+ /// Pretty-print Solidity-compatible output
+ #[arg(long)]
+ pretty: bool,
+ },
+
+ /// Export JSON
+ Json {
+ #[command(subcommand)]
+ target: JsonTarget,
+ },
+}
+
+#[derive(Subcommand)]
+enum JsonTarget {
+ /// Export R1CS as JSON
+ R1cs {
+ /// Circuit name
+ circuit: String,
+ },
+}
+
+fn main() -> Result<()> {
+ env_logger::init();
+ let cli = Cli::parse();
+
+ let ck = Circomkit::from_file(&cli.config)
+ .with_context(|| format!("failed to load config from {}", cli.config.display()))?;
+
+ match cli.command {
+ Commands::Config => {
+ println!("{}", serde_json::to_string_pretty(&ck.config)?);
+ }
+
+ Commands::List => {
+ if ck.config.circuits.is_empty() {
+ println!("No circuits configured.");
+ } else {
+ let mut names: Vec<_> = ck.config.circuits.keys().collect();
+ names.sort();
+ for name in names {
+ let config = &ck.config.circuits[name];
+ let params = if config.params.is_empty() {
+ String::new()
+ } else {
+ format!(
+ "({})",
+ config
+ .params
+ .iter()
+ .map(|p| p.to_string())
+ .collect::>()
+ .join(", ")
+ )
+ };
+ println!(" {name}: {}{params} ({})", config.template, config.file);
+ }
+ }
+ }
+
+ Commands::Compile { pattern } => {
+ let re = regex::Regex::new(&pattern)
+ .with_context(|| format!("invalid regex pattern: {pattern}"))?;
+
+ let mut matched = false;
+ let mut names: Vec<_> = ck.config.circuits.keys().collect();
+ names.sort();
+
+ for name in names {
+ if re.is_match(name) {
+ let out = ck.compile(name)?;
+ println!("compiled {name} to {}", out.display());
+ matched = true;
+ }
+ }
+
+ if !matched {
+ anyhow::bail!("no circuits matched pattern '{pattern}'");
+ }
+ }
+
+ Commands::Instantiate { circuit } => {
+ let path = ck.instantiate(&circuit)?;
+ println!("instantiated at {}", path.display());
+ }
+
+ Commands::Info { circuit } => {
+ let info = ck.info(&circuit)?;
+ println!("Circuit: {circuit}");
+ println!(" Wires: {}", info.wires);
+ println!(" Constraints: {}", info.constraints);
+ println!(" Public inputs: {}", info.public_inputs);
+ println!(" Private inputs: {}", info.private_inputs);
+ println!(" Public outputs: {}", info.public_outputs);
+ println!(" Labels: {}", info.labels);
+ if let Some(prime) = info.prime_name {
+ println!(" Prime: {prime}");
+ }
+ if info.uses_custom_gates {
+ println!(" Custom gates: yes");
+ }
+ }
+
+ Commands::Clear { circuit } => {
+ ck.clear(&circuit)?;
+ println!("cleared {circuit}");
+ }
+
+ Commands::Setup { circuit, ptau } => {
+ let out = ck.setup(&circuit, ptau.as_deref())?;
+ println!("pkey: {}", out.pkey_path.display());
+ println!("vkey: {}", out.vkey_path.display());
+ }
+
+ Commands::Ptau { circuit } => {
+ let path = ck.ptau(&circuit)?;
+ println!("ptau: {}", path.display());
+ }
+
+ Commands::Vkey { circuit } => {
+ let path = ck.vkey(&circuit)?;
+ println!("vkey: {}", path.display());
+ }
+
+ Commands::Contract { circuit } => {
+ let path = ck.contract(&circuit)?;
+ println!("contract: {}", path.display());
+ }
+
+ Commands::Witness { circuit, input } => {
+ let path = ck.witness(&circuit, &input, None)?;
+ println!("witness: {}", path.display());
+ }
+
+ Commands::Prove { circuit, input } => {
+ let path = ck.prove(&circuit, &input, None)?;
+ println!("proof: {}", path.display());
+ }
+
+ Commands::Verify { circuit, input } => {
+ let ok = ck.verify(&circuit, &input)?;
+ if ok {
+ println!("OK! Proof is valid.");
+ } else {
+ println!("INVALID. Proof verification failed.");
+ std::process::exit(1);
+ }
+ }
+
+ Commands::Calldata {
+ circuit,
+ input,
+ pretty,
+ } => {
+ let calldata = ck.calldata(&circuit, &input, pretty)?;
+ println!("{calldata}");
+ }
+
+ Commands::Json { target } => match target {
+ JsonTarget::R1cs { circuit } => {
+ let info = ck.info(&circuit)?;
+ println!(
+ "{}",
+ serde_json::to_string_pretty(&serde_json::json!({
+ "wires": info.wires,
+ "constraints": info.constraints,
+ "publicInputs": info.public_inputs,
+ "privateInputs": info.private_inputs,
+ "publicOutputs": info.public_outputs,
+ "labels": info.labels,
+ }))?
+ );
+ }
+ },
+ }
+
+ Ok(())
+}
diff --git a/crates/circomkit-core/Cargo.toml b/crates/circomkit-core/Cargo.toml
new file mode 100644
index 0000000..0383532
--- /dev/null
+++ b/crates/circomkit-core/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "circomkit-core"
+description = "Core types, config, pathing, parsing, and compilation for Circomkit"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+serde.workspace = true
+serde_json.workspace = true
+schemars.workspace = true
+num-bigint.workspace = true
+num-traits.workspace = true
+thiserror.workspace = true
+log.workspace = true
+
+[features]
+default = ["download"]
+download = ["dep:ureq"]
+
+[dependencies.ureq]
+workspace = true
+optional = true
diff --git a/crates/circomkit-core/src/config/circomkit.rs b/crates/circomkit-core/src/config/circomkit.rs
new file mode 100644
index 0000000..4097ecd
--- /dev/null
+++ b/crates/circomkit-core/src/config/circomkit.rs
@@ -0,0 +1,202 @@
+use std::collections::HashMap;
+use std::path::Path;
+
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use super::circuit::CircuitConfig;
+use super::compiler::CompilerConfig;
+use super::prover::ProverConfig;
+use super::witness::WitnessConfig;
+use crate::enums::{LogLevel, Protocol};
+use crate::error::{CoreError, Result};
+
+/// Top-level circomkit configuration.
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(default, rename_all = "camelCase")]
+pub struct CircomkitConfig {
+ /// Prover settings.
+ pub prover: ProverConfig,
+ /// Compiler settings.
+ pub compiler: CompilerConfig,
+ /// Witness generation settings.
+ pub witness: WitnessConfig,
+ /// Circuit definitions, keyed by circuit name.
+ pub circuits: HashMap,
+ /// Circom version for pragma.
+ pub version: String,
+ /// Log level.
+ pub log_level: LogLevel,
+}
+
+impl Default for CircomkitConfig {
+ fn default() -> Self {
+ Self {
+ prover: ProverConfig::default(),
+ compiler: CompilerConfig::default(),
+ witness: WitnessConfig::default(),
+ circuits: HashMap::new(),
+ version: "2.1.0".to_string(),
+ log_level: LogLevel::default(),
+ }
+ }
+}
+
+/// A circuit config with global settings resolved (overrides merged).
+#[derive(Debug, Clone)]
+pub struct ResolvedCircuitConfig {
+ pub circuit: CircuitConfig,
+ pub prover: ProverConfig,
+ pub compiler: CompilerConfig,
+ pub version: String,
+}
+
+impl CircomkitConfig {
+ /// Load config from a JSON file, falling back to defaults for missing fields.
+ ///
+ /// Automatically detects the legacy Circomkit v0.3 flat config format
+ /// and converts it to the new nested format.
+ pub fn from_file(path: &Path) -> Result {
+ if !path.exists() {
+ return Ok(Self::default());
+ }
+ let contents = std::fs::read_to_string(path)?;
+ let value: serde_json::Value = serde_json::from_str(&contents)?;
+
+ let config = if super::legacy::is_legacy_format(&value) {
+ log::info!("detected legacy Circomkit v0.3 config format, converting...");
+ let config_dir = path.parent().unwrap_or(Path::new("."));
+ super::legacy::from_legacy(&value, config_dir)?
+ } else {
+ serde_json::from_value(value)?
+ };
+
+ config.validate()?;
+ Ok(config)
+ }
+
+ /// Validate the configuration.
+ pub fn validate(&self) -> Result<()> {
+ if self.prover.protocol == Protocol::Plonk && self.compiler.optimization != 1 {
+ return Err(CoreError::PlonkOptimization);
+ }
+ Ok(())
+ }
+
+ /// Resolve a circuit config by name, merging per-circuit overrides with global settings.
+ pub fn resolve_circuit(&self, name: &str) -> Result {
+ let circuit = self
+ .circuits
+ .get(name)
+ .ok_or_else(|| CoreError::CircuitNotFound(name.to_string()))?;
+
+ let mut prover = self.prover.clone();
+ let mut compiler = self.compiler.clone();
+ let mut version = self.version.clone();
+
+ if let Some(overrides) = &circuit.overrides {
+ if let Some(p) = &overrides.prover {
+ p.merge_into(&mut prover);
+ }
+ if let Some(c) = &overrides.compiler {
+ c.merge_into(&mut compiler);
+ }
+ if let Some(v) = &overrides.version {
+ version = v.clone();
+ }
+ }
+
+ Ok(ResolvedCircuitConfig {
+ circuit: circuit.clone(),
+ prover,
+ compiler,
+ version,
+ })
+ }
+
+ /// Generate the JSON Schema for this config.
+ pub fn json_schema() -> schemars::schema::RootSchema {
+ schemars::schema_for!(CircomkitConfig)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::config::circuit::{CircuitOverrides, PartialCompilerConfig};
+ use crate::enums::Prime;
+
+ #[test]
+ fn default_config_is_valid() {
+ let config = CircomkitConfig::default();
+ assert!(config.validate().is_ok());
+ }
+
+ #[test]
+ fn plonk_requires_opt_1() {
+ let mut config = CircomkitConfig::default();
+ config.prover.protocol = Protocol::Plonk;
+ config.compiler.optimization = 0;
+ assert!(config.validate().is_err());
+ }
+
+ #[test]
+ fn circuit_overrides_merge() {
+ let mut config = CircomkitConfig::default();
+ config.circuits.insert(
+ "test".to_string(),
+ CircuitConfig {
+ file: "test".to_string(),
+ template: "Test".to_string(),
+ pubs: vec![],
+ params: vec![],
+ uses_custom_templates: false,
+ overrides: Some(CircuitOverrides {
+ compiler: Some(PartialCompilerConfig {
+ optimization: Some(2),
+ ..Default::default()
+ }),
+ ..Default::default()
+ }),
+ },
+ );
+
+ let resolved = config.resolve_circuit("test").unwrap();
+ assert_eq!(resolved.compiler.optimization, 2);
+ assert_eq!(resolved.compiler.prime, Prime::Bn128);
+ }
+
+ #[test]
+ fn circuit_not_found() {
+ let config = CircomkitConfig::default();
+ assert!(config.resolve_circuit("nonexistent").is_err());
+ }
+
+ #[test]
+ fn roundtrip_json() {
+ let config = CircomkitConfig::default();
+ let json = serde_json::to_string_pretty(&config).unwrap();
+ let parsed: CircomkitConfig = serde_json::from_str(&json).unwrap();
+ assert_eq!(parsed.version, config.version);
+ assert_eq!(parsed.prover.protocol, config.prover.protocol);
+ }
+
+ #[test]
+ fn json_schema_generates() {
+ let schema = CircomkitConfig::json_schema();
+ let json = serde_json::to_string_pretty(&schema).unwrap();
+ assert!(json.contains("CircomkitConfig"));
+ }
+
+ #[test]
+ #[ignore = "run manually to generate schema: cargo test -p circomkit-core generate_schema -- --ignored"]
+ fn generate_schema() {
+ let schema = CircomkitConfig::json_schema();
+ let json = serde_json::to_string_pretty(&schema).unwrap();
+ let out_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../..")
+ .join("schema.json");
+ std::fs::write(&out_path, &json).unwrap();
+ println!("schema written to {}", out_path.display());
+ }
+}
diff --git a/crates/circomkit-core/src/config/circuit.rs b/crates/circomkit-core/src/config/circuit.rs
new file mode 100644
index 0000000..76fc5c2
--- /dev/null
+++ b/crates/circomkit-core/src/config/circuit.rs
@@ -0,0 +1,69 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use super::compiler::CompilerConfig;
+use super::overrides::partial_config;
+use super::prover::ProverConfig;
+use crate::enums::{Prime, Protocol, ProvingBackendKind};
+
+/// Per-circuit configuration.
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CircuitConfig {
+ /// Path to the circuit template file (relative to `src_dir`, without extension).
+ pub file: String,
+ /// Name of the circom template to instantiate.
+ pub template: String,
+ /// Public input signal names.
+ #[serde(default)]
+ pub pubs: Vec,
+ /// Template parameters.
+ #[serde(default)]
+ pub params: Vec,
+ /// Whether the circuit uses custom templates (custom pragma).
+ #[serde(default)]
+ pub uses_custom_templates: bool,
+ /// Per-circuit config overrides.
+ #[serde(default)]
+ pub overrides: Option,
+}
+
+/// Per-circuit overrides that merge on top of global config.
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(default, rename_all = "camelCase")]
+pub struct CircuitOverrides {
+ pub prover: Option,
+ pub compiler: Option,
+ pub version: Option,
+}
+
+partial_config! {
+ #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
+ #[serde(default, rename_all = "camelCase")]
+ pub struct PartialProverConfig merges into ProverConfig {
+ pub protocol: Protocol,
+ pub backend: ProvingBackendKind,
+ pub verbose: bool,
+ pub ptau_dir: std::path::PathBuf,
+ pub input_dir: std::path::PathBuf
+ }
+}
+
+partial_config! {
+ #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
+ #[serde(default, rename_all = "camelCase")]
+ pub struct PartialCompilerConfig merges into CompilerConfig {
+ pub prime: Prime,
+ pub src_dir: std::path::PathBuf,
+ pub out_dir: std::path::PathBuf,
+ pub optimization: u32,
+ pub verbose: bool,
+ pub wasm: bool,
+ pub sym: bool,
+ pub r1cs: bool,
+ pub c: bool,
+ pub inspect: bool,
+ pub circom_path: String,
+ pub recompile: bool
+ }
+}
diff --git a/crates/circomkit-core/src/config/compiler.rs b/crates/circomkit-core/src/config/compiler.rs
new file mode 100644
index 0000000..9824926
--- /dev/null
+++ b/crates/circomkit-core/src/config/compiler.rs
@@ -0,0 +1,60 @@
+use std::path::PathBuf;
+
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::enums::Prime;
+
+/// Compiler configuration.
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(default, rename_all = "camelCase")]
+pub struct CompilerConfig {
+ /// Prime field for circuit compilation.
+ pub prime: Prime,
+ /// Source directory for circuit templates.
+ pub src_dir: PathBuf,
+ /// Output directory for build artifacts.
+ pub out_dir: PathBuf,
+ /// Include paths for circom (e.g. `node_modules`).
+ pub include: Vec,
+ /// Optimization level (0, 1, 2, or higher for O2round).
+ pub optimization: u32,
+ /// Whether circom should output verbose logs.
+ pub verbose: bool,
+ /// Generate WASM witness calculator.
+ pub wasm: bool,
+ /// Generate symbol file.
+ pub sym: bool,
+ /// Generate R1CS file.
+ pub r1cs: bool,
+ /// Generate C witness calculator.
+ pub c: bool,
+ /// Run constraint inspection during compilation.
+ pub inspect: bool,
+ /// Path to the circom binary.
+ pub circom_path: String,
+ /// Force recompilation even if build artifacts are up-to-date.
+ /// When `false` (default), compilation is skipped if the R1CS file exists
+ /// and is newer than the source `.circom` file.
+ pub recompile: bool,
+}
+
+impl Default for CompilerConfig {
+ fn default() -> Self {
+ Self {
+ prime: Prime::Bn128,
+ src_dir: PathBuf::from("./circuits"),
+ out_dir: PathBuf::from("./build"),
+ include: vec![PathBuf::from("./node_modules")],
+ optimization: 1,
+ verbose: true,
+ wasm: true,
+ sym: true,
+ r1cs: true,
+ c: false,
+ inspect: true,
+ circom_path: "circom".to_string(),
+ recompile: false,
+ }
+ }
+}
diff --git a/crates/circomkit-core/src/config/legacy.rs b/crates/circomkit-core/src/config/legacy.rs
new file mode 100644
index 0000000..0af1661
--- /dev/null
+++ b/crates/circomkit-core/src/config/legacy.rs
@@ -0,0 +1,245 @@
+//! Backwards compatibility for the original Circomkit v0.3 config format.
+//!
+//! The old format is a flat JSON with fields like `dirCircuits`, `dirBuild`,
+//! `groth16numContributions`, etc., and a `circuits` field pointing to a
+//! separate JSON file containing the circuit definitions.
+
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+
+use serde::Deserialize;
+
+use super::circomkit::CircomkitConfig;
+use super::circuit::CircuitConfig;
+use super::compiler::CompilerConfig;
+use super::prover::{Groth16Options, ProverConfig};
+use super::witness::WitnessConfig;
+use crate::enums::{LogLevel, Prime, Protocol, ProvingBackendKind, WitnessBackend};
+use crate::error::Result;
+
+/// The original Circomkit v0.3 flat config format.
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct LegacyConfig {
+ #[serde(default = "default_protocol")]
+ protocol: Protocol,
+ #[serde(default = "default_prime")]
+ prime: Prime,
+ #[serde(default = "default_version")]
+ version: String,
+
+ /// Path to a separate circuits.json file.
+ #[serde(default = "default_circuits_path")]
+ circuits: String,
+
+ #[serde(default = "default_dir_circuits")]
+ dir_circuits: PathBuf,
+ #[serde(default = "default_dir_inputs")]
+ dir_inputs: PathBuf,
+ #[serde(default = "default_dir_ptau")]
+ dir_ptau: PathBuf,
+ #[serde(default = "default_dir_build")]
+ dir_build: PathBuf,
+ #[serde(default = "default_circom_path")]
+ circom_path: String,
+
+ #[serde(default)]
+ optimization: Option,
+ #[serde(default = "default_true")]
+ inspect: bool,
+ #[serde(default = "default_include")]
+ include: Vec,
+ #[serde(default = "default_true")]
+ verbose: bool,
+ #[serde(default = "default_log_level")]
+ log_level: LogLevel,
+
+ #[serde(default)]
+ c_witness: bool,
+ #[serde(default = "default_true")]
+ wasm_witness: bool,
+
+ #[serde(default = "default_one")]
+ groth16num_contributions: u32,
+ #[serde(default)]
+ groth16ask_for_entropy: bool,
+}
+
+fn default_protocol() -> Protocol {
+ Protocol::Groth16
+}
+fn default_prime() -> Prime {
+ Prime::Bn128
+}
+fn default_version() -> String {
+ "2.1.0".to_string()
+}
+fn default_circuits_path() -> String {
+ "./circuits.json".to_string()
+}
+fn default_dir_circuits() -> PathBuf {
+ PathBuf::from("./circuits")
+}
+fn default_dir_inputs() -> PathBuf {
+ PathBuf::from("./inputs")
+}
+fn default_dir_ptau() -> PathBuf {
+ PathBuf::from("./ptau")
+}
+fn default_dir_build() -> PathBuf {
+ PathBuf::from("./build")
+}
+fn default_circom_path() -> String {
+ "circom".to_string()
+}
+fn default_include() -> Vec {
+ vec![PathBuf::from("./node_modules")]
+}
+fn default_log_level() -> LogLevel {
+ LogLevel::Info
+}
+fn default_true() -> bool {
+ true
+}
+fn default_one() -> u32 {
+ 1
+}
+
+/// Detect whether a JSON value is in legacy format.
+///
+/// Legacy configs have top-level `dirCircuits` or `dirBuild` fields.
+/// New configs have a nested `compiler` object.
+pub fn is_legacy_format(value: &serde_json::Value) -> bool {
+ value.get("dirCircuits").is_some()
+ || value.get("dirBuild").is_some()
+ || value.get("dirPtau").is_some()
+ || value.get("dirInputs").is_some()
+ || (value.get("circuits").is_some_and(|v| v.is_string()))
+}
+
+/// Convert a legacy config JSON into a new `CircomkitConfig`.
+///
+/// If the legacy config has a `circuits` field pointing to a file path,
+/// the circuits are loaded from that file (resolved relative to `config_dir`).
+pub fn from_legacy(value: &serde_json::Value, config_dir: &Path) -> Result {
+ let legacy: LegacyConfig = serde_json::from_value(value.clone())?;
+
+ // Load circuits from the external file
+ let circuits = load_legacy_circuits(&legacy.circuits, config_dir)?;
+
+ let optimization = legacy.optimization.unwrap_or(1);
+
+ Ok(CircomkitConfig {
+ prover: ProverConfig {
+ protocol: legacy.protocol,
+ backend: ProvingBackendKind::Snarkjs,
+ verbose: legacy.verbose,
+ ptau_dir: legacy.dir_ptau,
+ input_dir: legacy.dir_inputs,
+ groth16: Groth16Options {
+ num_contributions: legacy.groth16num_contributions,
+ ask_for_entropy: legacy.groth16ask_for_entropy,
+ },
+ },
+ compiler: CompilerConfig {
+ prime: legacy.prime,
+ src_dir: legacy.dir_circuits,
+ out_dir: legacy.dir_build,
+ include: legacy.include,
+ optimization,
+ verbose: legacy.verbose,
+ wasm: legacy.wasm_witness,
+ sym: true,
+ r1cs: true,
+ c: legacy.c_witness,
+ inspect: legacy.inspect,
+ circom_path: legacy.circom_path,
+ recompile: false,
+ },
+ witness: WitnessConfig {
+ calculator: if legacy.wasm_witness {
+ WitnessBackend::Wasm
+ } else {
+ WitnessBackend::C
+ },
+ },
+ circuits,
+ version: legacy.version,
+ log_level: legacy.log_level,
+ })
+}
+
+/// Load circuit configs from a separate JSON file (the v0.3 pattern).
+fn load_legacy_circuits(
+ circuits_path: &str,
+ config_dir: &Path,
+) -> Result> {
+ let resolved = config_dir.join(circuits_path);
+ if !resolved.exists() {
+ log::warn!("legacy circuits file not found: {}", resolved.display());
+ return Ok(HashMap::new());
+ }
+
+ let contents = std::fs::read_to_string(&resolved)?;
+ let circuits: HashMap = serde_json::from_str(&contents)?;
+ Ok(circuits)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn detect_legacy_format() {
+ let legacy = serde_json::json!({
+ "protocol": "groth16",
+ "prime": "bn128",
+ "dirCircuits": "./circuits",
+ "dirBuild": "./build",
+ "circuits": "./circuits.json"
+ });
+ assert!(is_legacy_format(&legacy));
+
+ let new = serde_json::json!({
+ "compiler": { "prime": "bn128" },
+ "prover": { "protocol": "groth16" },
+ "circuits": { "mul": { "file": "mul", "template": "Mul" } }
+ });
+ assert!(!is_legacy_format(&new));
+ }
+
+ #[test]
+ fn convert_legacy_config() {
+ let legacy = serde_json::json!({
+ "protocol": "groth16",
+ "prime": "bn128",
+ "version": "2.1.4",
+ "verbose": false,
+ "optimization": 2,
+ "dirCircuits": "./src/circuits",
+ "dirInputs": "./src/inputs",
+ "dirPtau": "./ptau",
+ "dirBuild": "./out",
+ "circuits": "./circuits.json",
+ "groth16numContributions": 3,
+ "groth16askForEntropy": true,
+ "cWitness": true,
+ "wasmWitness": false
+ });
+
+ // Use a temp dir so we don't need an actual circuits.json
+ let config = from_legacy(&legacy, Path::new("/nonexistent")).unwrap();
+
+ assert_eq!(config.prover.protocol, Protocol::Groth16);
+ assert_eq!(config.compiler.prime, Prime::Bn128);
+ assert_eq!(config.compiler.optimization, 2);
+ assert_eq!(config.compiler.src_dir, PathBuf::from("./src/circuits"));
+ assert_eq!(config.compiler.out_dir, PathBuf::from("./out"));
+ assert_eq!(config.prover.ptau_dir, PathBuf::from("./ptau"));
+ assert_eq!(config.prover.groth16.num_contributions, 3);
+ assert!(config.prover.groth16.ask_for_entropy);
+ assert!(config.compiler.c);
+ assert!(!config.compiler.wasm);
+ assert_eq!(config.version, "2.1.4");
+ }
+}
diff --git a/crates/circomkit-core/src/config/mod.rs b/crates/circomkit-core/src/config/mod.rs
new file mode 100644
index 0000000..b145819
--- /dev/null
+++ b/crates/circomkit-core/src/config/mod.rs
@@ -0,0 +1,13 @@
+mod circomkit;
+mod circuit;
+mod compiler;
+pub mod legacy;
+mod overrides;
+mod prover;
+mod witness;
+
+pub use circomkit::{CircomkitConfig, ResolvedCircuitConfig};
+pub use circuit::{CircuitConfig, CircuitOverrides, PartialCompilerConfig, PartialProverConfig};
+pub use compiler::CompilerConfig;
+pub use prover::{Groth16Options, ProverConfig};
+pub use witness::WitnessConfig;
diff --git a/crates/circomkit-core/src/config/overrides.rs b/crates/circomkit-core/src/config/overrides.rs
new file mode 100644
index 0000000..2843711
--- /dev/null
+++ b/crates/circomkit-core/src/config/overrides.rs
@@ -0,0 +1,34 @@
+/// Macro to generate partial config structs where every field is `Option`.
+/// Used for per-circuit config overrides.
+macro_rules! partial_config {
+ (
+ $(#[$meta:meta])*
+ pub struct $name:ident merges into $base:ty {
+ $(
+ $(#[$field_meta:meta])*
+ pub $field:ident : $ty:ty
+ ),* $(,)?
+ }
+ ) => {
+ $(#[$meta])*
+ pub struct $name {
+ $(
+ $(#[$field_meta])*
+ pub $field: Option<$ty>,
+ )*
+ }
+
+ impl $name {
+ /// Merge non-None fields into the base config.
+ pub fn merge_into(&self, base: &mut $base) {
+ $(
+ if let Some(v) = &self.$field {
+ base.$field = v.clone();
+ }
+ )*
+ }
+ }
+ };
+}
+
+pub(crate) use partial_config;
diff --git a/crates/circomkit-core/src/config/prover.rs b/crates/circomkit-core/src/config/prover.rs
new file mode 100644
index 0000000..c6b7879
--- /dev/null
+++ b/crates/circomkit-core/src/config/prover.rs
@@ -0,0 +1,56 @@
+use std::path::PathBuf;
+
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::enums::{Protocol, ProvingBackendKind};
+
+/// Groth16-specific prover options.
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(default, rename_all = "camelCase")]
+pub struct Groth16Options {
+ /// Number of phase-2 contributions.
+ pub num_contributions: u32,
+ /// Whether to prompt for entropy during contributions.
+ pub ask_for_entropy: bool,
+}
+
+impl Default for Groth16Options {
+ fn default() -> Self {
+ Self {
+ num_contributions: 1,
+ ask_for_entropy: false,
+ }
+ }
+}
+
+/// Prover configuration.
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(default, rename_all = "camelCase")]
+pub struct ProverConfig {
+ /// Proof protocol to use.
+ pub protocol: Protocol,
+ /// Proving backend.
+ pub backend: ProvingBackendKind,
+ /// Whether snarkjs should log verbosely.
+ pub verbose: bool,
+ /// Directory for Powers of Tau files.
+ pub ptau_dir: PathBuf,
+ /// Directory for circuit input JSON files.
+ pub input_dir: PathBuf,
+ /// Groth16-specific options.
+ pub groth16: Groth16Options,
+}
+
+impl Default for ProverConfig {
+ fn default() -> Self {
+ Self {
+ protocol: Protocol::Groth16,
+ backend: ProvingBackendKind::default(),
+ verbose: true,
+ ptau_dir: PathBuf::from("./ptau"),
+ input_dir: PathBuf::from("./inputs"),
+ groth16: Groth16Options::default(),
+ }
+ }
+}
diff --git a/crates/circomkit-core/src/config/witness.rs b/crates/circomkit-core/src/config/witness.rs
new file mode 100644
index 0000000..4de58f1
--- /dev/null
+++ b/crates/circomkit-core/src/config/witness.rs
@@ -0,0 +1,12 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::enums::WitnessBackend;
+
+/// Witness generation configuration.
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(default, rename_all = "camelCase")]
+pub struct WitnessConfig {
+ /// Which witness calculator backend to use.
+ pub calculator: WitnessBackend,
+}
diff --git a/crates/circomkit-core/src/enums.rs b/crates/circomkit-core/src/enums.rs
new file mode 100644
index 0000000..b530b00
--- /dev/null
+++ b/crates/circomkit-core/src/enums.rs
@@ -0,0 +1,97 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+/// Zero-knowledge proof protocol.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum Protocol {
+ Groth16,
+ Plonk,
+ Fflonk,
+}
+
+impl fmt::Display for Protocol {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Groth16 => write!(f, "groth16"),
+ Self::Plonk => write!(f, "plonk"),
+ Self::Fflonk => write!(f, "fflonk"),
+ }
+ }
+}
+
+/// Finite field / elliptic curve prime.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum Prime {
+ Bn128,
+ Bls12381,
+ Goldilocks,
+ Grumpkin,
+ Pallas,
+ Vesta,
+ Secq256r1,
+}
+
+impl fmt::Display for Prime {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Bn128 => write!(f, "bn128"),
+ Self::Bls12381 => write!(f, "bls12381"),
+ Self::Goldilocks => write!(f, "goldilocks"),
+ Self::Grumpkin => write!(f, "grumpkin"),
+ Self::Pallas => write!(f, "pallas"),
+ Self::Vesta => write!(f, "vesta"),
+ Self::Secq256r1 => write!(f, "secq256r1"),
+ }
+ }
+}
+
+/// Witness calculator backend.
+/// TODO: should be at circomkit-witness?
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum WitnessBackend {
+ #[default]
+ Wasm,
+ C,
+}
+
+/// Proving backend kind.
+/// TODO: should be at circomkit-prove?
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum ProvingBackendKind {
+ #[default]
+ Snarkjs,
+ Arkworks,
+ Lambdaworks,
+}
+
+/// Log level for circomkit operations.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum LogLevel {
+ Trace,
+ Debug,
+ #[default]
+ Info,
+ Warn,
+ Error,
+ Silent,
+}
+
+impl LogLevel {
+ /// Convert to `log::LevelFilter`.
+ pub fn to_level_filter(self) -> log::LevelFilter {
+ match self {
+ Self::Trace => log::LevelFilter::Trace,
+ Self::Debug => log::LevelFilter::Debug,
+ Self::Info => log::LevelFilter::Info,
+ Self::Warn => log::LevelFilter::Warn,
+ Self::Error => log::LevelFilter::Error,
+ Self::Silent => log::LevelFilter::Off,
+ }
+ }
+}
diff --git a/crates/circomkit-core/src/error.rs b/crates/circomkit-core/src/error.rs
new file mode 100644
index 0000000..1efe7ba
--- /dev/null
+++ b/crates/circomkit-core/src/error.rs
@@ -0,0 +1,52 @@
+use std::path::PathBuf;
+
+/// Core error type for circomkit-core operations.
+#[derive(Debug, thiserror::Error)]
+pub enum CoreError {
+ #[error("circuit '{0}' not found in config")]
+ CircuitNotFound(String),
+
+ #[error("invalid protocol '{0}': expected groth16, plonk, or fflonk")]
+ InvalidProtocol(String),
+
+ #[error("invalid prime '{0}'")]
+ InvalidPrime(String),
+
+ #[error("invalid R1CS file: {0}")]
+ InvalidR1cs(String),
+
+ #[error("invalid witness file: {0}")]
+ InvalidWitness(String),
+
+ #[error("invalid symbols file: {0}")]
+ InvalidSymbols(String),
+
+ #[error("compilation failed: {0}")]
+ CompilationFailed(String),
+
+ #[error("PLONK protocol requires optimization level 1")]
+ PlonkOptimization,
+
+ #[error("PTAU not found and auto-download is unavailable for prime '{0}'")]
+ PtauUnavailable(String),
+
+ #[error("PTAU download failed: {0}")]
+ PtauDownloadFailed(String),
+
+ #[error("file not found: {}", .0.display())]
+ FileNotFound(PathBuf),
+
+ #[error("signal not found: {0}")]
+ SignalNotFound(String),
+
+ #[error("config validation failed: {0}")]
+ ConfigValidation(String),
+
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+
+ #[error(transparent)]
+ Json(#[from] serde_json::Error),
+}
+
+pub type Result = std::result::Result;
diff --git a/crates/circomkit-core/src/functions/calldata.rs b/crates/circomkit-core/src/functions/calldata.rs
new file mode 100644
index 0000000..e4d7e80
--- /dev/null
+++ b/crates/circomkit-core/src/functions/calldata.rs
@@ -0,0 +1,182 @@
+use num_bigint::BigInt;
+
+use crate::error::{CoreError, Result};
+
+/// Generate calldata string from a proof and public signals.
+///
+/// Dispatches to the appropriate protocol-specific formatter based on the
+/// `protocol` field in the proof JSON.
+pub fn get_calldata(proof: &serde_json::Value, pubs: &[String], pretty: bool) -> Result {
+ let protocol = proof["protocol"]
+ .as_str()
+ .ok_or_else(|| CoreError::InvalidR1cs("proof missing 'protocol' field".to_string()))?;
+
+ let pubs_calldata = public_signals_calldata(pubs, pretty);
+
+ let proof_calldata = match protocol {
+ "groth16" => groth16_calldata(proof, pretty),
+ "plonk" => plonk_calldata(proof, pretty),
+ "fflonk" => fflonk_calldata(proof, pretty),
+ _ => return Err(CoreError::InvalidProtocol(protocol.to_string())),
+ };
+
+ Ok(format!("\n{proof_calldata}\n\n{pubs_calldata}\n"))
+}
+
+fn public_signals_calldata(pubs: &[String], pretty: bool) -> String {
+ let pubs256 = values_to_padded_uint256s(pubs);
+ if pretty {
+ format!(
+ "uint[{}] memory pubs = [\n {}\n];",
+ pubs.len(),
+ pubs256.join(",\n ")
+ )
+ } else {
+ let quoted: Vec = pubs256.iter().map(|s| format!("\"{s}\"")).collect();
+ format!("[{}]", quoted.join(","))
+ }
+}
+
+fn groth16_calldata(proof: &serde_json::Value, pretty: bool) -> String {
+ let p_a = values_to_padded_uint256s(&extract_strs(&proof["pi_a"], 2));
+ // Note: pB indices are reversed for Ethereum compatibility
+ let p_b0 = values_to_padded_uint256s(&[
+ extract_str(&proof["pi_b"][0][1]),
+ extract_str(&proof["pi_b"][0][0]),
+ ]);
+ let p_b1 = values_to_padded_uint256s(&[
+ extract_str(&proof["pi_b"][1][1]),
+ extract_str(&proof["pi_b"][1][0]),
+ ]);
+ let p_c = values_to_padded_uint256s(&extract_strs(&proof["pi_c"], 2));
+
+ if pretty {
+ [
+ format!("uint[2] memory pA = [\n {}\n];", p_a.join(",\n ")),
+ format!(
+ "uint[2][2] memory pB = [\n [\n {}\n ],\n [\n {}\n ]\n];",
+ p_b0.join(",\n "),
+ p_b1.join(",\n ")
+ ),
+ format!("uint[2] memory pC = [\n {}\n];", p_c.join(",\n ")),
+ ]
+ .join("\n")
+ } else {
+ [
+ format!("[{}]", with_quotes(&p_a).join(", ")),
+ format!(
+ "[[{}], [{}]]",
+ with_quotes(&p_b0).join(", "),
+ with_quotes(&p_b1).join(", ")
+ ),
+ format!("[{}]", with_quotes(&p_c).join(", ")),
+ ]
+ .join("\n")
+ }
+}
+
+fn plonk_calldata(proof: &serde_json::Value, pretty: bool) -> String {
+ let vals = values_to_padded_uint256s(&[
+ extract_str(&proof["A"][0]),
+ extract_str(&proof["A"][1]),
+ extract_str(&proof["B"][0]),
+ extract_str(&proof["B"][1]),
+ extract_str(&proof["C"][0]),
+ extract_str(&proof["C"][1]),
+ extract_str(&proof["Z"][0]),
+ extract_str(&proof["Z"][1]),
+ extract_str(&proof["T1"][0]),
+ extract_str(&proof["T1"][1]),
+ extract_str(&proof["T2"][0]),
+ extract_str(&proof["T2"][1]),
+ extract_str(&proof["T3"][0]),
+ extract_str(&proof["T3"][1]),
+ extract_str(&proof["Wxi"][0]),
+ extract_str(&proof["Wxi"][1]),
+ extract_str(&proof["Wxiw"][0]),
+ extract_str(&proof["Wxiw"][1]),
+ extract_str(&proof["eval_a"]),
+ extract_str(&proof["eval_b"]),
+ extract_str(&proof["eval_c"]),
+ extract_str(&proof["eval_s1"]),
+ extract_str(&proof["eval_s2"]),
+ extract_str(&proof["eval_zw"]),
+ ]);
+
+ if pretty {
+ format!(
+ "uint[24] memory proof = [\n {}\n];",
+ vals.join(",\n ")
+ )
+ } else {
+ format!("[{}]", with_quotes(&vals).join(","))
+ }
+}
+
+fn fflonk_calldata(proof: &serde_json::Value, pretty: bool) -> String {
+ let vals = values_to_padded_uint256s(&[
+ extract_str(&proof["polynomials"]["C1"][0]),
+ extract_str(&proof["polynomials"]["C1"][1]),
+ extract_str(&proof["polynomials"]["C2"][0]),
+ extract_str(&proof["polynomials"]["C2"][1]),
+ extract_str(&proof["polynomials"]["W1"][0]),
+ extract_str(&proof["polynomials"]["W1"][1]),
+ extract_str(&proof["polynomials"]["W2"][0]),
+ extract_str(&proof["polynomials"]["W2"][1]),
+ extract_str(&proof["evaluations"]["ql"]),
+ extract_str(&proof["evaluations"]["qr"]),
+ extract_str(&proof["evaluations"]["qm"]),
+ extract_str(&proof["evaluations"]["qo"]),
+ extract_str(&proof["evaluations"]["qc"]),
+ extract_str(&proof["evaluations"]["s1"]),
+ extract_str(&proof["evaluations"]["s2"]),
+ extract_str(&proof["evaluations"]["s3"]),
+ extract_str(&proof["evaluations"]["a"]),
+ extract_str(&proof["evaluations"]["b"]),
+ extract_str(&proof["evaluations"]["c"]),
+ extract_str(&proof["evaluations"]["z"]),
+ extract_str(&proof["evaluations"]["zw"]),
+ extract_str(&proof["evaluations"]["t1w"]),
+ extract_str(&proof["evaluations"]["t2w"]),
+ extract_str(&proof["evaluations"]["inv"]),
+ ]);
+
+ if pretty {
+ format!(
+ "uint256[24] memory proof = [\n {}\n];",
+ vals.join(",\n ")
+ )
+ } else {
+ format!("[{}]", with_quotes(&vals).join(","))
+ }
+}
+
+/// Convert decimal string values to 0x-prefixed, zero-padded 64-hex-char (uint256) strings.
+fn values_to_padded_uint256s(values: &[String]) -> Vec {
+ values
+ .iter()
+ .map(|v| {
+ let n = v
+ .parse::()
+ .unwrap_or_else(|_| panic!("invalid decimal value: {v}"));
+ let hex = format!("{:064x}", n);
+ assert!(hex.len() == 64, "uint256 overflow for value: {v}");
+ format!("0x{hex}")
+ })
+ .collect()
+}
+
+fn with_quotes(vals: &[String]) -> Vec {
+ vals.iter().map(|v| format!("\"{v}\"")).collect()
+}
+
+fn extract_str(value: &serde_json::Value) -> String {
+ value
+ .as_str()
+ .map(|s| s.to_string())
+ .unwrap_or_else(|| value.to_string().trim_matches('"').to_string())
+}
+
+fn extract_strs(value: &serde_json::Value, count: usize) -> Vec {
+ (0..count).map(|i| extract_str(&value[i])).collect()
+}
diff --git a/crates/circomkit-core/src/functions/compile.rs b/crates/circomkit-core/src/functions/compile.rs
new file mode 100644
index 0000000..9499f93
--- /dev/null
+++ b/crates/circomkit-core/src/functions/compile.rs
@@ -0,0 +1,76 @@
+use std::path::Path;
+use std::process::Command;
+
+use crate::config::CompilerConfig;
+use crate::error::{CoreError, Result};
+
+/// Output from the circom compilation process.
+#[derive(Debug)]
+pub struct CompileOutput {
+ pub stdout: String,
+ pub stderr: String,
+}
+
+/// Compile a circuit by invoking the `circom` binary as a subprocess.
+pub fn compile_circuit(
+ config: &CompilerConfig,
+ target_path: &Path,
+ out_dir: &Path,
+) -> Result {
+ std::fs::create_dir_all(out_dir)?;
+
+ let mut cmd = Command::new(&config.circom_path);
+
+ // Required flags
+ cmd.arg(target_path);
+ cmd.args(["-p", &config.prime.to_string()]);
+ cmd.args(["-o", &out_dir.to_string_lossy()]);
+
+ // Optional output flags
+ if config.r1cs {
+ cmd.arg("--r1cs");
+ }
+ if config.sym {
+ cmd.arg("--sym");
+ }
+ if config.wasm {
+ cmd.arg("--wasm");
+ }
+ if config.c {
+ cmd.arg("--c");
+ }
+ if config.verbose {
+ cmd.arg("--verbose");
+ }
+ if config.inspect {
+ cmd.arg("--inspect");
+ }
+
+ // Include paths
+ for inc in &config.include {
+ cmd.args(["-l", &inc.to_string_lossy()]);
+ }
+
+ // Optimization level
+ if config.optimization > 2 {
+ cmd.arg(format!("--O2round {}", config.optimization));
+ } else {
+ cmd.arg(format!("--O{}", config.optimization));
+ }
+
+ log::debug!("running: {:?}", cmd);
+
+ let output = cmd.output()?;
+
+ let stdout = String::from_utf8_lossy(&output.stdout).to_string();
+ let stderr = String::from_utf8_lossy(&output.stderr).to_string();
+
+ if !output.status.success() {
+ return Err(CoreError::CompilationFailed(format!(
+ "circom exited with {}\nstderr: {stderr}",
+ output.status
+ )));
+ }
+
+ Ok(CompileOutput { stdout, stderr })
+}
diff --git a/crates/circomkit-core/src/functions/instantiate.rs b/crates/circomkit-core/src/functions/instantiate.rs
new file mode 100644
index 0000000..dff757e
--- /dev/null
+++ b/crates/circomkit-core/src/functions/instantiate.rs
@@ -0,0 +1,114 @@
+use std::path::Path;
+
+use crate::config::CircuitConfig;
+use crate::error::Result;
+
+/// Generate the circom source code for a main component.
+///
+/// The generated file includes the pragma, an include directive
+/// (relative from `main/` up to the circuit file), and the
+/// `component main` declaration.
+pub fn make_circuit_source(config: &CircuitConfig, version: &str) -> String {
+ let mut source = String::new();
+
+ source.push_str("// auto-generated by circomkit\n");
+ source.push_str(&format!("pragma circom {version};\n"));
+
+ if config.uses_custom_templates {
+ source.push_str("pragma custom_templates;\n");
+ }
+
+ source.push_str(&format!("include \"../{}.circom\";\n\n", config.file));
+
+ // component main declaration
+ source.push_str("component main");
+
+ if !config.pubs.is_empty() {
+ source.push_str(&format!(" {{public[{}]}}", config.pubs.join(", ")));
+ }
+
+ source.push_str(&format!(
+ " = {}({});\n",
+ config.template,
+ config
+ .params
+ .iter()
+ .map(|p| p.to_string())
+ .collect::>()
+ .join(", ")
+ ));
+
+ source
+}
+
+/// Write the main component circom file to disk.
+///
+/// Creates the target directory if it doesn't exist.
+pub fn instantiate_circuit(
+ config: &CircuitConfig,
+ target_path: &Path,
+ version: &str,
+) -> Result<()> {
+ let source = make_circuit_source(config, version);
+
+ if let Some(parent) = target_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+
+ std::fs::write(target_path, source)?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn basic_circuit_source() {
+ let config = CircuitConfig {
+ file: "multiplier".to_string(),
+ template: "Multiplier".to_string(),
+ pubs: vec![],
+ params: vec![serde_json::json!(3)],
+ uses_custom_templates: false,
+ overrides: None,
+ };
+
+ let source = make_circuit_source(&config, "2.1.0");
+ assert!(source.contains("pragma circom 2.1.0;"));
+ assert!(source.contains("include \"../multiplier.circom\";"));
+ assert!(source.contains("component main = Multiplier(3);"));
+ assert!(!source.contains("custom_templates"));
+ }
+
+ #[test]
+ fn circuit_with_public_signals() {
+ let config = CircuitConfig {
+ file: "arrays".to_string(),
+ template: "Arrays".to_string(),
+ pubs: vec!["in1D".to_string(), "in2D".to_string()],
+ params: vec![serde_json::json!(2), serde_json::json!(3)],
+ uses_custom_templates: false,
+ overrides: None,
+ };
+
+ let source = make_circuit_source(&config, "2.1.0");
+ assert!(source.contains("{public[in1D, in2D]}"));
+ assert!(source.contains("= Arrays(2, 3);"));
+ }
+
+ #[test]
+ fn circuit_with_custom_templates() {
+ let config = CircuitConfig {
+ file: "custom".to_string(),
+ template: "Custom".to_string(),
+ pubs: vec![],
+ params: vec![],
+ uses_custom_templates: true,
+ overrides: None,
+ };
+
+ let source = make_circuit_source(&config, "2.1.0");
+ assert!(source.contains("pragma custom_templates;"));
+ }
+}
diff --git a/crates/circomkit-core/src/functions/mod.rs b/crates/circomkit-core/src/functions/mod.rs
new file mode 100644
index 0000000..a8020cc
--- /dev/null
+++ b/crates/circomkit-core/src/functions/mod.rs
@@ -0,0 +1,11 @@
+mod calldata;
+mod compile;
+mod instantiate;
+mod symbols;
+
+pub use calldata::get_calldata;
+pub use compile::{CompileOutput, compile_circuit};
+pub use instantiate::{instantiate_circuit, make_circuit_source};
+pub use symbols::{
+ edit_witness, parse_symbols, parse_symbols_str, read_witness_raw, read_witness_signals,
+};
diff --git a/crates/circomkit-core/src/functions/symbols.rs b/crates/circomkit-core/src/functions/symbols.rs
new file mode 100644
index 0000000..0ec9fb7
--- /dev/null
+++ b/crates/circomkit-core/src/functions/symbols.rs
@@ -0,0 +1,180 @@
+use std::collections::HashMap;
+use std::path::Path;
+
+use num_bigint::BigInt;
+
+use crate::error::{CoreError, Result};
+use crate::types::{CircuitSignals, SignalValue, SymbolInfo, Symbols, Witness};
+
+/// Parse a `.sym` file into a symbol table.
+///
+/// Each line has the format: `label_idx,var_idx,component_idx,symbol_name`
+pub fn parse_symbols(path: &Path) -> Result {
+ let content = std::fs::read_to_string(path)?;
+ parse_symbols_str(&content)
+}
+
+/// Parse symbols from a string.
+pub fn parse_symbols_str(content: &str) -> Result {
+ let mut symbols = HashMap::new();
+
+ for line in content.lines() {
+ let line = line.trim();
+ if line.is_empty() {
+ continue;
+ }
+
+ let parts: Vec<&str> = line.splitn(4, ',').collect();
+ if parts.len() < 4 {
+ return Err(CoreError::InvalidSymbols(format!("malformed line: {line}")));
+ }
+
+ let label_idx: usize = parts[0]
+ .parse()
+ .map_err(|_| CoreError::InvalidSymbols(format!("invalid label_idx: {}", parts[0])))?;
+ let var_idx: usize = parts[1]
+ .parse()
+ .map_err(|_| CoreError::InvalidSymbols(format!("invalid var_idx: {}", parts[1])))?;
+ let component_idx: usize = parts[2].parse().map_err(|_| {
+ CoreError::InvalidSymbols(format!("invalid component_idx: {}", parts[2]))
+ })?;
+ let name = parts[3].to_string();
+
+ symbols.insert(
+ name,
+ SymbolInfo {
+ label_idx,
+ var_idx,
+ component_idx,
+ },
+ );
+ }
+
+ Ok(symbols)
+}
+
+/// Read signal values from a witness using the symbol table.
+///
+/// Handles both single signals (`main.out`) and array signals (`main.in`),
+/// automatically collecting indexed entries like `main.in[0]`, `main.in[1]`, etc.
+pub fn read_witness_signals(
+ witness: &Witness,
+ symbols: &Symbols,
+ signals: &[&str],
+) -> Result {
+ let mut result = CircuitSignals::new();
+
+ for &signal in signals {
+ let full_name = format!("main.{signal}");
+
+ // Check for a direct match first
+ if let Some(info) = symbols.get(&full_name) {
+ result.insert(
+ signal.to_string(),
+ SignalValue::Single(witness[info.var_idx].clone()),
+ );
+ continue;
+ }
+
+ // Try to collect array entries: main.signal[0], main.signal[1], ...
+ let prefix = format!("{full_name}[");
+ let mut indexed: Vec<(usize, BigInt)> = Vec::new();
+
+ for (name, info) in symbols {
+ if let Some(idx) = name
+ .strip_prefix(&prefix)
+ .and_then(|rest| rest.strip_suffix(']'))
+ .and_then(|s| s.parse::().ok())
+ {
+ indexed.push((idx, witness[info.var_idx].clone()));
+ }
+ }
+
+ if indexed.is_empty() {
+ return Err(CoreError::SignalNotFound(signal.to_string()));
+ }
+
+ indexed.sort_by_key(|(idx, _)| *idx);
+ let values: Vec = indexed
+ .into_iter()
+ .map(|(_, v)| SignalValue::Single(v))
+ .collect();
+ result.insert(signal.to_string(), SignalValue::Array(values));
+ }
+
+ Ok(result)
+}
+
+/// Read raw symbol values from a witness by full symbol names.
+pub fn read_witness_raw(
+ witness: &Witness,
+ symbols: &Symbols,
+ symbol_names: &[&str],
+) -> Result> {
+ let mut result = HashMap::new();
+
+ for &name in symbol_names {
+ let info = symbols
+ .get(name)
+ .ok_or_else(|| CoreError::SignalNotFound(name.to_string()))?;
+ result.insert(name.to_string(), witness[info.var_idx].clone());
+ }
+
+ Ok(result)
+}
+
+/// Edit witness values by symbol name (useful for soundness testing).
+pub fn edit_witness(
+ witness: &Witness,
+ symbols: &Symbols,
+ overrides: &HashMap,
+) -> Result {
+ let mut edited = witness.clone();
+
+ for (name, value) in overrides {
+ let info = symbols
+ .get(name)
+ .ok_or_else(|| CoreError::SignalNotFound(name.clone()))?;
+ edited[info.var_idx] = value.clone();
+ }
+
+ Ok(edited)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_sym_content() {
+ let content = "1,1,0,main.in[0]\n2,2,0,main.in[1]\n3,3,0,main.out\n";
+ let symbols = parse_symbols_str(content).unwrap();
+
+ assert_eq!(symbols.len(), 3);
+ assert_eq!(symbols["main.out"].var_idx, 3);
+ assert_eq!(symbols["main.in[0]"].var_idx, 1);
+ }
+
+ #[test]
+ fn read_signals_single_and_array() {
+ let content = "1,1,0,main.in[0]\n2,2,0,main.in[1]\n3,3,0,main.out\n";
+ let symbols = parse_symbols_str(content).unwrap();
+ let witness: Witness = vec![
+ BigInt::from(0),
+ BigInt::from(3),
+ BigInt::from(5),
+ BigInt::from(15),
+ ];
+
+ let signals = read_witness_signals(&witness, &symbols, &["in", "out"]).unwrap();
+
+ assert_eq!(signals["out"], SignalValue::Single(BigInt::from(15)));
+ assert_eq!(
+ signals["in"],
+ SignalValue::Array(vec![
+ SignalValue::Single(BigInt::from(3)),
+ SignalValue::Single(BigInt::from(5)),
+ ])
+ );
+ }
+}
diff --git a/crates/circomkit-core/src/lib.rs b/crates/circomkit-core/src/lib.rs
new file mode 100644
index 0000000..9f2ca20
--- /dev/null
+++ b/crates/circomkit-core/src/lib.rs
@@ -0,0 +1,7 @@
+pub mod config;
+pub mod enums;
+pub mod error;
+pub mod functions;
+pub mod pathing;
+pub mod types;
+pub mod utils;
diff --git a/crates/circomkit-core/src/pathing.rs b/crates/circomkit-core/src/pathing.rs
new file mode 100644
index 0000000..49b54c5
--- /dev/null
+++ b/crates/circomkit-core/src/pathing.rs
@@ -0,0 +1,198 @@
+use std::path::PathBuf;
+
+use crate::config::CircomkitConfig;
+use crate::enums::Protocol;
+
+/// Resolves all filesystem paths for circomkit artifacts.
+#[derive(Debug, Clone)]
+pub struct CircomkitPaths {
+ // TODO: can this be static perhaps?
+ // path lives as long as the Circomkit instance, so we can store refs instead of owned Paths if needed
+ src_dir: PathBuf,
+ out_dir: PathBuf,
+ input_dir: PathBuf,
+ ptau_dir: PathBuf,
+}
+
+impl CircomkitPaths {
+ pub fn new(config: &CircomkitConfig) -> Self {
+ Self {
+ src_dir: config.compiler.src_dir.clone(),
+ out_dir: config.compiler.out_dir.clone(),
+ input_dir: config.prover.input_dir.clone(),
+ ptau_dir: config.prover.ptau_dir.clone(),
+ }
+ }
+
+ // ---- Circuit-level paths ----
+
+ /// Build directory for a circuit: `{out_dir}/{circuit}`
+ pub fn circuit_dir(&self, circuit: &str) -> PathBuf {
+ self.out_dir.join(circuit)
+ }
+
+ /// R1CS file: `{out_dir}/{circuit}/{circuit}.r1cs`
+ pub fn circuit_r1cs(&self, circuit: &str) -> PathBuf {
+ self.circuit_dir(circuit).join(format!("{circuit}.r1cs"))
+ }
+
+ /// Symbol file: `{out_dir}/{circuit}/{circuit}.sym`
+ pub fn circuit_sym(&self, circuit: &str) -> PathBuf {
+ self.circuit_dir(circuit).join(format!("{circuit}.sym"))
+ }
+
+ /// WASM file: `{out_dir}/{circuit}/{circuit}_js/{circuit}.wasm`
+ pub fn circuit_wasm(&self, circuit: &str) -> PathBuf {
+ self.circuit_dir(circuit)
+ .join(format!("{circuit}_js"))
+ .join(format!("{circuit}.wasm"))
+ }
+
+ /// Main component source file: `{src_dir}/main/{circuit}.circom`
+ pub fn circuit_main(&self, circuit: &str) -> PathBuf {
+ self.src_dir.join("main").join(format!("{circuit}.circom"))
+ }
+
+ /// Original circuit template source file: `{src_dir}/{file}.circom`
+ pub fn circuit_source(&self, file: &str) -> PathBuf {
+ self.src_dir.join(format!("{file}.circom"))
+ }
+
+ /// C witness calculator directory: `{out_dir}/{circuit}/{circuit}_cpp`
+ pub fn circuit_c_dir(&self, circuit: &str) -> PathBuf {
+ self.circuit_dir(circuit).join(format!("{circuit}_cpp"))
+ }
+
+ // ---- Protocol-dependent paths ----
+
+ /// Prover key: `{out_dir}/{circuit}/{protocol}_pkey.zkey`
+ pub fn pkey(&self, circuit: &str, protocol: Protocol) -> PathBuf {
+ self.circuit_dir(circuit)
+ .join(format!("{protocol}_pkey.zkey"))
+ }
+
+ /// Verification key: `{out_dir}/{circuit}/{protocol}_vkey.json`
+ pub fn vkey(&self, circuit: &str, protocol: Protocol) -> PathBuf {
+ self.circuit_dir(circuit)
+ .join(format!("{protocol}_vkey.json"))
+ }
+
+ /// Solidity verifier contract: `{out_dir}/{circuit}/{protocol}_verifier.sol`
+ pub fn verifier_sol(&self, circuit: &str, protocol: Protocol) -> PathBuf {
+ self.circuit_dir(circuit)
+ .join(format!("{protocol}_verifier.sol"))
+ }
+
+ // ---- Input-dependent paths ----
+
+ /// Input-specific output directory: `{out_dir}/{circuit}/{input}`
+ pub fn input_dir(&self, circuit: &str, input: &str) -> PathBuf {
+ self.circuit_dir(circuit).join(input)
+ }
+
+ /// Witness file: `{out_dir}/{circuit}/{input}/witness.wtns`
+ pub fn witness_path(&self, circuit: &str, input: &str) -> PathBuf {
+ self.input_dir(circuit, input).join("witness.wtns")
+ }
+
+ /// Proof file: `{out_dir}/{circuit}/{input}/{protocol}_proof.json`
+ pub fn proof_path(&self, circuit: &str, input: &str, protocol: Protocol) -> PathBuf {
+ self.input_dir(circuit, input)
+ .join(format!("{protocol}_proof.json"))
+ }
+
+ /// Public signals file: `{out_dir}/{circuit}/{input}/public.json`
+ pub fn public_signals_path(&self, circuit: &str, input: &str) -> PathBuf {
+ self.input_dir(circuit, input).join("public.json")
+ }
+
+ // ---- Other paths ----
+
+ /// Input JSON file: `{input_dir}/{circuit}/{input}.json`
+ pub fn input_json(&self, circuit: &str, input: &str) -> PathBuf {
+ self.input_dir.join(circuit).join(format!("{input}.json"))
+ }
+
+ /// PTAU file: `{ptau_dir}/{ptau_name}`
+ pub fn ptau(&self, ptau_name: &str) -> PathBuf {
+ self.ptau_dir.join(ptau_name)
+ }
+
+ /// Intermediate zkey: `{out_dir}/{circuit}/{circuit}_{id}.zkey`
+ pub fn zkey(&self, circuit: &str, id: u32) -> PathBuf {
+ self.circuit_dir(circuit)
+ .join(format!("{circuit}_{id}.zkey"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn test_config() -> CircomkitConfig {
+ CircomkitConfig::default()
+ }
+
+ #[test]
+ fn circuit_artifact_paths() {
+ let paths = CircomkitPaths::new(&test_config());
+
+ assert_eq!(
+ paths.circuit_r1cs("mul"),
+ PathBuf::from("./build/mul/mul.r1cs")
+ );
+ assert_eq!(
+ paths.circuit_sym("mul"),
+ PathBuf::from("./build/mul/mul.sym")
+ );
+ assert_eq!(
+ paths.circuit_wasm("mul"),
+ PathBuf::from("./build/mul/mul_js/mul.wasm")
+ );
+ assert_eq!(
+ paths.circuit_main("mul"),
+ PathBuf::from("./circuits/main/mul.circom")
+ );
+ }
+
+ #[test]
+ fn protocol_dependent_paths() {
+ let paths = CircomkitPaths::new(&test_config());
+
+ assert_eq!(
+ paths.pkey("mul", Protocol::Groth16),
+ PathBuf::from("./build/mul/groth16_pkey.zkey")
+ );
+ assert_eq!(
+ paths.vkey("mul", Protocol::Plonk),
+ PathBuf::from("./build/mul/plonk_vkey.json")
+ );
+ }
+
+ #[test]
+ fn input_dependent_paths() {
+ let paths = CircomkitPaths::new(&test_config());
+
+ assert_eq!(
+ paths.witness_path("mul", "test"),
+ PathBuf::from("./build/mul/test/witness.wtns")
+ );
+ assert_eq!(
+ paths.proof_path("mul", "test", Protocol::Groth16),
+ PathBuf::from("./build/mul/test/groth16_proof.json")
+ );
+ assert_eq!(
+ paths.input_json("mul", "test"),
+ PathBuf::from("./inputs/mul/test.json")
+ );
+ }
+
+ #[test]
+ fn zkey_path() {
+ let paths = CircomkitPaths::new(&test_config());
+ assert_eq!(
+ paths.zkey("mul", 0),
+ PathBuf::from("./build/mul/mul_0.zkey")
+ );
+ }
+}
diff --git a/crates/circomkit-core/src/types/circuit.rs b/crates/circomkit-core/src/types/circuit.rs
new file mode 100644
index 0000000..4c98d29
--- /dev/null
+++ b/crates/circomkit-core/src/types/circuit.rs
@@ -0,0 +1,160 @@
+use num_bigint::BigInt;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::collections::HashMap;
+
+/// A signal value: either a single field element or a nested array.
+///
+/// When serialized to JSON, single values are written as decimal strings
+/// (e.g. `"42"`) for compatibility with snarkjs. Arrays are regular JSON arrays.
+/// Deserialization accepts both numbers and strings.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum SignalValue {
+ Single(BigInt),
+ Array(Vec),
+}
+
+impl Serialize for SignalValue {
+ fn serialize(&self, serializer: S) -> Result {
+ match self {
+ SignalValue::Single(v) => {
+ if let Ok(n) = i64::try_from(v) {
+ serializer.serialize_i64(n)
+ } else {
+ serializer.serialize_str(&v.to_string())
+ }
+ }
+ SignalValue::Array(arr) => arr.serialize(serializer),
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for SignalValue {
+ fn deserialize>(deserializer: D) -> Result {
+ let value = serde_json::Value::deserialize(deserializer)?;
+ signal_from_json(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+fn signal_from_json(value: &serde_json::Value) -> Result {
+ match value {
+ serde_json::Value::Number(n) => {
+ if let Some(i) = n.as_i64() {
+ Ok(SignalValue::Single(BigInt::from(i)))
+ } else if let Some(u) = n.as_u64() {
+ Ok(SignalValue::Single(BigInt::from(u)))
+ } else {
+ Err(format!("unsupported number: {n}"))
+ }
+ }
+ serde_json::Value::String(s) => s
+ .parse::()
+ .map(SignalValue::Single)
+ .map_err(|e| format!("invalid BigInt string '{s}': {e}")),
+ serde_json::Value::Array(arr) => {
+ let items: Result, _> = arr.iter().map(signal_from_json).collect();
+ Ok(SignalValue::Array(items?))
+ }
+ _ => Err(format!("unexpected JSON value for signal: {value}")),
+ }
+}
+
+impl From for SignalValue {
+ fn from(v: i64) -> Self {
+ Self::Single(BigInt::from(v))
+ }
+}
+
+impl From for SignalValue {
+ fn from(v: u64) -> Self {
+ Self::Single(BigInt::from(v))
+ }
+}
+
+impl From for SignalValue {
+ fn from(v: i32) -> Self {
+ Self::Single(BigInt::from(v))
+ }
+}
+
+impl From for SignalValue {
+ fn from(v: u32) -> Self {
+ Self::Single(BigInt::from(v))
+ }
+}
+
+impl From for SignalValue {
+ fn from(v: BigInt) -> Self {
+ Self::Single(v)
+ }
+}
+
+impl> From> for SignalValue {
+ fn from(v: Vec) -> Self {
+ Self::Array(v.into_iter().map(Into::into).collect())
+ }
+}
+
+impl + Clone> From<&[T]> for SignalValue {
+ fn from(v: &[T]) -> Self {
+ Self::Array(v.iter().cloned().map(Into::into).collect())
+ }
+}
+
+/// Circuit input/output signals: a map from signal name to value.
+pub type CircuitSignals = HashMap;
+
+/// A witness: vector of field elements.
+pub type Witness = Vec;
+
+/// Convenience macro for building `CircuitSignals`.
+///
+/// # Examples
+///
+/// ```
+/// use circomkit_core::signals;
+///
+/// let s = signals! {
+/// "in" => vec![3_i64, 5, 7],
+/// "out" => 105_i64,
+/// };
+/// ```
+#[macro_export]
+macro_rules! signals {
+ ($($key:literal => $val:expr),* $(,)?) => {{
+ let mut map = std::collections::HashMap::new();
+ $(map.insert($key.to_string(), $crate::types::SignalValue::from($val));)*
+ map
+ }};
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn serialize_signals_as_json() {
+ let input = crate::signals! {
+ "in" => vec![2_i64, 3, 5],
+ "out" => 30_i64,
+ };
+ let json = serde_json::to_string(&input).unwrap();
+ assert!(json.contains("[2,3,5]") || json.contains("[2, 3, 5]"));
+ assert!(json.contains("30"));
+ assert!(!json.contains("BigInt"));
+ }
+
+ #[test]
+ fn deserialize_signals_from_json() {
+ let json = r#"{"in": [2, 3, 5], "out": 30}"#;
+ let signals: CircuitSignals = serde_json::from_str(json).unwrap();
+ assert_eq!(signals["out"], SignalValue::Single(BigInt::from(30)));
+ }
+
+ #[test]
+ fn deserialize_string_signals() {
+ let json = r#"{"val": "12345678901234567890"}"#;
+ let signals: CircuitSignals = serde_json::from_str(json).unwrap();
+ let expected = "12345678901234567890".parse::().unwrap();
+ assert_eq!(signals["val"], SignalValue::Single(expected));
+ }
+}
diff --git a/crates/circomkit-core/src/types/mod.rs b/crates/circomkit-core/src/types/mod.rs
new file mode 100644
index 0000000..6e3dffa
--- /dev/null
+++ b/crates/circomkit-core/src/types/mod.rs
@@ -0,0 +1,7 @@
+mod circuit;
+mod r1cs;
+mod symbols;
+
+pub use circuit::{CircuitSignals, SignalValue, Witness};
+pub use r1cs::{LinearCombination, R1CSConstraint, R1CSFile, R1CSInfo};
+pub use symbols::{SymbolInfo, Symbols};
diff --git a/crates/circomkit-core/src/types/r1cs.rs b/crates/circomkit-core/src/types/r1cs.rs
new file mode 100644
index 0000000..1d06227
--- /dev/null
+++ b/crates/circomkit-core/src/types/r1cs.rs
@@ -0,0 +1,52 @@
+use std::collections::HashMap;
+
+use num_bigint::BigUint;
+
+use crate::enums::Prime;
+
+/// Information extracted from the R1CS binary header.
+#[derive(Debug, Clone)]
+pub struct R1CSInfo {
+ pub wires: u32,
+ pub constraints: u32,
+ pub private_inputs: u32,
+ pub public_inputs: u32,
+ pub public_outputs: u32,
+ pub uses_custom_gates: bool,
+ pub labels: u64,
+ pub prime: BigUint,
+ pub prime_name: Option,
+}
+
+/// A single linear combination: mapping from wire index to coefficient.
+pub type LinearCombination = HashMap;
+
+/// A single R1CS constraint: three linear combinations (A, B, C) such that A * B = C.
+pub type R1CSConstraint = [LinearCombination; 3];
+
+/// A fully-parsed R1CS file with generic field element type.
+///
+/// The type parameter `T` is the field element type, determined by the
+/// `chunk_to_elem` closure passed to [`parse_r1cs_bytes`](crate::utils::parse_r1cs_bytes).
+/// This allows zero-cost abstraction over different backends (BigInt, arkworks, lambdaworks).
+#[derive(Debug, Clone)]
+pub struct R1CSFile {
+ /// Bytes per field element (typically 32).
+ pub n8: usize,
+ /// Field prime as raw little-endian bytes.
+ pub prime: BigUint,
+ /// Total number of variables (wires).
+ pub num_variables: usize,
+ /// Number of public outputs.
+ pub num_outputs: u32,
+ /// Number of public inputs (excludes the constant wire).
+ pub num_pub_inputs: u32,
+ /// Number of private inputs.
+ pub num_priv_inputs: u32,
+ /// Number of labels.
+ pub num_labels: u64,
+ /// Number of constraints.
+ pub num_constraints: usize,
+ /// Constraint list: each entry has three sparse linear combinations [A, B, C].
+ pub constraints: Vec>,
+}
diff --git a/crates/circomkit-core/src/types/symbols.rs b/crates/circomkit-core/src/types/symbols.rs
new file mode 100644
index 0000000..89f0baa
--- /dev/null
+++ b/crates/circomkit-core/src/types/symbols.rs
@@ -0,0 +1,12 @@
+use std::collections::HashMap;
+
+/// Symbol table entry from a `.sym` file.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SymbolInfo {
+ pub label_idx: usize,
+ pub var_idx: usize,
+ pub component_idx: usize,
+}
+
+/// Symbol table: maps full symbol names (e.g. `main.signal[0]`) to their metadata.
+pub type Symbols = HashMap;
diff --git a/crates/circomkit-core/src/utils/mod.rs b/crates/circomkit-core/src/utils/mod.rs
new file mode 100644
index 0000000..c1bcad7
--- /dev/null
+++ b/crates/circomkit-core/src/utils/mod.rs
@@ -0,0 +1,11 @@
+mod primes;
+mod ptau;
+mod r1cs;
+mod witness;
+
+pub use primes::{prime_from_value, prime_value};
+pub use ptau::{download_ptau, ptau_name_for_constraints, ptau_path_if_exists};
+pub use r1cs::{parse_r1cs_bytes, parse_r1cs_info, read_r1cs_file, read_r1cs_info};
+pub use witness::{
+ parse_witness_bytes, parse_witness_file, parse_witness_to_elems, write_witness_file,
+};
diff --git a/crates/circomkit-core/src/utils/primes.rs b/crates/circomkit-core/src/utils/primes.rs
new file mode 100644
index 0000000..fae66aa
--- /dev/null
+++ b/crates/circomkit-core/src/utils/primes.rs
@@ -0,0 +1,84 @@
+use num_bigint::BigUint;
+
+use crate::enums::Prime;
+
+/// Returns the field prime value for a given prime variant.
+pub fn prime_value(prime: Prime) -> BigUint {
+ match prime {
+ Prime::Bn128 => BigUint::parse_bytes(
+ b"21888242871839275222246405745257275088548364400416034343698204186575808495617",
+ 10,
+ )
+ .expect("valid bn128 prime"),
+ Prime::Bls12381 => BigUint::parse_bytes(
+ b"73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001",
+ 16,
+ )
+ .expect("valid bls12381 prime"),
+ Prime::Goldilocks => {
+ BigUint::parse_bytes(b"18446744069414584321", 10).expect("valid goldilocks prime")
+ }
+ Prime::Grumpkin => BigUint::parse_bytes(
+ b"21888242871839275222246405745257275088696311157297823662689037894645226208583",
+ 10,
+ )
+ .expect("valid grumpkin prime"),
+ Prime::Pallas => BigUint::parse_bytes(
+ b"28948022309329048855892746252171976963363056481941560715954676764349967630337",
+ 10,
+ )
+ .expect("valid pallas prime"),
+ Prime::Vesta => BigUint::parse_bytes(
+ b"28948022309329048855892746252171976963363056481941647379583120057206261314561",
+ 10,
+ )
+ .expect("valid vesta prime"),
+ Prime::Secq256r1 => BigUint::parse_bytes(
+ b"115792089210356248762697446949407573530086143415290314195533631308867097853951",
+ 10,
+ )
+ .expect("valid secq256r1 prime"),
+ }
+}
+
+/// Attempts to identify a `Prime` variant from its field value.
+pub fn prime_from_value(value: &BigUint) -> Option {
+ let primes = [
+ Prime::Bn128,
+ Prime::Bls12381,
+ Prime::Goldilocks,
+ Prime::Grumpkin,
+ Prime::Pallas,
+ Prime::Vesta,
+ Prime::Secq256r1,
+ ];
+ primes.into_iter().find(|p| prime_value(*p) == *value)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn roundtrip_all_primes() {
+ let primes = [
+ Prime::Bn128,
+ Prime::Bls12381,
+ Prime::Goldilocks,
+ Prime::Grumpkin,
+ Prime::Pallas,
+ Prime::Vesta,
+ Prime::Secq256r1,
+ ];
+ for p in primes {
+ let val = prime_value(p);
+ assert_eq!(prime_from_value(&val), Some(p), "roundtrip failed for {p}");
+ }
+ }
+
+ #[test]
+ fn unknown_prime_returns_none() {
+ let unknown = BigUint::from(42u32);
+ assert_eq!(prime_from_value(&unknown), None);
+ }
+}
diff --git a/crates/circomkit-core/src/utils/ptau.rs b/crates/circomkit-core/src/utils/ptau.rs
new file mode 100644
index 0000000..338695c
--- /dev/null
+++ b/crates/circomkit-core/src/utils/ptau.rs
@@ -0,0 +1,88 @@
+use std::path::{Path, PathBuf};
+
+use crate::error::{CoreError, Result};
+
+/// Base URL for Hermez PTAU files.
+const PTAU_BASE_URL: &str = "https://storage.googleapis.com/zkevm/ptau";
+
+/// Determine the PTAU file name for a given constraint count.
+///
+/// Finds the smallest power of 2 such that `2^p >= constraints`,
+/// then returns the corresponding PTAU filename.
+pub fn ptau_name_for_constraints(constraints: u32) -> String {
+ let mut power = 1u32;
+ let mut exp = 0u32;
+ while power < constraints {
+ power = power.saturating_mul(2);
+ exp += 1;
+ }
+ // Minimum PTAU is 2^1
+ exp = exp.max(1);
+ format!("powersOfTau28_hez_final_{exp:02}.ptau")
+}
+
+/// Download a PTAU file if it doesn't already exist.
+///
+/// Only available for BN128 prime. Returns the path to the PTAU file.
+#[cfg(feature = "download")]
+pub fn download_ptau(ptau_name: &str, ptau_dir: &Path) -> Result {
+ let ptau_path = ptau_dir.join(ptau_name);
+
+ if ptau_path.exists() {
+ log::info!("PTAU already exists: {}", ptau_path.display());
+ return Ok(ptau_path);
+ }
+
+ std::fs::create_dir_all(ptau_dir)?;
+
+ let url = format!("{PTAU_BASE_URL}/{ptau_name}");
+ log::info!("downloading PTAU from {url}");
+
+ let response = ureq::get(&url)
+ .call()
+ .map_err(|e| CoreError::PtauDownloadFailed(e.to_string()))?;
+
+ let mut reader = response.into_reader();
+ let mut file = std::fs::File::create(&ptau_path)?;
+ std::io::copy(&mut reader, &mut file)?;
+
+ log::info!("PTAU saved to {}", ptau_path.display());
+ Ok(ptau_path)
+}
+
+/// Check if a PTAU file exists at the expected path (without downloading).
+pub fn ptau_path_if_exists(ptau_name: &str, ptau_dir: &Path) -> Option {
+ let path = ptau_dir.join(ptau_name);
+ path.exists().then_some(path)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // TODO: add ignored tests for `download_ptau` that actually download the file
+
+ #[test]
+ fn ptau_naming() {
+ assert_eq!(
+ ptau_name_for_constraints(1),
+ "powersOfTau28_hez_final_01.ptau"
+ );
+ assert_eq!(
+ ptau_name_for_constraints(2),
+ "powersOfTau28_hez_final_01.ptau"
+ );
+ assert_eq!(
+ ptau_name_for_constraints(3),
+ "powersOfTau28_hez_final_02.ptau"
+ );
+ assert_eq!(
+ ptau_name_for_constraints(1024),
+ "powersOfTau28_hez_final_10.ptau"
+ );
+ assert_eq!(
+ ptau_name_for_constraints(1025),
+ "powersOfTau28_hez_final_11.ptau"
+ );
+ }
+}
diff --git a/crates/circomkit-core/src/utils/r1cs.rs b/crates/circomkit-core/src/utils/r1cs.rs
new file mode 100644
index 0000000..bac28f8
--- /dev/null
+++ b/crates/circomkit-core/src/utils/r1cs.rs
@@ -0,0 +1,245 @@
+use std::collections::HashMap;
+use std::path::Path;
+
+use num_bigint::BigUint;
+
+use super::primes::prime_from_value;
+use crate::error::{CoreError, Result};
+use crate::types::{R1CSFile, R1CSInfo};
+
+/// Read circuit information from an R1CS binary file (header only).
+///
+/// Follows the spec at .
+pub fn read_r1cs_info(path: &Path) -> Result {
+ let buffer = std::fs::read(path)?;
+ parse_r1cs_info(&buffer)
+}
+
+/// Parse R1CS info from a byte buffer.
+pub fn parse_r1cs_info(buffer: &[u8]) -> Result {
+ let mut pos = 0;
+
+ // Magic: "r1cs" (4 bytes)
+ if buffer.len() < 12 || &buffer[pos..pos + 4] != b"r1cs" {
+ return Err(CoreError::InvalidR1cs(
+ "missing 'r1cs' magic bytes".to_string(),
+ ));
+ }
+ pos += 4;
+
+ // Version (4 bytes LE)
+ let version = read_u32(buffer, &mut pos);
+ if version != 1 {
+ return Err(CoreError::InvalidR1cs(format!(
+ "unsupported version: expected 1, got {version}"
+ )));
+ }
+
+ // Number of sections (4 bytes LE)
+ let n_sections = read_u32(buffer, &mut pos);
+
+ let mut info = R1CSInfo {
+ wires: 0,
+ constraints: 0,
+ private_inputs: 0,
+ public_inputs: 0,
+ public_outputs: 0,
+ uses_custom_gates: false,
+ labels: 0,
+ prime: BigUint::ZERO,
+ prime_name: None,
+ };
+
+ for _ in 0..n_sections {
+ if pos + 12 > buffer.len() {
+ break;
+ }
+
+ let section_type = read_u32(buffer, &mut pos);
+ let section_size = read_u64(buffer, &mut pos) as usize;
+
+ if pos + section_size > buffer.len() {
+ return Err(CoreError::InvalidR1cs("section exceeds buffer".to_string()));
+ }
+
+ match section_type {
+ // Header section
+ 1 => {
+ let field_size = read_u32(buffer, &mut pos) as usize;
+
+ // Prime (field_size bytes, little-endian)
+ info.prime = BigUint::from_bytes_le(&buffer[pos..pos + field_size]);
+ pos += field_size;
+
+ info.prime_name = prime_from_value(&info.prime);
+
+ info.wires = read_u32(buffer, &mut pos);
+ info.public_outputs = read_u32(buffer, &mut pos);
+ info.public_inputs = read_u32(buffer, &mut pos);
+ info.private_inputs = read_u32(buffer, &mut pos);
+ info.labels = read_u64(buffer, &mut pos);
+ info.constraints = read_u32(buffer, &mut pos);
+ }
+ // Custom gates list (PLONK)
+ 4 => {
+ info.uses_custom_gates = read_u32(buffer, &mut pos) > 0;
+ // Skip the rest of this section
+ pos += section_size - 4;
+ }
+ // Skip other sections
+ _ => {
+ pos += section_size;
+ }
+ }
+ }
+
+ Ok(info)
+}
+
+/// Read and fully parse a binary `.r1cs` file, including constraints.
+///
+/// The `chunk_to_elem` closure converts raw little-endian coefficient bytes (`n8` bytes)
+/// into the target field element type `T`. This follows the same pattern as
+/// [`parse_witness_to_elems`](super::witness::parse_witness_to_elems), enabling zero-cost
+/// abstraction over different backends (BigInt, arkworks, lambdaworks).
+pub fn read_r1cs_file(path: &Path, chunk_to_elem: impl Fn(&[u8]) -> T) -> Result> {
+ let buffer = std::fs::read(path)?;
+ parse_r1cs_bytes(&buffer, chunk_to_elem)
+}
+
+/// Parse a full R1CS from a byte buffer, including constraints.
+///
+/// See [`read_r1cs_file`] for details on the `chunk_to_elem` closure.
+pub fn parse_r1cs_bytes(
+ buffer: &[u8],
+ chunk_to_elem: impl Fn(&[u8]) -> T,
+) -> Result> {
+ let mut pos = 0;
+
+ // Magic: "r1cs" (4 bytes)
+ if buffer.len() < 12 || &buffer[pos..pos + 4] != b"r1cs" {
+ return Err(CoreError::InvalidR1cs(
+ "missing 'r1cs' magic bytes".to_string(),
+ ));
+ }
+ pos += 4;
+
+ // Version (4 bytes LE)
+ let version = read_u32(buffer, &mut pos);
+ if version != 1 {
+ return Err(CoreError::InvalidR1cs(format!(
+ "unsupported version: expected 1, got {version}"
+ )));
+ }
+
+ // Number of sections (4 bytes LE)
+ let n_sections = read_u32(buffer, &mut pos);
+
+ // First pass: collect section offsets
+ let mut section_offsets: HashMap = HashMap::new();
+ for _ in 0..n_sections {
+ if pos + 12 > buffer.len() {
+ return Err(CoreError::InvalidR1cs("unexpected end of file".to_string()));
+ }
+ let section_type = read_u32(buffer, &mut pos);
+ let section_size = read_u64(buffer, &mut pos) as usize;
+ if pos + section_size > buffer.len() {
+ return Err(CoreError::InvalidR1cs("section exceeds buffer".to_string()));
+ }
+ section_offsets.insert(section_type, (pos, section_size));
+ pos += section_size;
+ }
+
+ // Parse header (section 1)
+ let &(header_pos, _) = section_offsets
+ .get(&1)
+ .ok_or_else(|| CoreError::InvalidR1cs("missing header section".to_string()))?;
+ pos = header_pos;
+
+ let n8 = read_u32(buffer, &mut pos) as usize;
+ let prime = BigUint::from_bytes_le(&buffer[pos..pos + n8]);
+ pos += n8;
+
+ let num_variables = read_u32(buffer, &mut pos) as usize;
+ let num_outputs = read_u32(buffer, &mut pos);
+ let num_pub_inputs = read_u32(buffer, &mut pos);
+ let num_priv_inputs = read_u32(buffer, &mut pos);
+ let num_labels = read_u64(buffer, &mut pos);
+ let num_constraints = read_u32(buffer, &mut pos) as usize;
+
+ // Parse constraints (section 2)
+ let &(constraint_pos, _) = section_offsets
+ .get(&2)
+ .ok_or_else(|| CoreError::InvalidR1cs("missing constraints section".to_string()))?;
+ pos = constraint_pos;
+
+ let constraints = parse_constraints(buffer, &mut pos, num_constraints, n8, &chunk_to_elem)?;
+
+ Ok(R1CSFile {
+ n8,
+ prime,
+ num_variables,
+ num_outputs,
+ num_pub_inputs,
+ num_priv_inputs,
+ num_labels,
+ num_constraints,
+ constraints,
+ })
+}
+
+/// Parse all constraints from the constraints section.
+///
+/// Each constraint has three linear combinations (A, B, C).
+/// Each linear combination is: count (u32), then count × (wire_index: u32, coefficient: n8 bytes LE).
+fn parse_constraints(
+ buffer: &[u8],
+ pos: &mut usize,
+ num_constraints: usize,
+ n8: usize,
+ chunk_to_elem: &impl Fn(&[u8]) -> T,
+) -> Result; 3]>> {
+ let mut constraints = Vec::with_capacity(num_constraints);
+ for _ in 0..num_constraints {
+ let a = parse_linear_combination(buffer, pos, n8, chunk_to_elem)?;
+ let b = parse_linear_combination(buffer, pos, n8, chunk_to_elem)?;
+ let c = parse_linear_combination(buffer, pos, n8, chunk_to_elem)?;
+ constraints.push([a, b, c]);
+ }
+ Ok(constraints)
+}
+
+/// Parse a single linear combination: a sparse map from wire index to coefficient.
+fn parse_linear_combination(
+ buffer: &[u8],
+ pos: &mut usize,
+ n8: usize,
+ chunk_to_elem: &impl Fn(&[u8]) -> T,
+) -> Result> {
+ let n_terms = read_u32(buffer, pos) as usize;
+ let mut lc = HashMap::with_capacity(n_terms);
+ for _ in 0..n_terms {
+ let wire_idx = read_u32(buffer, pos) as usize;
+ if *pos + n8 > buffer.len() {
+ return Err(CoreError::InvalidR1cs(
+ "constraint coefficient exceeds buffer".to_string(),
+ ));
+ }
+ let coeff = chunk_to_elem(&buffer[*pos..*pos + n8]);
+ *pos += n8;
+ lc.insert(wire_idx, coeff);
+ }
+ Ok(lc)
+}
+
+fn read_u32(buffer: &[u8], pos: &mut usize) -> u32 {
+ let val = u32::from_le_bytes(buffer[*pos..*pos + 4].try_into().unwrap());
+ *pos += 4;
+ val
+}
+
+fn read_u64(buffer: &[u8], pos: &mut usize) -> u64 {
+ let val = u64::from_le_bytes(buffer[*pos..*pos + 8].try_into().unwrap());
+ *pos += 8;
+ val
+}
diff --git a/crates/circomkit-core/src/utils/witness.rs b/crates/circomkit-core/src/utils/witness.rs
new file mode 100644
index 0000000..17ff0b3
--- /dev/null
+++ b/crates/circomkit-core/src/utils/witness.rs
@@ -0,0 +1,157 @@
+use std::io::Write;
+use std::path::Path;
+
+use num_bigint::BigInt;
+use num_traits::Signed;
+
+use super::primes::prime_value;
+use crate::error::{CoreError, Result};
+use crate::types::Witness;
+
+/// Parse a binary `.wtns` file into witness elements.
+///
+/// The witness file format (as generated by Circom witness calculators):
+/// - Header: `"wtns"` magic (4 bytes), version (4 bytes LE), num sections (4 bytes LE)
+/// - Section 1: field metadata — `n8` (4 bytes LE), prime `q` (n8 bytes LE), num witnesses (4 bytes LE)
+/// - Section 2: witness data — `n8` bytes per element
+pub fn parse_witness_file(path: &Path) -> Result {
+ let buffer = std::fs::read(path)?;
+ parse_witness_bytes(&buffer)
+}
+
+/// Parse witness data from a byte buffer.
+pub fn parse_witness_bytes(buffer: &[u8]) -> Result {
+ parse_witness_to_elems(buffer, BigInt::from_signed_bytes_le)
+ .map_err(|e| CoreError::InvalidWitness(e.to_string()))
+}
+
+/// Generic witness parser that accepts a conversion function for each field element chunk.
+pub fn parse_witness_to_elems(
+ buffer: &[u8],
+ chunk_to_elem: impl Fn(&[u8]) -> T,
+) -> std::io::Result> {
+ let mut pos = 0;
+
+ // Magic bytes: "wtns"
+ if buffer.len() < 12 || &buffer[pos..pos + 4] != b"wtns" {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "invalid witness file: missing 'wtns' magic",
+ ));
+ }
+ pos += 4;
+
+ // Version (4 bytes LE)
+ let _version = u32::from_le_bytes(buffer[pos..pos + 4].try_into().unwrap());
+ pos += 4;
+
+ // Number of sections (4 bytes LE)
+ let n_sections = u32::from_le_bytes(buffer[pos..pos + 4].try_into().unwrap());
+ pos += 4;
+
+ // Bytes per field element, set when we encounter section 1
+ let mut n8: u32 = 0;
+
+ for _ in 0..n_sections {
+ if pos + 12 > buffer.len() {
+ break;
+ }
+
+ let section_id = u32::from_le_bytes(buffer[pos..pos + 4].try_into().unwrap());
+ pos += 4;
+
+ let section_length = u64::from_le_bytes(buffer[pos..pos + 8].try_into().unwrap()) as usize;
+ pos += 8;
+
+ if pos + section_length > buffer.len() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "section exceeds buffer length",
+ ));
+ }
+
+ match section_id {
+ // Section 1: field metadata
+ 1 => {
+ n8 = u32::from_le_bytes(buffer[pos..pos + 4].try_into().unwrap());
+ // Skip the rest (prime q + num witness values)
+ pos += section_length;
+ }
+ // Section 2: witness data
+ 2 => {
+ let elems: Vec = buffer[pos..pos + section_length]
+ .chunks(n8 as usize)
+ .map(&chunk_to_elem)
+ .collect();
+ return Ok(elems);
+ }
+ // Skip unknown sections
+ _ => {
+ pos += section_length;
+ }
+ }
+ }
+
+ Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "witness data section not found",
+ ))
+}
+
+/// Write a witness to a binary `.wtns` file (version 2, BN128 prime).
+///
+/// Uses 32 bytes per field element (n8 = 32), which is standard for BN128/BLS12-381.
+///
+/// TODO: take prime as argument
+pub fn write_witness_file(path: &Path, witness: &Witness) -> Result<()> {
+ let n8: u32 = 32;
+ let prime = prime_value(crate::enums::Prime::Bn128);
+ let prime_bytes = prime.to_bytes_le();
+
+ let witness_count = witness.len() as u32;
+
+ let mut buf = Vec::new();
+
+ // Header
+ buf.write_all(b"wtns")?;
+ buf.write_all(&2u32.to_le_bytes())?; // version
+ buf.write_all(&2u32.to_le_bytes())?; // num sections
+
+ // Section 1: field metadata
+ let section1_len: u64 = 4 + (n8 as u64) + 4; // n8 + prime + witness_count
+ buf.write_all(&1u32.to_le_bytes())?; // section id
+ buf.write_all(§ion1_len.to_le_bytes())?;
+ buf.write_all(&n8.to_le_bytes())?;
+
+ // Prime (padded to n8 bytes)
+ let mut prime_buf = vec![0u8; n8 as usize];
+ let len = prime_bytes.len().min(n8 as usize);
+ prime_buf[..len].copy_from_slice(&prime_bytes[..len]);
+ buf.write_all(&prime_buf)?;
+
+ buf.write_all(&witness_count.to_le_bytes())?;
+
+ // Section 2: witness data
+ let section2_len: u64 = (n8 as u64) * (witness_count as u64);
+ buf.write_all(&2u32.to_le_bytes())?; // section id
+ buf.write_all(§ion2_len.to_le_bytes())?;
+
+ for elem in witness {
+ let mut elem_buf = vec![0u8; n8 as usize];
+ let (_, bytes) = if elem.is_negative() {
+ // Shouldn't happen for valid witnesses, but handle gracefully
+ elem.to_bytes_le()
+ } else {
+ elem.to_bytes_le()
+ };
+ let len = bytes.len().min(n8 as usize);
+ elem_buf[..len].copy_from_slice(&bytes[..len]);
+ buf.write_all(&elem_buf)?;
+ }
+
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ std::fs::write(path, buf)?;
+ Ok(())
+}
diff --git a/crates/circomkit-prove/Cargo.toml b/crates/circomkit-prove/Cargo.toml
new file mode 100644
index 0000000..8ff2ccb
--- /dev/null
+++ b/crates/circomkit-prove/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "circomkit-prove"
+description = "Proving backend trait and implementations for Circomkit"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+circomkit-core.workspace = true
+thiserror.workspace = true
+log.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+num-bigint.workspace = true
+tempfile.workspace = true
+
+# Lambdaworks (optional)
+lambdaworks-math = { workspace = true, optional = true }
+lambdaworks-circom-adapter = { workspace = true, optional = true }
+lambdaworks-groth16 = { workspace = true, optional = true }
+
+# Arkworks (optional)
+ark-circom = { workspace = true, optional = true }
+ark-bn254 = { workspace = true, optional = true }
+ark-groth16 = { workspace = true, optional = true }
+ark-ff = { workspace = true, optional = true }
+ark-ec = { workspace = true, optional = true }
+ark-relations = { workspace = true, optional = true }
+ark-serialize = { workspace = true, optional = true }
+ark-std = { workspace = true, optional = true }
+rand = { workspace = true, optional = true }
+
+[features]
+default = []
+prove-arkworks = [
+ "ark-circom", "ark-bn254", "ark-groth16", "ark-ff", "ark-ec",
+ "ark-relations", "ark-serialize", "ark-std", "rand",
+]
+prove-lambdaworks = ["lambdaworks-math", "lambdaworks-circom-adapter", "lambdaworks-groth16"]
diff --git a/crates/circomkit-prove/src/arkworks/convert.rs b/crates/circomkit-prove/src/arkworks/convert.rs
new file mode 100644
index 0000000..4d71d01
--- /dev/null
+++ b/crates/circomkit-prove/src/arkworks/convert.rs
@@ -0,0 +1,24 @@
+use ark_bn254::{Bn254, Fr};
+use ark_groth16::Proof;
+
+/// Convert an Arkworks Groth16 proof + public inputs to snarkjs-compatible JSON.
+pub fn proof_to_snarkjs_json(
+ proof: &Proof,
+ public_inputs: &[Fr],
+) -> (serde_json::Value, Vec) {
+ let proof_json = serde_json::json!({
+ "pi_a": [proof.a.x.to_string(), proof.a.y.to_string(), "1"],
+ "pi_b": [
+ [proof.b.x.c0.to_string(), proof.b.x.c1.to_string()],
+ [proof.b.y.c0.to_string(), proof.b.y.c1.to_string()],
+ ["1", "0"]
+ ],
+ "pi_c": [proof.c.x.to_string(), proof.c.y.to_string(), "1"],
+ "protocol": "groth16",
+ "curve": "bn128"
+ });
+
+ let public_signals: Vec = public_inputs.iter().map(|s| s.to_string()).collect();
+
+ (proof_json, public_signals)
+}
diff --git a/crates/circomkit-prove/src/arkworks/core.rs b/crates/circomkit-prove/src/arkworks/core.rs
new file mode 100644
index 0000000..72d66c8
--- /dev/null
+++ b/crates/circomkit-prove/src/arkworks/core.rs
@@ -0,0 +1,68 @@
+use std::fs::File;
+use std::io::BufReader;
+use std::path::Path;
+
+use ark_bn254::{Bn254, Fr};
+use ark_circom::circom::{R1CS, R1CSFile};
+use ark_circom::{CircomCircuit, CircomReduction, read_zkey};
+use ark_ff::PrimeField;
+use ark_groth16::{Groth16, Proof, ProvingKey};
+use ark_relations::gr1cs::SynthesisError;
+use ark_serialize::SerializationError;
+use rand::thread_rng;
+
+use crate::error::ProveError;
+
+/// Load R1CS from a binary `.r1cs` file.
+pub fn load_r1cs(r1cs_path: &Path) -> Result, ProveError> {
+ let reader = BufReader::new(File::open(r1cs_path)?);
+ R1CSFile::new(reader)
+ .map(Into::into)
+ .map_err(|e: SerializationError| {
+ ProveError::ProvingFailed(format!("failed to load R1CS: {e}"))
+ })
+}
+
+/// Load a proving key from a `.zkey` file.
+pub fn load_proving_key(pkey_path: &Path) -> Result, ProveError> {
+ let mut reader = BufReader::new(File::open(pkey_path)?);
+ let (params, _) = read_zkey(&mut reader)
+ .map_err(|e| ProveError::ProvingFailed(format!("failed to load zkey: {e}")))?;
+ Ok(params)
+}
+
+/// Load a witness from a binary `.wtns` file.
+pub fn load_witness(wtns_path: &Path) -> Result, ProveError> {
+ let data = std::fs::read(wtns_path)?;
+ circomkit_core::utils::parse_witness_to_elems(&data, F::from_le_bytes_mod_order)
+ .map_err(|e| ProveError::ProvingFailed(format!("failed to load witness: {e}")))
+}
+
+/// Create a Groth16 proof from a circuit and proving key.
+pub fn prove_circuit(
+ circuit: CircomCircuit,
+ pkey: &ProvingKey,
+) -> Result, ProveError> {
+ Groth16::::create_random_proof_with_reduction(
+ circuit,
+ pkey,
+ &mut thread_rng(),
+ )
+ .map_err(|e: SynthesisError| ProveError::ProvingFailed(format!("proof generation failed: {e}")))
+}
+
+/// Verify a Groth16 proof.
+pub fn verify(
+ proof: &Proof,
+ public_inputs: &[Fr],
+ proving_key: &ProvingKey,
+) -> Result {
+ Groth16::::verify_proof(
+ &ark_groth16::prepare_verifying_key(&proving_key.vk),
+ proof,
+ public_inputs,
+ )
+ .map_err(|e: SynthesisError| {
+ ProveError::VerificationFailed(format!("verification failed: {e}"))
+ })
+}
diff --git a/crates/circomkit-prove/src/arkworks/mod.rs b/crates/circomkit-prove/src/arkworks/mod.rs
new file mode 100644
index 0000000..97f9b4a
--- /dev/null
+++ b/crates/circomkit-prove/src/arkworks/mod.rs
@@ -0,0 +1,97 @@
+mod convert;
+mod core;
+
+use std::path::Path;
+
+use ark_bn254::Fr;
+use ark_circom::CircomCircuit;
+
+use circomkit_core::types::CircuitSignals;
+
+use crate::error::ProveError;
+use crate::traits::ProvingBackend;
+use crate::types::ProofOutput;
+
+use self::convert::proof_to_snarkjs_json;
+use self::core::{load_proving_key, load_r1cs, load_witness, prove_circuit, verify};
+
+/// Native Groth16 proving backend using Arkworks (BN254).
+///
+/// Supports proving with existing witnesses and proving keys.
+/// Setup is not yet supported natively — use `SnarkjsBackend` for setup.
+pub struct ArkworksBackend;
+
+impl ProvingBackend for ArkworksBackend {
+ fn capabilities(&self) -> crate::capabilities::BackendCapabilities {
+ crate::capabilities::capabilities_for(circomkit_core::enums::ProvingBackendKind::Arkworks)
+ }
+
+ fn full_prove(
+ &self,
+ _input: &CircuitSignals,
+ _wasm_path: &Path,
+ _pkey_path: &Path,
+ ) -> Result {
+ // full_prove requires witness calculation, which should be done
+ // by the caller using circomkit-witness, then calling `prove()`.
+ Err(ProveError::ProvingFailed(
+ "ArkworksBackend does not support full_prove; use circomkit-witness \
+ for witness calculation, then call prove() with the witness file"
+ .to_string(),
+ ))
+ }
+
+ fn prove(
+ &self,
+ witness_path: &Path,
+ r1cs_path: &Path,
+ pkey_path: &Path,
+ ) -> Result {
+ let wtns: Vec = load_witness(witness_path)?;
+ let pkey = load_proving_key(pkey_path)?;
+
+ let mut r1cs = load_r1cs(r1cs_path)?;
+ // Disable wire mapping for WASM-generated witnesses.
+ // See: https://github.com/arkworks-rs/circom-compat/blob/master/src/circom/builder.rs#L82
+ r1cs.wire_mapping = None;
+
+ let circom = CircomCircuit {
+ r1cs,
+ witness: Some(wtns),
+ };
+
+ let public_inputs = circom
+ .get_public_inputs()
+ .ok_or_else(|| ProveError::ProvingFailed("could not extract public inputs".into()))?;
+
+ let proof = prove_circuit(circom, &pkey)?;
+
+ debug_assert!(
+ verify(&proof, &public_inputs, &pkey).unwrap_or(false),
+ "proof verification failed internally"
+ );
+
+ let (proof_json, public_signals) = proof_to_snarkjs_json(&proof, &public_inputs);
+
+ Ok(ProofOutput {
+ proof: proof_json,
+ public_signals,
+ })
+ }
+
+ fn verify(
+ &self,
+ vkey: &serde_json::Value,
+ public_signals: &[String],
+ proof: &serde_json::Value,
+ ) -> Result {
+ // For native verification we'd need to deserialize the snarkjs JSON back
+ // into Arkworks types. For now, delegate to snarkjs for verification.
+ let _ = (vkey, public_signals, proof);
+ Err(ProveError::VerificationFailed(
+ "native Arkworks verification from snarkjs JSON not yet implemented; \
+ use SnarkjsBackend for verification"
+ .to_string(),
+ ))
+ }
+}
diff --git a/crates/circomkit-prove/src/capabilities.rs b/crates/circomkit-prove/src/capabilities.rs
new file mode 100644
index 0000000..7303b3c
--- /dev/null
+++ b/crates/circomkit-prove/src/capabilities.rs
@@ -0,0 +1,163 @@
+//! Backend capability matrix.
+//!
+//! Each proving backend supports a different slice of the (protocol, curve)
+//! space. snarkjs is the universal backend (all protocols, all primes), while
+//! the native backends are specialized:
+//!
+//! | backend | protocols | curves | setup | native verify |
+//! |-------------|--------------------------|------------------|--------------|---------------|
+//! | snarkjs | groth16, plonk, fflonk | all 7 primes | yes | yes |
+//! | lambdaworks | groth16 | bls12381 | on-the-fly | no |
+//! | arkworks | groth16 | bn128 (BN254) | no (zkey) | no |
+//!
+//! These tables are the single source of truth used both to pick a backend
+//! ([`crate::make_proving_backend`]) and to produce clear errors for
+//! unsupported combinations.
+
+use circomkit_core::enums::{Prime, Protocol, ProvingBackendKind};
+
+use crate::error::ProveError;
+
+/// Describes which proving features a backend supports.
+#[derive(Debug, Clone)]
+pub struct BackendCapabilities {
+ /// Human-readable backend name (matches [`ProvingBackendKind`]).
+ pub name: &'static str,
+ /// Protocols this backend can prove.
+ pub protocols: &'static [Protocol],
+ /// Primes (curves) this backend supports.
+ pub primes: &'static [Prime],
+ /// Whether the backend can run its own trusted setup.
+ pub supports_setup: bool,
+ /// Whether the backend can verify a snarkjs-format proof natively.
+ pub supports_native_verify: bool,
+ /// Whether the backend consumes a `.zkey` proving key
+ /// (`false` means it performs an on-the-fly setup instead).
+ pub uses_zkey: bool,
+}
+
+impl BackendCapabilities {
+ /// Returns `true` if the backend supports the given protocol and curve.
+ pub fn supports(&self, protocol: Protocol, prime: Prime) -> bool {
+ self.protocols.contains(&protocol) && self.primes.contains(&prime)
+ }
+
+ /// Returns an error describing exactly why a (protocol, curve) combination
+ /// is not supported, or `Ok(())` if it is.
+ pub fn ensure_supports(&self, protocol: Protocol, prime: Prime) -> Result<(), ProveError> {
+ if !self.protocols.contains(&protocol) {
+ return Err(ProveError::UnsupportedProtocol {
+ backend: self.name,
+ protocol,
+ supported: join_display(self.protocols),
+ });
+ }
+ if !self.primes.contains(&prime) {
+ return Err(ProveError::UnsupportedCurve {
+ backend: self.name,
+ prime,
+ supported: join_display(self.primes),
+ });
+ }
+ Ok(())
+ }
+}
+
+/// All primes snarkjs can prove over.
+const ALL_PRIMES: &[Prime] = &[
+ Prime::Bn128,
+ Prime::Bls12381,
+ Prime::Goldilocks,
+ Prime::Grumpkin,
+ Prime::Pallas,
+ Prime::Vesta,
+ Prime::Secq256r1,
+];
+
+/// Static capability table for a backend kind.
+///
+/// This is available without constructing the backend, so a capability check
+/// can run even when the corresponding backend feature is compiled out.
+pub fn capabilities_for(kind: ProvingBackendKind) -> BackendCapabilities {
+ match kind {
+ ProvingBackendKind::Snarkjs => BackendCapabilities {
+ name: "snarkjs",
+ protocols: &[Protocol::Groth16, Protocol::Plonk, Protocol::Fflonk],
+ primes: ALL_PRIMES,
+ supports_setup: true,
+ supports_native_verify: true,
+ uses_zkey: true,
+ },
+ ProvingBackendKind::Lambdaworks => BackendCapabilities {
+ name: "lambdaworks",
+ protocols: &[Protocol::Groth16],
+ primes: &[Prime::Bls12381],
+ supports_setup: true,
+ supports_native_verify: false,
+ uses_zkey: false,
+ },
+ ProvingBackendKind::Arkworks => BackendCapabilities {
+ name: "arkworks",
+ protocols: &[Protocol::Groth16],
+ primes: &[Prime::Bn128],
+ supports_setup: false,
+ supports_native_verify: false,
+ uses_zkey: true,
+ },
+ }
+}
+
+fn join_display(items: &[T]) -> String {
+ items
+ .iter()
+ .map(ToString::to_string)
+ .collect::>()
+ .join(", ")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn snarkjs_supports_everything() {
+ let caps = capabilities_for(ProvingBackendKind::Snarkjs);
+ assert!(caps.supports(Protocol::Groth16, Prime::Bn128));
+ assert!(caps.supports(Protocol::Plonk, Prime::Bls12381));
+ assert!(caps.supports(Protocol::Fflonk, Prime::Vesta));
+ assert!(
+ caps.ensure_supports(Protocol::Plonk, Prime::Goldilocks)
+ .is_ok()
+ );
+ }
+
+ #[test]
+ fn lambdaworks_is_groth16_bls12381_only() {
+ let caps = capabilities_for(ProvingBackendKind::Lambdaworks);
+ assert!(caps.supports(Protocol::Groth16, Prime::Bls12381));
+ // wrong protocol
+ assert!(matches!(
+ caps.ensure_supports(Protocol::Plonk, Prime::Bls12381),
+ Err(ProveError::UnsupportedProtocol { .. })
+ ));
+ // wrong curve
+ assert!(matches!(
+ caps.ensure_supports(Protocol::Groth16, Prime::Bn128),
+ Err(ProveError::UnsupportedCurve { .. })
+ ));
+ }
+
+ #[test]
+ fn arkworks_is_groth16_bn128_only() {
+ let caps = capabilities_for(ProvingBackendKind::Arkworks);
+ assert!(caps.supports(Protocol::Groth16, Prime::Bn128));
+ assert!(matches!(
+ caps.ensure_supports(Protocol::Groth16, Prime::Bls12381),
+ Err(ProveError::UnsupportedCurve { .. })
+ ));
+ assert!(matches!(
+ caps.ensure_supports(Protocol::Fflonk, Prime::Bn128),
+ Err(ProveError::UnsupportedProtocol { .. })
+ ));
+ }
+}
diff --git a/crates/circomkit-prove/src/error.rs b/crates/circomkit-prove/src/error.rs
new file mode 100644
index 0000000..8568e4e
--- /dev/null
+++ b/crates/circomkit-prove/src/error.rs
@@ -0,0 +1,48 @@
+use circomkit_core::enums::{Prime, Protocol};
+
+/// Errors that can occur during proving operations.
+#[derive(Debug, thiserror::Error)]
+pub enum ProveError {
+ #[error("proving failed: {0}")]
+ ProvingFailed(String),
+
+ #[error("verification failed: {0}")]
+ VerificationFailed(String),
+
+ #[error("setup failed: {0}")]
+ SetupFailed(String),
+
+ #[error("snarkjs subprocess failed: {0}")]
+ SnarkjsError(String),
+
+ #[error("backend `{backend}` does not support protocol `{protocol}` (supported: {supported})")]
+ UnsupportedProtocol {
+ backend: &'static str,
+ protocol: Protocol,
+ supported: String,
+ },
+
+ #[error("backend `{backend}` does not support curve `{prime}` (supported: {supported})")]
+ UnsupportedCurve {
+ backend: &'static str,
+ prime: Prime,
+ supported: String,
+ },
+
+ #[error(
+ "backend `{backend}` is not enabled; rebuild circomkit-prove with the `{feature}` feature"
+ )]
+ BackendNotEnabled {
+ backend: &'static str,
+ feature: &'static str,
+ },
+
+ #[error(transparent)]
+ Core(#[from] circomkit_core::error::CoreError),
+
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+
+ #[error(transparent)]
+ Json(#[from] serde_json::Error),
+}
diff --git a/crates/circomkit-prove/src/lambdaworks/convert.rs b/crates/circomkit-prove/src/lambdaworks/convert.rs
new file mode 100644
index 0000000..3787a54
--- /dev/null
+++ b/crates/circomkit-prove/src/lambdaworks/convert.rs
@@ -0,0 +1,55 @@
+use lambdaworks_groth16::Proof;
+use lambdaworks_math::field::{element::FieldElement, traits::IsPrimeField};
+use num_bigint::BigUint;
+
+/// Convert a Lambdaworks `UnsignedInteger` hex representation to a decimal string.
+///
+/// `representative().to_string()` returns `0x...` hex; snarkjs expects decimal.
+fn to_decimal(hex_str: &str) -> String {
+ let hex = hex_str.strip_prefix("0x").unwrap_or(hex_str);
+ if hex.is_empty() || hex.chars().all(|c| c == '0') {
+ return "0".to_string();
+ }
+ BigUint::parse_bytes(hex.as_bytes(), 16)
+ .expect("invalid hex from representative()")
+ .to_string()
+}
+
+/// Convert a field element's representative to a decimal string.
+fn field_to_dec(elem: &FieldElement) -> String {
+ to_decimal(&elem.representative().to_string())
+}
+
+/// Convert a Lambdaworks Groth16 proof to snarkjs-compatible JSON.
+pub fn proof_to_snarkjs_json(proof: &Proof) -> serde_json::Value {
+ serde_json::json!({
+ "pi_a": [
+ field_to_dec(&proof.pi1.x()),
+ field_to_dec(&proof.pi1.y()),
+ "1"
+ ],
+ "pi_b": [
+ [
+ field_to_dec(&proof.pi2.x().value()[0]),
+ field_to_dec(&proof.pi2.x().value()[1]),
+ ],
+ [
+ field_to_dec(&proof.pi2.y().value()[0]),
+ field_to_dec(&proof.pi2.y().value()[1]),
+ ],
+ ["1", "0"]
+ ],
+ "pi_c": [
+ field_to_dec(&proof.pi3.x()),
+ field_to_dec(&proof.pi3.y()),
+ "1"
+ ],
+ "protocol": "groth16",
+ "curve": "bls12381"
+ })
+}
+
+/// Convert Lambdaworks field elements to decimal public signal strings.
+pub fn public_inputs_to_strings(public_inputs: &[FieldElement]) -> Vec {
+ public_inputs.iter().map(|s| field_to_dec(s)).collect()
+}
diff --git a/crates/circomkit-prove/src/lambdaworks/mod.rs b/crates/circomkit-prove/src/lambdaworks/mod.rs
new file mode 100644
index 0000000..49f3386
--- /dev/null
+++ b/crates/circomkit-prove/src/lambdaworks/mod.rs
@@ -0,0 +1,143 @@
+mod convert;
+
+use std::path::Path;
+
+use lambdaworks_circom_adapter::{CircomR1CS, circom_to_lambda};
+use lambdaworks_groth16::common::FrElement;
+use lambdaworks_math::traits::ByteConversion;
+
+use circomkit_core::types::CircuitSignals;
+use circomkit_core::utils::{parse_witness_to_elems, read_r1cs_file};
+
+use crate::error::ProveError;
+use crate::traits::ProvingBackend;
+use crate::types::ProofOutput;
+
+use self::convert::{proof_to_snarkjs_json, public_inputs_to_strings};
+
+/// Native Groth16 proving backend using Lambdaworks (BLS12-381).
+///
+/// This backend performs trusted setup on-the-fly for each proof, since Lambdaworks
+/// does not yet support loading pre-computed `.zkey` files.
+///
+/// Supports both binary `.r1cs` and JSON `.r1cs.json` files.
+pub struct LambdaworksBackend;
+
+impl ProvingBackend for LambdaworksBackend {
+ fn capabilities(&self) -> crate::capabilities::BackendCapabilities {
+ crate::capabilities::capabilities_for(
+ circomkit_core::enums::ProvingBackendKind::Lambdaworks,
+ )
+ }
+
+ fn full_prove(
+ &self,
+ _input: &CircuitSignals,
+ _wasm_path: &Path,
+ _pkey_path: &Path,
+ ) -> Result {
+ Err(ProveError::ProvingFailed(
+ "LambdaworksBackend does not support full_prove; use circomkit-witness \
+ for witness calculation, then call prove() with the witness file"
+ .to_string(),
+ ))
+ }
+
+ fn prove(
+ &self,
+ witness_path: &Path,
+ r1cs_path: &Path,
+ _pkey_path: &Path,
+ ) -> Result {
+ // Load R1CS (binary or JSON)
+ let r1cs = load_r1cs(r1cs_path)?;
+
+ // Load witness (binary .wtns or JSON)
+ let wtns = load_witness(witness_path)?;
+
+ // Convert to Lambdaworks QAP representation
+ let (qap, wtns, pubs) = circom_to_lambda(r1cs, wtns);
+
+ // Trusted setup (on-the-fly, since zkey loading is not supported)
+ log::info!("running Lambdaworks Groth16 setup (on-the-fly)");
+ let (proving_key, verifying_key) = lambdaworks_groth16::setup(&qap);
+
+ // Generate proof
+ let proof = lambdaworks_groth16::Prover::prove(&wtns, &qap, &proving_key);
+
+ // Verify internally in debug builds
+ debug_assert!(
+ lambdaworks_groth16::verify(&verifying_key, &proof, &pubs),
+ "Lambdaworks proof verification failed internally"
+ );
+
+ let proof_json = proof_to_snarkjs_json(&proof);
+ let public_signals = public_inputs_to_strings(&pubs);
+
+ Ok(ProofOutput {
+ proof: proof_json,
+ public_signals,
+ })
+ }
+
+ fn verify(
+ &self,
+ vkey: &serde_json::Value,
+ public_signals: &[String],
+ proof: &serde_json::Value,
+ ) -> Result {
+ let _ = (vkey, public_signals, proof);
+ Err(ProveError::VerificationFailed(
+ "native Lambdaworks verification from snarkjs JSON not yet implemented; \
+ use SnarkjsBackend for verification"
+ .to_string(),
+ ))
+ }
+}
+
+/// Load an R1CS file, supporting both binary `.r1cs` and JSON `.r1cs.json` formats.
+fn load_r1cs(path: &Path) -> Result {
+ let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
+
+ if ext == "json" {
+ // JSON format: use lambdaworks' own reader
+ lambdaworks_circom_adapter::read_circom_r1cs(path).map_err(|e| {
+ ProveError::ProvingFailed(format!(
+ "failed to read R1CS JSON from {}: {e:?}",
+ path.display()
+ ))
+ })
+ } else {
+ // Binary format: parse with our generic reader, convert to CircomR1CS
+ let r1cs = read_r1cs_file(path, |bytes| FrElement::from_bytes_le(bytes).unwrap())?;
+
+ Ok(CircomR1CS {
+ n8: r1cs.n8,
+ prime: r1cs.prime.to_string(),
+ num_vars: r1cs.num_variables,
+ num_outputs: r1cs.num_outputs as usize,
+ num_pub_inputs: r1cs.num_pub_inputs as usize,
+ num_priv_inputs: r1cs.num_priv_inputs as usize,
+ num_labels: r1cs.num_labels as usize,
+ num_constraints: r1cs.num_constraints,
+ constraints: r1cs.constraints,
+ })
+ }
+}
+
+/// Load a witness file, supporting both binary `.wtns` and JSON formats.
+fn load_witness(path: &Path) -> Result, ProveError> {
+ let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
+ if ext == "json" {
+ lambdaworks_circom_adapter::read_circom_witness(path).map_err(|e| {
+ ProveError::ProvingFailed(format!(
+ "failed to read witness JSON from {}: {e:?}",
+ path.display()
+ ))
+ })
+ } else {
+ let data = std::fs::read(path)?;
+ parse_witness_to_elems(&data, |bytes| FrElement::from_bytes_le(bytes).unwrap())
+ .map_err(|e| ProveError::ProvingFailed(format!("failed to parse witness: {e}")))
+ }
+}
diff --git a/crates/circomkit-prove/src/lib.rs b/crates/circomkit-prove/src/lib.rs
new file mode 100644
index 0000000..349ac10
--- /dev/null
+++ b/crates/circomkit-prove/src/lib.rs
@@ -0,0 +1,73 @@
+mod capabilities;
+mod error;
+mod snarkjs;
+mod traits;
+mod types;
+
+#[cfg(feature = "prove-arkworks")]
+mod arkworks;
+
+#[cfg(feature = "prove-lambdaworks")]
+mod lambdaworks;
+
+pub use capabilities::{BackendCapabilities, capabilities_for};
+pub use error::ProveError;
+pub use snarkjs::SnarkjsBackend;
+pub use traits::{ProvingBackend, SetupBackend};
+pub use types::ProofOutput;
+
+#[cfg(feature = "prove-arkworks")]
+pub use arkworks::ArkworksBackend;
+
+#[cfg(feature = "prove-lambdaworks")]
+pub use lambdaworks::LambdaworksBackend;
+
+use circomkit_core::enums::{Prime, Protocol, ProvingBackendKind};
+
+/// Construct a proving backend for a `(backend, protocol, curve)` request.
+///
+/// The capability matrix is checked first, so an unsupported protocol/curve
+/// combination produces a precise [`ProveError::UnsupportedProtocol`] /
+/// [`ProveError::UnsupportedCurve`] regardless of which backend features are
+/// compiled in. If the combination is valid but the backend's feature is not
+/// enabled, [`ProveError::BackendNotEnabled`] is returned instead.
+pub fn make_proving_backend(
+ kind: ProvingBackendKind,
+ protocol: Protocol,
+ prime: Prime,
+) -> Result, ProveError> {
+ // Capability check is always available, even for compiled-out backends.
+ capabilities_for(kind).ensure_supports(protocol, prime)?;
+
+ match kind {
+ ProvingBackendKind::Snarkjs => Ok(Box::new(SnarkjsBackend::new("snarkjs", protocol))),
+
+ ProvingBackendKind::Lambdaworks => {
+ #[cfg(feature = "prove-lambdaworks")]
+ {
+ Ok(Box::new(LambdaworksBackend))
+ }
+ #[cfg(not(feature = "prove-lambdaworks"))]
+ {
+ Err(ProveError::BackendNotEnabled {
+ backend: "lambdaworks",
+ feature: "prove-lambdaworks",
+ })
+ }
+ }
+
+ ProvingBackendKind::Arkworks => {
+ #[cfg(feature = "prove-arkworks")]
+ {
+ Ok(Box::new(ArkworksBackend))
+ }
+ #[cfg(not(feature = "prove-arkworks"))]
+ {
+ Err(ProveError::BackendNotEnabled {
+ backend: "arkworks",
+ feature: "prove-arkworks",
+ })
+ }
+ }
+ }
+}
diff --git a/crates/circomkit-prove/src/snarkjs/mod.rs b/crates/circomkit-prove/src/snarkjs/mod.rs
new file mode 100644
index 0000000..c2e56c5
--- /dev/null
+++ b/crates/circomkit-prove/src/snarkjs/mod.rs
@@ -0,0 +1,261 @@
+use std::path::Path;
+use std::process::Command;
+
+use circomkit_core::config::ProverConfig;
+use circomkit_core::enums::Protocol;
+use circomkit_core::types::CircuitSignals;
+
+use crate::error::ProveError;
+use crate::traits::{ProvingBackend, SetupBackend};
+use crate::types::ProofOutput;
+
+/// Proving backend that delegates to the `snarkjs` CLI via subprocess.
+pub struct SnarkjsBackend {
+ /// Command to invoke snarkjs (e.g. "snarkjs" or "npx snarkjs").
+ snarkjs_cmd: String,
+ /// Protocol to use for proving.
+ protocol: Protocol,
+}
+
+impl Default for SnarkjsBackend {
+ fn default() -> Self {
+ Self {
+ snarkjs_cmd: "snarkjs".to_string(),
+ protocol: Protocol::Groth16,
+ }
+ }
+}
+
+impl SnarkjsBackend {
+ pub fn new(cmd: impl Into, protocol: Protocol) -> Self {
+ Self {
+ snarkjs_cmd: cmd.into(),
+ protocol,
+ }
+ }
+
+ fn protocol_cmd(&self) -> &str {
+ match self.protocol {
+ Protocol::Groth16 => "groth16",
+ Protocol::Plonk => "plonk",
+ Protocol::Fflonk => "fflonk",
+ }
+ }
+
+ fn run(&self, args: &[&str]) -> Result {
+ log::debug!("{} {}", self.snarkjs_cmd, args.join(" "));
+
+ let output = Command::new(&self.snarkjs_cmd)
+ .args(args)
+ .output()
+ .map_err(|e| ProveError::SnarkjsError(format!("failed to run snarkjs: {e}")))?;
+
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ return Err(ProveError::SnarkjsError(format!(
+ "snarkjs exited with {}:\nstdout: {stdout}\nstderr: {stderr}",
+ output.status
+ )));
+ }
+
+ Ok(String::from_utf8_lossy(&output.stdout).to_string())
+ }
+}
+
+impl ProvingBackend for SnarkjsBackend {
+ fn capabilities(&self) -> crate::capabilities::BackendCapabilities {
+ crate::capabilities::capabilities_for(circomkit_core::enums::ProvingBackendKind::Snarkjs)
+ }
+
+ fn full_prove(
+ &self,
+ input: &CircuitSignals,
+ wasm_path: &Path,
+ pkey_path: &Path,
+ ) -> Result {
+ let tmp_dir = tempfile::tempdir()?;
+ let input_path = tmp_dir.path().join("input.json");
+ let proof_path = tmp_dir.path().join("proof.json");
+ let public_path = tmp_dir.path().join("public.json");
+
+ std::fs::write(&input_path, serde_json::to_string(input)?)?;
+
+ self.run(&[
+ self.protocol_cmd(),
+ "fullprove",
+ &input_path.to_string_lossy(),
+ &wasm_path.to_string_lossy(),
+ &pkey_path.to_string_lossy(),
+ &proof_path.to_string_lossy(),
+ &public_path.to_string_lossy(),
+ ])?;
+
+ let proof: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(&proof_path)?)?;
+ let public_signals: Vec =
+ serde_json::from_str(&std::fs::read_to_string(&public_path)?)?;
+
+ Ok(ProofOutput {
+ proof,
+ public_signals,
+ })
+ }
+
+ fn prove(
+ &self,
+ witness_path: &Path,
+ _r1cs_path: &Path,
+ pkey_path: &Path,
+ ) -> Result {
+ let tmp_dir = tempfile::tempdir()?;
+ let proof_path = tmp_dir.path().join("proof.json");
+ let public_path = tmp_dir.path().join("public.json");
+
+ self.run(&[
+ self.protocol_cmd(),
+ "prove",
+ &pkey_path.to_string_lossy(),
+ &witness_path.to_string_lossy(),
+ &proof_path.to_string_lossy(),
+ &public_path.to_string_lossy(),
+ ])?;
+
+ let proof: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(&proof_path)?)?;
+ let public_signals: Vec =
+ serde_json::from_str(&std::fs::read_to_string(&public_path)?)?;
+
+ Ok(ProofOutput {
+ proof,
+ public_signals,
+ })
+ }
+
+ fn verify(
+ &self,
+ vkey: &serde_json::Value,
+ public_signals: &[String],
+ proof: &serde_json::Value,
+ ) -> Result {
+ let tmp_dir = tempfile::tempdir()?;
+ let vkey_path = tmp_dir.path().join("vkey.json");
+ let proof_path = tmp_dir.path().join("proof.json");
+ let public_path = tmp_dir.path().join("public.json");
+
+ std::fs::write(&vkey_path, serde_json::to_string(vkey)?)?;
+ std::fs::write(&proof_path, serde_json::to_string(proof)?)?;
+ std::fs::write(&public_path, serde_json::to_string(public_signals)?)?;
+
+ // For verify, snarkjs exits 1 on invalid proof — that's not an error,
+ // it means the proof is rejected. We check stdout for "OK!" instead.
+ let output = Command::new(&self.snarkjs_cmd)
+ .args([
+ self.protocol_cmd(),
+ "verify",
+ &vkey_path.to_string_lossy(),
+ &public_path.to_string_lossy(),
+ &proof_path.to_string_lossy(),
+ ])
+ .output()
+ .map_err(|e| ProveError::SnarkjsError(format!("failed to run snarkjs: {e}")))?;
+
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ Ok(stdout.contains("OK!"))
+ }
+}
+
+impl SetupBackend for SnarkjsBackend {
+ fn setup(
+ &self,
+ r1cs_path: &Path,
+ ptau_path: &Path,
+ pkey_path: &Path,
+ config: &ProverConfig,
+ ) -> Result<(), ProveError> {
+ match config.protocol {
+ Protocol::Groth16 => {
+ let tmp_dir = tempfile::tempdir()?;
+ let mut current_zkey = tmp_dir.path().join("circuit_0.zkey");
+
+ // snarkjs groth16 setup is actually "zkey new"
+ self.run(&[
+ "groth16",
+ "setup",
+ &r1cs_path.to_string_lossy(),
+ &ptau_path.to_string_lossy(),
+ ¤t_zkey.to_string_lossy(),
+ ])?;
+
+ // Phase-2 contributions
+ for i in 0..config.groth16.num_contributions {
+ let next_zkey = tmp_dir.path().join(format!("circuit_{}.zkey", i + 1));
+ let name = format!("contribution_{}", i + 1);
+
+ self.run(&[
+ "zkey",
+ "contribute",
+ ¤t_zkey.to_string_lossy(),
+ &next_zkey.to_string_lossy(),
+ &format!("--name={name}"),
+ "-e=random_entropy",
+ ])?;
+
+ current_zkey = next_zkey;
+ }
+
+ if let Some(parent) = pkey_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ std::fs::rename(¤t_zkey, pkey_path)?;
+ }
+ Protocol::Plonk | Protocol::Fflonk => {
+ if let Some(parent) = pkey_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ self.run(&[
+ self.protocol_cmd(),
+ "setup",
+ &r1cs_path.to_string_lossy(),
+ &ptau_path.to_string_lossy(),
+ &pkey_path.to_string_lossy(),
+ ])?;
+ }
+ }
+
+ Ok(())
+ }
+
+ fn export_vkey(&self, pkey_path: &Path, vkey_path: &Path) -> Result<(), ProveError> {
+ if let Some(parent) = vkey_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ self.run(&[
+ "zkey",
+ "export",
+ "verificationkey",
+ &pkey_path.to_string_lossy(),
+ &vkey_path.to_string_lossy(),
+ ])?;
+ Ok(())
+ }
+
+ fn export_contract(
+ &self,
+ pkey_path: &Path,
+ sol_path: &Path,
+ _protocol: Protocol,
+ ) -> Result<(), ProveError> {
+ if let Some(parent) = sol_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ self.run(&[
+ "zkey",
+ "export",
+ "solidityverifier",
+ &pkey_path.to_string_lossy(),
+ &sol_path.to_string_lossy(),
+ ])?;
+ Ok(())
+ }
+}
diff --git a/crates/circomkit-prove/src/traits.rs b/crates/circomkit-prove/src/traits.rs
new file mode 100644
index 0000000..dcada1a
--- /dev/null
+++ b/crates/circomkit-prove/src/traits.rs
@@ -0,0 +1,62 @@
+use std::path::Path;
+
+use circomkit_core::config::ProverConfig;
+use circomkit_core::enums::Protocol;
+use circomkit_core::types::CircuitSignals;
+
+use crate::capabilities::BackendCapabilities;
+use crate::error::ProveError;
+use crate::types::ProofOutput;
+
+/// Trait for proving backends.
+pub trait ProvingBackend {
+ /// Describe which protocols and curves this backend supports.
+ fn capabilities(&self) -> BackendCapabilities;
+
+ /// Generate a proof from circuit inputs (full pipeline: witness + prove).
+ fn full_prove(
+ &self,
+ input: &CircuitSignals,
+ wasm_path: &Path,
+ pkey_path: &Path,
+ ) -> Result;
+
+ /// Generate a proof from a pre-computed witness file.
+ fn prove(
+ &self,
+ witness_path: &Path,
+ r1cs_path: &Path,
+ pkey_path: &Path,
+ ) -> Result;
+
+ /// Verify a proof against public signals and a verification key.
+ fn verify(
+ &self,
+ vkey: &serde_json::Value,
+ public_signals: &[String],
+ proof: &serde_json::Value,
+ ) -> Result;
+}
+
+/// Trait for trusted setup operations.
+pub trait SetupBackend {
+ /// Run trusted setup (phase-2 ceremony for Groth16, or direct setup for PLONK/FFLONK).
+ fn setup(
+ &self,
+ r1cs_path: &Path,
+ ptau_path: &Path,
+ pkey_path: &Path,
+ config: &ProverConfig,
+ ) -> Result<(), ProveError>;
+
+ /// Export a verification key from a proving key.
+ fn export_vkey(&self, pkey_path: &Path, vkey_path: &Path) -> Result<(), ProveError>;
+
+ /// Export a Solidity verifier contract from a proving key.
+ fn export_contract(
+ &self,
+ pkey_path: &Path,
+ sol_path: &Path,
+ protocol: Protocol,
+ ) -> Result<(), ProveError>;
+}
diff --git a/crates/circomkit-prove/src/types.rs b/crates/circomkit-prove/src/types.rs
new file mode 100644
index 0000000..7eb4b89
--- /dev/null
+++ b/crates/circomkit-prove/src/types.rs
@@ -0,0 +1,11 @@
+use serde::{Deserialize, Serialize};
+
+/// A proof output in snarkjs-compatible format.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProofOutput {
+ /// Protocol-specific proof JSON (Groth16, PLONK, or FFLONK format).
+ pub proof: serde_json::Value,
+ /// Public signals as decimal strings.
+ pub public_signals: Vec,
+}
diff --git a/crates/circomkit-prove/tests/arkworks_integration.rs b/crates/circomkit-prove/tests/arkworks_integration.rs
new file mode 100644
index 0000000..28cd0f2
--- /dev/null
+++ b/crates/circomkit-prove/tests/arkworks_integration.rs
@@ -0,0 +1,64 @@
+#![cfg(feature = "prove-arkworks")]
+
+use std::path::PathBuf;
+
+use circomkit_prove::{ArkworksBackend, ProofOutput, ProvingBackend};
+
+fn test_data_dir() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/data/multiplier_30")
+}
+
+#[test]
+fn arkworks_prove_with_binary_r1cs() {
+ let backend = ArkworksBackend;
+ let data = test_data_dir();
+
+ let ProofOutput {
+ proof,
+ public_signals,
+ } = backend
+ .prove(
+ &data.join("witness.wtns"),
+ &data.join("multiplier_30.r1cs"),
+ &data.join("groth16_pkey.zkey"),
+ )
+ .expect("prove with binary r1cs should succeed");
+
+ // Unlike Lambdaworks, Arkworks reports only the circuit's public output:
+ // 2^30 = 1073741824.
+ assert_eq!(public_signals, vec!["1073741824"]);
+
+ assert_eq!(proof["protocol"], "groth16");
+ assert_eq!(proof["curve"], "bn128");
+}
+
+#[test]
+fn arkworks_full_prove_unsupported() {
+ let backend = ArkworksBackend;
+ let data = test_data_dir();
+
+ let input = circomkit_core::signals! {
+ "in" => vec![2_i64; 30],
+ };
+
+ // Arkworks proves from a pre-computed witness; it does not run the witness
+ // calculator itself, so `full_prove` is not supported.
+ let result = backend.full_prove(
+ &input,
+ &data.join("multiplier_30.wasm"),
+ &data.join("groth16_pkey.zkey"),
+ );
+
+ assert!(result.is_err());
+}
+
+#[test]
+fn arkworks_verify_from_json_unsupported() {
+ let backend = ArkworksBackend;
+
+ // Native verification from snarkjs-format JSON is not implemented yet;
+ // verification should be delegated to snarkjs.
+ let result = backend.verify(&serde_json::json!({}), &[], &serde_json::json!({}));
+
+ assert!(result.is_err());
+}
diff --git a/crates/circomkit-prove/tests/backend_matrix.rs b/crates/circomkit-prove/tests/backend_matrix.rs
new file mode 100644
index 0000000..d121d22
--- /dev/null
+++ b/crates/circomkit-prove/tests/backend_matrix.rs
@@ -0,0 +1,226 @@
+//! Cross-backend / cross-curve proving matrix.
+//!
+//! Two concerns are covered:
+//!
+//! 1. **Capability matrix** — every `(backend, protocol, curve)` request is
+//! routed through [`make_proving_backend`], which must either return a
+//! backend (supported, feature enabled), a `BackendNotEnabled` error
+//! (supported, feature off), or a precise `UnsupportedProtocol` /
+//! `UnsupportedCurve` error (unsupported). This part always runs.
+//!
+//! 2. **Real proofs** — for the combinations we have fixtures for, an actual
+//! proof is generated with each backend on its native curve. These parts are
+//! gated behind the relevant `prove-*` features (and snarkjs needs the
+//! `snarkjs` CLI on PATH).
+
+use std::path::PathBuf;
+
+use circomkit_core::enums::{Prime, Protocol, ProvingBackendKind};
+use circomkit_prove::{ProveError, capabilities_for, make_proving_backend};
+
+fn test_data_dir() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/data/multiplier_30")
+}
+
+const ALL_BACKENDS: &[ProvingBackendKind] = &[
+ ProvingBackendKind::Snarkjs,
+ ProvingBackendKind::Lambdaworks,
+ ProvingBackendKind::Arkworks,
+];
+
+const ALL_PROTOCOLS: &[Protocol] = &[Protocol::Groth16, Protocol::Plonk, Protocol::Fflonk];
+
+const ALL_PRIMES: &[Prime] = &[
+ Prime::Bn128,
+ Prime::Bls12381,
+ Prime::Goldilocks,
+ Prime::Grumpkin,
+ Prime::Pallas,
+ Prime::Vesta,
+ Prime::Secq256r1,
+];
+
+/// Walk the entire grid and assert `make_proving_backend` agrees with the
+/// capability table: unsupported combos error with a capability error,
+/// supported combos either build or report the feature is disabled.
+#[test]
+fn capability_matrix_is_enforced() {
+ for &kind in ALL_BACKENDS {
+ let caps = capabilities_for(kind);
+ for &protocol in ALL_PROTOCOLS {
+ for &prime in ALL_PRIMES {
+ let result = make_proving_backend(kind, protocol, prime);
+ let supported = caps.supports(protocol, prime);
+
+ match result {
+ Ok(backend) => {
+ assert!(
+ supported,
+ "{kind:?} built a backend for an unsupported ({protocol}, {prime})"
+ );
+ // The constructed backend must report the same support.
+ assert!(backend.capabilities().supports(protocol, prime));
+ }
+ Err(ProveError::BackendNotEnabled { .. }) => {
+ // Only valid when the combo is genuinely supported but
+ // the backend feature is compiled out.
+ assert!(
+ supported,
+ "{kind:?} reported BackendNotEnabled for unsupported ({protocol}, {prime})"
+ );
+ }
+ Err(ProveError::UnsupportedProtocol { .. }) => {
+ assert!(
+ !caps.protocols.contains(&protocol),
+ "{kind:?} rejected protocol {protocol} that it claims to support"
+ );
+ }
+ Err(ProveError::UnsupportedCurve { .. }) => {
+ assert!(
+ caps.protocols.contains(&protocol) && !caps.primes.contains(&prime),
+ "{kind:?} rejected curve {prime} unexpectedly"
+ );
+ }
+ Err(e) => panic!("unexpected error for {kind:?} ({protocol}, {prime}): {e}"),
+ }
+ }
+ }
+ }
+}
+
+/// Spot-check the specific examples called out in the design: the native
+/// backends are Groth16-only and curve-specific.
+#[test]
+fn unsupported_combinations_give_clear_errors() {
+ // Lambdaworks: Groth16 + BLS12-381 only.
+ assert!(matches!(
+ make_proving_backend(
+ ProvingBackendKind::Lambdaworks,
+ Protocol::Plonk,
+ Prime::Bls12381
+ ),
+ Err(ProveError::UnsupportedProtocol {
+ backend: "lambdaworks",
+ ..
+ })
+ ));
+ assert!(matches!(
+ make_proving_backend(
+ ProvingBackendKind::Lambdaworks,
+ Protocol::Groth16,
+ Prime::Bn128
+ ),
+ Err(ProveError::UnsupportedCurve {
+ backend: "lambdaworks",
+ ..
+ })
+ ));
+
+ // Arkworks: Groth16 + BN254 (bn128) only.
+ assert!(matches!(
+ make_proving_backend(ProvingBackendKind::Arkworks, Protocol::Fflonk, Prime::Bn128),
+ Err(ProveError::UnsupportedProtocol {
+ backend: "arkworks",
+ ..
+ })
+ ));
+ assert!(matches!(
+ make_proving_backend(
+ ProvingBackendKind::Arkworks,
+ Protocol::Groth16,
+ Prime::Bls12381
+ ),
+ Err(ProveError::UnsupportedCurve {
+ backend: "arkworks",
+ ..
+ })
+ ));
+
+ // snarkjs is universal — no capability error anywhere in the grid.
+ for &protocol in ALL_PROTOCOLS {
+ for &prime in ALL_PRIMES {
+ assert!(
+ make_proving_backend(ProvingBackendKind::Snarkjs, protocol, prime).is_ok(),
+ "snarkjs should support ({protocol}, {prime})"
+ );
+ }
+ }
+}
+
+/// Arkworks proves Groth16 over BN254 from a snarkjs `.zkey` + witness.
+#[cfg(feature = "prove-arkworks")]
+#[test]
+fn arkworks_groth16_bn254_proves() {
+ let data = test_data_dir();
+ let backend = make_proving_backend(
+ ProvingBackendKind::Arkworks,
+ Protocol::Groth16,
+ Prime::Bn128,
+ )
+ .expect("arkworks backend should build");
+
+ let output = backend
+ .prove(
+ &data.join("witness.wtns"),
+ &data.join("multiplier_30.r1cs"),
+ &data.join("groth16_pkey.zkey"),
+ )
+ .expect("arkworks prove should succeed");
+
+ assert_eq!(output.proof["protocol"], "groth16");
+ assert_eq!(output.proof["curve"], "bn128");
+ // multiplier_30 over 2 thirty times => 2^30 = 1073741824 is the public output.
+ assert_eq!(output.public_signals, vec!["1073741824"]);
+}
+
+/// Lambdaworks proves Groth16 over BLS12-381 with an on-the-fly setup.
+#[cfg(feature = "prove-lambdaworks")]
+#[test]
+fn lambdaworks_groth16_bls12381_proves() {
+ let data = test_data_dir();
+ let backend = make_proving_backend(
+ ProvingBackendKind::Lambdaworks,
+ Protocol::Groth16,
+ Prime::Bls12381,
+ )
+ .expect("lambdaworks backend should build");
+
+ let output = backend
+ .prove(
+ &data.join("witness.wtns"),
+ &data.join("multiplier_30.r1cs"),
+ &data.join("unused.zkey"),
+ )
+ .expect("lambdaworks prove should succeed");
+
+ assert_eq!(output.proof["protocol"], "groth16");
+ assert_eq!(output.proof["curve"], "bls12381");
+ // Lambdaworks includes the constant "1" wire ahead of the output.
+ assert_eq!(output.public_signals, vec!["1", "1073741824"]);
+}
+
+/// snarkjs proves Groth16 over BN254 via the CLI (needs `snarkjs` on PATH).
+#[test]
+fn snarkjs_groth16_bn254_proves() {
+ let data = test_data_dir();
+ let zkey = data.join("groth16_pkey.zkey");
+ let wasm = data.join("multiplier_30.wasm");
+ if !zkey.exists() || !wasm.exists() {
+ eprintln!("skipping: snarkjs fixtures missing");
+ return;
+ }
+
+ let backend =
+ make_proving_backend(ProvingBackendKind::Snarkjs, Protocol::Groth16, Prime::Bn128)
+ .expect("snarkjs backend should build");
+
+ let input = circomkit_core::signals! { "in" => vec![2_i64; 30] };
+
+ match backend.full_prove(&input, &wasm, &zkey) {
+ Ok(output) => {
+ assert_eq!(output.proof["protocol"], "groth16");
+ assert_eq!(output.public_signals, vec!["1073741824"]);
+ }
+ Err(e) => eprintln!("skipping snarkjs assertion (snarkjs CLI unavailable?): {e}"),
+ }
+}
diff --git a/crates/circomkit-prove/tests/lambdaworks_integration.rs b/crates/circomkit-prove/tests/lambdaworks_integration.rs
new file mode 100644
index 0000000..dffd10c
--- /dev/null
+++ b/crates/circomkit-prove/tests/lambdaworks_integration.rs
@@ -0,0 +1,73 @@
+#![cfg(feature = "prove-lambdaworks")]
+
+use std::path::PathBuf;
+
+use circomkit_prove::{LambdaworksBackend, ProofOutput, ProvingBackend};
+
+fn test_data_dir() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/data/multiplier_30")
+}
+
+#[test]
+fn lambdaworks_prove_with_binary_r1cs() {
+ let backend = LambdaworksBackend;
+ let data = test_data_dir();
+
+ let ProofOutput {
+ proof,
+ public_signals,
+ } = backend
+ .prove(
+ &data.join("witness.wtns"),
+ &data.join("multiplier_30.r1cs"),
+ &data.join("unused.zkey"), // pkey_path is unused by Lambdaworks
+ )
+ .expect("prove with binary r1cs should succeed");
+
+ // Lambdaworks includes the constant wire "1" as the first public signal,
+ // followed by the actual output: 2^30 = 1073741824
+ assert_eq!(public_signals, vec!["1", "1073741824"]);
+
+ assert_eq!(proof["protocol"], "groth16");
+ assert_eq!(proof["curve"], "bls12381");
+}
+
+#[test]
+fn lambdaworks_prove_with_json_r1cs() {
+ let backend = LambdaworksBackend;
+ let data = test_data_dir();
+
+ let ProofOutput {
+ proof,
+ public_signals,
+ } = backend
+ .prove(
+ &data.join("witness.wtns"),
+ &data.join("multiplier_30.r1cs.json"),
+ &data.join("unused.zkey"),
+ )
+ .expect("prove with json r1cs should succeed");
+
+ assert_eq!(public_signals, vec!["1", "1073741824"]);
+
+ assert_eq!(proof["protocol"], "groth16");
+ assert_eq!(proof["curve"], "bls12381");
+}
+
+#[test]
+fn lambdaworks_full_prove_unsupported() {
+ let backend = LambdaworksBackend;
+ let data = test_data_dir();
+
+ let input = circomkit_core::signals! {
+ "in" => vec![2_i64; 30],
+ };
+
+ let result = backend.full_prove(
+ &input,
+ &data.join("multiplier_30.wasm"),
+ &data.join("unused.zkey"),
+ );
+
+ assert!(result.is_err());
+}
diff --git a/crates/circomkit-prove/tests/snarkjs_integration.rs b/crates/circomkit-prove/tests/snarkjs_integration.rs
new file mode 100644
index 0000000..aa53c5e
--- /dev/null
+++ b/crates/circomkit-prove/tests/snarkjs_integration.rs
@@ -0,0 +1,76 @@
+use std::path::PathBuf;
+
+use circomkit_core::signals;
+use circomkit_prove::{ProofOutput, ProvingBackend, SnarkjsBackend};
+
+fn test_data_dir() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/data/multiplier_30")
+}
+
+#[test]
+fn snarkjs_full_prove_and_verify() {
+ let backend = SnarkjsBackend::default();
+ let data = test_data_dir();
+
+ let input = signals! {
+ "in" => vec![2_i64; 30],
+ };
+
+ let ProofOutput {
+ proof,
+ public_signals,
+ } = backend
+ .full_prove(
+ &input,
+ &data.join("multiplier_30.wasm"),
+ &data.join("groth16_pkey.zkey"),
+ )
+ .expect("full_prove should succeed");
+
+ // Output should be 2^30 = 1073741824
+ assert_eq!(public_signals, vec!["1073741824"]);
+
+ // Proof should have the groth16 structure
+ assert_eq!(proof["protocol"], "groth16");
+
+ // Verify with the vkey
+ let vkey: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(data.join("groth16_vkey.json")).unwrap())
+ .unwrap();
+
+ let ok = backend
+ .verify(&vkey, &public_signals, &proof)
+ .expect("verify should not error");
+ assert!(ok, "proof should verify");
+}
+
+#[test]
+fn snarkjs_verify_rejects_bad_proof() {
+ let backend = SnarkjsBackend::default();
+ let data = test_data_dir();
+
+ let input = signals! {
+ "in" => vec![2_i64; 30],
+ };
+
+ let ProofOutput {
+ proof,
+ mut public_signals,
+ } = backend
+ .full_prove(
+ &input,
+ &data.join("multiplier_30.wasm"),
+ &data.join("groth16_pkey.zkey"),
+ )
+ .unwrap();
+
+ // Tamper with public signals
+ public_signals[0] = "999".to_string();
+
+ let vkey: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(data.join("groth16_vkey.json")).unwrap())
+ .unwrap();
+
+ let ok = backend.verify(&vkey, &public_signals, &proof).unwrap();
+ assert!(!ok, "tampered proof should not verify");
+}
diff --git a/crates/circomkit-test/Cargo.toml b/crates/circomkit-test/Cargo.toml
new file mode 100644
index 0000000..9c2ef1e
--- /dev/null
+++ b/crates/circomkit-test/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "circomkit-test"
+description = "Testing utilities for Circom circuits"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+circomkit-core.workspace = true
+circomkit-witness.workspace = true
+circomkit-prove.workspace = true
+thiserror.workspace = true
+log.workspace = true
+serde_json.workspace = true
+num-bigint.workspace = true
diff --git a/crates/circomkit-test/src/error.rs b/crates/circomkit-test/src/error.rs
new file mode 100644
index 0000000..a6be312
--- /dev/null
+++ b/crates/circomkit-test/src/error.rs
@@ -0,0 +1,43 @@
+/// Errors that can occur during circuit testing.
+#[derive(Debug, thiserror::Error)]
+pub enum TestError {
+ #[error("expected witness computation to fail, but it succeeded")]
+ ExpectedFailure,
+
+ #[error("expected at least {expected} constraints, got {actual}")]
+ UnderConstrained { expected: u32, actual: u32 },
+
+ #[error("expected exactly {expected} constraints, got {actual}")]
+ ConstraintCountMismatch { expected: u32, actual: u32 },
+
+ #[error("expected constraints to fail, but they passed")]
+ ExpectedConstraintFailure,
+
+ #[error("output mismatch for signal '{signal}': expected {expected}, got {actual}")]
+ OutputMismatch {
+ signal: String,
+ expected: String,
+ actual: String,
+ },
+
+ #[error("verification expected to pass, but failed")]
+ VerificationExpectedPass,
+
+ #[error("verification expected to fail, but passed")]
+ VerificationExpectedFail,
+
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+
+ #[error(transparent)]
+ Json(#[from] serde_json::Error),
+
+ #[error(transparent)]
+ Witness(#[from] circomkit_witness::WitnessError),
+
+ #[error(transparent)]
+ Prove(#[from] circomkit_prove::ProveError),
+
+ #[error(transparent)]
+ Core(#[from] circomkit_core::error::CoreError),
+}
diff --git a/crates/circomkit-test/src/lib.rs b/crates/circomkit-test/src/lib.rs
new file mode 100644
index 0000000..fb84c4b
--- /dev/null
+++ b/crates/circomkit-test/src/lib.rs
@@ -0,0 +1,7 @@
+mod error;
+mod proof_tester;
+mod witness_tester;
+
+pub use error::TestError;
+pub use proof_tester::ProofTester;
+pub use witness_tester::WitnessTester;
diff --git a/crates/circomkit-test/src/proof_tester.rs b/crates/circomkit-test/src/proof_tester.rs
new file mode 100644
index 0000000..85b50fd
--- /dev/null
+++ b/crates/circomkit-test/src/proof_tester.rs
@@ -0,0 +1,80 @@
+use std::path::PathBuf;
+
+use circomkit_core::enums::Protocol;
+use circomkit_core::types::CircuitSignals;
+use circomkit_prove::ProofOutput;
+use circomkit_prove::ProvingBackend;
+
+use crate::error::TestError;
+
+/// A circuit tester focused on proof-level testing.
+pub struct ProofTester {
+ backend: Box,
+ wasm_path: PathBuf,
+ pkey_path: PathBuf,
+ /// The parsed verification key.
+ pub vkey: serde_json::Value,
+ /// The protocol used for proofs.
+ pub protocol: Protocol,
+}
+
+impl ProofTester {
+ pub fn new(
+ backend: Box,
+ wasm_path: PathBuf,
+ pkey_path: PathBuf,
+ vkey_path: PathBuf,
+ protocol: Protocol,
+ ) -> Result {
+ let vkey_str = std::fs::read_to_string(&vkey_path)?;
+ let vkey: serde_json::Value = serde_json::from_str(&vkey_str)?;
+
+ Ok(Self {
+ backend,
+ wasm_path,
+ pkey_path,
+ vkey,
+ protocol,
+ })
+ }
+
+ /// Generate a proof from circuit inputs.
+ pub fn prove(&self, input: &CircuitSignals) -> Result {
+ Ok(self
+ .backend
+ .full_prove(input, &self.wasm_path, &self.pkey_path)?)
+ }
+
+ /// Verify a proof.
+ pub fn verify(
+ &self,
+ proof: &serde_json::Value,
+ public_signals: &[String],
+ ) -> Result {
+ Ok(self.backend.verify(&self.vkey, public_signals, proof)?)
+ }
+
+ /// Assert that verification passes.
+ pub fn expect_pass(
+ &self,
+ proof: &serde_json::Value,
+ public_signals: &[String],
+ ) -> Result<(), TestError> {
+ if !self.verify(proof, public_signals)? {
+ return Err(TestError::VerificationExpectedPass);
+ }
+ Ok(())
+ }
+
+ /// Assert that verification fails.
+ pub fn expect_fail(
+ &self,
+ proof: &serde_json::Value,
+ public_signals: &[String],
+ ) -> Result<(), TestError> {
+ if self.verify(proof, public_signals)? {
+ return Err(TestError::VerificationExpectedFail);
+ }
+ Ok(())
+ }
+}
diff --git a/crates/circomkit-test/src/witness_tester.rs b/crates/circomkit-test/src/witness_tester.rs
new file mode 100644
index 0000000..e8f01fc
--- /dev/null
+++ b/crates/circomkit-test/src/witness_tester.rs
@@ -0,0 +1,176 @@
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::sync::{Mutex, OnceLock};
+
+use num_bigint::BigInt;
+
+use circomkit_core::functions::{edit_witness, parse_symbols, read_witness_signals};
+use circomkit_core::types::{CircuitSignals, R1CSInfo, Symbols, Witness};
+use circomkit_core::utils::read_r1cs_info;
+use circomkit_witness::WitnessCalculator;
+
+use crate::error::TestError;
+
+/// A circuit tester focused on witness-level testing.
+///
+/// Supports computing witnesses, checking outputs, asserting failures,
+/// reading signals, editing witnesses for soundness testing, and
+/// inspecting constraint counts.
+pub struct WitnessTester {
+ calculator: Box,
+ symbols: OnceLock>>,
+ r1cs_info: OnceLock>>,
+ r1cs_path: PathBuf,
+ sym_path: PathBuf,
+}
+
+impl WitnessTester {
+ pub fn new(
+ calculator: Box,
+ r1cs_path: PathBuf,
+ sym_path: PathBuf,
+ ) -> Self {
+ Self {
+ calculator,
+ symbols: OnceLock::new(),
+ r1cs_info: OnceLock::new(),
+ r1cs_path,
+ sym_path,
+ }
+ }
+
+ /// Compute a witness from input signals.
+ pub fn calculate_witness(&self, input: &CircuitSignals) -> Result {
+ Ok(self.calculator.calculate(input)?)
+ }
+
+ /// Get R1CS info (lazily loaded).
+ pub fn r1cs_info(&self) -> Result {
+ let mutex = self.r1cs_info.get_or_init(|| Mutex::new(None));
+ let mut guard = mutex.lock().unwrap();
+ if guard.is_none() {
+ *guard = Some(read_r1cs_info(&self.r1cs_path)?);
+ }
+ Ok(guard.as_ref().unwrap().clone())
+ }
+
+ /// Get the symbol table (lazily loaded).
+ pub fn symbols(&self) -> Result {
+ let mutex = self.symbols.get_or_init(|| Mutex::new(None));
+ let mut guard = mutex.lock().unwrap();
+ if guard.is_none() {
+ *guard = Some(parse_symbols(&self.sym_path)?);
+ }
+ Ok(guard.as_ref().unwrap().clone())
+ }
+
+ /// Get the constraint count.
+ pub fn constraint_count(&self) -> Result {
+ Ok(self.r1cs_info()?.constraints)
+ }
+
+ /// Assert the constraint count.
+ ///
+ /// If `exact` is true, asserts equality. Otherwise asserts `actual >= expected`.
+ pub fn expect_constraint_count(&self, expected: u32, exact: bool) -> Result<(), TestError> {
+ let actual = self.constraint_count()?;
+ if exact && actual != expected {
+ return Err(TestError::ConstraintCountMismatch { expected, actual });
+ }
+ if !exact && actual < expected {
+ return Err(TestError::UnderConstrained { expected, actual });
+ }
+ Ok(())
+ }
+
+ /// Expect the input to produce a valid witness.
+ ///
+ /// If `output` is provided, also checks that the output signals match.
+ pub fn expect_pass(
+ &self,
+ input: &CircuitSignals,
+ output: Option<&CircuitSignals>,
+ ) -> Result<(), TestError> {
+ let witness = self.calculate_witness(input)?;
+
+ if let Some(expected) = output {
+ let symbols = self.symbols()?;
+ let signal_names: Vec<&str> = expected.keys().map(|s| s.as_str()).collect();
+ let actual = read_witness_signals(&witness, &symbols, &signal_names)?;
+
+ for (name, expected_val) in expected {
+ let actual_val = actual.get(name).ok_or_else(|| TestError::OutputMismatch {
+ signal: name.clone(),
+ expected: format!("{expected_val:?}"),
+ actual: "not found".to_string(),
+ })?;
+
+ if expected_val != actual_val {
+ return Err(TestError::OutputMismatch {
+ signal: name.clone(),
+ expected: format!("{expected_val:?}"),
+ actual: format!("{actual_val:?}"),
+ });
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Expect witness computation to fail with a circuit error.
+ ///
+ /// Returns the error message on success.
+ pub fn expect_fail(&self, input: &CircuitSignals) -> Result {
+ match self.calculator.calculate(input) {
+ Err(e) if e.is_circuit_error() => Ok(e.to_string()),
+ Err(e) => Err(TestError::Witness(e)),
+ Ok(_) => Err(TestError::ExpectedFailure),
+ }
+ }
+
+ /// Compute a witness and read specific output signals.
+ pub fn compute(
+ &self,
+ input: &CircuitSignals,
+ signals: &[&str],
+ ) -> Result {
+ let witness = self.calculate_witness(input)?;
+ let symbols = self.symbols()?;
+ Ok(read_witness_signals(&witness, &symbols, signals)?)
+ }
+
+ /// Read signal values from a pre-computed witness.
+ pub fn read_witness_signals(
+ &self,
+ witness: &Witness,
+ signals: &[&str],
+ ) -> Result {
+ let symbols = self.symbols()?;
+ Ok(read_witness_signals(witness, &symbols, signals)?)
+ }
+
+ /// Read raw symbol values from a witness by full symbol names.
+ pub fn read_witness(
+ &self,
+ witness: &Witness,
+ symbol_names: &[&str],
+ ) -> Result, TestError> {
+ let symbols = self.symbols()?;
+ Ok(circomkit_core::functions::read_witness_raw(
+ witness,
+ &symbols,
+ symbol_names,
+ )?)
+ }
+
+ /// Edit witness values by symbol name (for soundness testing).
+ pub fn edit_witness(
+ &self,
+ witness: &Witness,
+ overrides: &HashMap,
+ ) -> Result {
+ let symbols = self.symbols()?;
+ Ok(edit_witness(witness, &symbols, overrides)?)
+ }
+}
diff --git a/crates/circomkit-witness/Cargo.toml b/crates/circomkit-witness/Cargo.toml
new file mode 100644
index 0000000..253a0b0
--- /dev/null
+++ b/crates/circomkit-witness/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "circomkit-witness"
+description = "Witness calculator trait and backends for Circomkit"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+circomkit-core.workspace = true
+thiserror.workspace = true
+log.workspace = true
+serde_json.workspace = true
+num-bigint.workspace = true
+num-traits.workspace = true
+
+[dependencies.wasmtime]
+workspace = true
+optional = true
+
+[features]
+default = ["witness-wasm"]
+witness-wasm = ["dep:wasmtime"]
+witness-c = []
diff --git a/crates/circomkit-witness/src/error.rs b/crates/circomkit-witness/src/error.rs
new file mode 100644
index 0000000..1c7de7a
--- /dev/null
+++ b/crates/circomkit-witness/src/error.rs
@@ -0,0 +1,35 @@
+/// Errors that can occur during witness calculation.
+#[derive(Debug, thiserror::Error)]
+pub enum WitnessError {
+ #[error("assertion failed in circuit: {0}")]
+ AssertionFailed(String),
+
+ #[error("not enough values for input signal: {0}")]
+ NotEnoughInputs(String),
+
+ #[error("too many values for input signal: {0}")]
+ TooManyInputs(String),
+
+ #[error("not all inputs have been set")]
+ MissingInputs,
+
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+
+ #[error("{0}")]
+ Other(String),
+}
+
+impl WitnessError {
+ /// Returns `true` if this is an expected circuit-level failure
+ /// (assertion, input mismatch) rather than an infrastructure error.
+ pub fn is_circuit_error(&self) -> bool {
+ matches!(
+ self,
+ Self::AssertionFailed(_)
+ | Self::NotEnoughInputs(_)
+ | Self::TooManyInputs(_)
+ | Self::MissingInputs
+ )
+ }
+}
diff --git a/crates/circomkit-witness/src/factory.rs b/crates/circomkit-witness/src/factory.rs
new file mode 100644
index 0000000..6c819f7
--- /dev/null
+++ b/crates/circomkit-witness/src/factory.rs
@@ -0,0 +1,34 @@
+use std::path::Path;
+
+use circomkit_core::enums::WitnessBackend;
+
+use crate::WitnessCalculator;
+use crate::WitnessError;
+
+/// Create the appropriate witness calculator based on the configured backend.
+pub fn make_witness_calculator(
+ backend: WitnessBackend,
+ wasm_path: &Path,
+ _c_binary_path: Option<&Path>,
+) -> Result, WitnessError> {
+ match backend {
+ #[cfg(feature = "witness-wasm")]
+ WitnessBackend::Wasm => {
+ let calc = crate::wasm::WasmWitnessCalculator::new(wasm_path.to_path_buf())?;
+ Ok(Box::new(calc))
+ }
+
+ #[cfg(feature = "witness-c")]
+ WitnessBackend::C => {
+ let _ = (_c_binary_path, wasm_path);
+ Err(WitnessError::Other(
+ "C witness calculator not yet implemented".to_string(),
+ ))
+ }
+
+ #[allow(unreachable_patterns)]
+ _ => Err(WitnessError::Other(format!(
+ "witness backend '{backend:?}' is not enabled via feature flags"
+ ))),
+ }
+}
diff --git a/crates/circomkit-witness/src/lib.rs b/crates/circomkit-witness/src/lib.rs
new file mode 100644
index 0000000..bdc7c4a
--- /dev/null
+++ b/crates/circomkit-witness/src/lib.rs
@@ -0,0 +1,13 @@
+mod error;
+mod factory;
+mod traits;
+
+#[cfg(feature = "witness-wasm")]
+mod wasm;
+
+pub use error::WitnessError;
+pub use factory::make_witness_calculator;
+pub use traits::WitnessCalculator;
+
+#[cfg(feature = "witness-wasm")]
+pub use wasm::WasmWitnessCalculator;
diff --git a/crates/circomkit-witness/src/traits.rs b/crates/circomkit-witness/src/traits.rs
new file mode 100644
index 0000000..47a2259
--- /dev/null
+++ b/crates/circomkit-witness/src/traits.rs
@@ -0,0 +1,9 @@
+use circomkit_core::types::{CircuitSignals, Witness};
+
+use crate::WitnessError;
+
+/// Trait for witness calculators that compute a witness from circuit inputs.
+pub trait WitnessCalculator {
+ /// Calculate a witness from circuit input signals.
+ fn calculate(&self, input: &CircuitSignals) -> Result;
+}
diff --git a/crates/circomkit-witness/src/wasm.rs b/crates/circomkit-witness/src/wasm.rs
new file mode 100644
index 0000000..6ca0ac6
--- /dev/null
+++ b/crates/circomkit-witness/src/wasm.rs
@@ -0,0 +1,481 @@
+use std::path::PathBuf;
+
+use num_bigint::BigInt;
+use num_traits::Zero;
+use wasmtime::{Engine, Instance, Linker, Module, Store};
+
+use circomkit_core::types::{CircuitSignals, SignalValue, Witness};
+
+use crate::error::WitnessError;
+use crate::traits::WitnessCalculator;
+
+/// WASM-based witness calculator using wasmtime.
+///
+/// Loads a circom-generated WASM module and implements the circom
+/// witness calculation protocol (shared RW memory, FNV signal hashing, etc.).
+pub struct WasmWitnessCalculator {
+ wasm_path: PathBuf,
+}
+
+impl WasmWitnessCalculator {
+ pub fn new(wasm_path: PathBuf) -> Result {
+ if !wasm_path.exists() {
+ return Err(WitnessError::Other(format!(
+ "WASM file not found: {}",
+ wasm_path.display()
+ )));
+ }
+ Ok(Self { wasm_path })
+ }
+}
+
+/// State stored inside the wasmtime Store, shared with host functions.
+#[derive(Default)]
+struct HostState {
+ error_message: String,
+ log_buffer: String,
+ exception: Option,
+}
+
+impl WitnessCalculator for WasmWitnessCalculator {
+ fn calculate(&self, input: &CircuitSignals) -> Result {
+ let engine = Engine::default();
+ let module = Module::from_file(&engine, &self.wasm_path)
+ .map_err(|e| WitnessError::Other(format!("failed to load WASM module: {e}")))?;
+
+ let mut linker = Linker::::new(&engine);
+
+ // Register circom runtime host functions
+ linker
+ .func_wrap(
+ "runtime",
+ "exceptionHandler",
+ |mut caller: wasmtime::Caller<'_, HostState>, code: i32| {
+ caller.data_mut().exception = Some(code as u32);
+ },
+ )
+ .map_err(|e| WitnessError::Other(format!("linker error: {e}")))?;
+
+ linker
+ .func_wrap(
+ "runtime",
+ "printErrorMessage",
+ |mut caller: wasmtime::Caller<'_, HostState>| {
+ let msg = get_message_from_wasm(&mut caller);
+ let state = caller.data_mut();
+ state.error_message.push_str(&msg);
+ state.error_message.push('\n');
+ },
+ )
+ .map_err(|e| WitnessError::Other(format!("linker error: {e}")))?;
+
+ linker
+ .func_wrap(
+ "runtime",
+ "writeBufferMessage",
+ |mut caller: wasmtime::Caller<'_, HostState>| {
+ let msg = get_message_from_wasm(&mut caller);
+ let state = caller.data_mut();
+ if msg == "\n" {
+ log::info!("{}", state.log_buffer);
+ state.log_buffer.clear();
+ } else {
+ if !state.log_buffer.is_empty() {
+ state.log_buffer.push(' ');
+ }
+ state.log_buffer.push_str(&msg);
+ }
+ },
+ )
+ .map_err(|e| WitnessError::Other(format!("linker error: {e}")))?;
+
+ linker
+ .func_wrap(
+ "runtime",
+ "showSharedRWMemory",
+ |_caller: wasmtime::Caller<'_, HostState>| {
+ // Debug callback — no-op in production
+ },
+ )
+ .map_err(|e| WitnessError::Other(format!("linker error: {e}")))?;
+
+ let mut store = Store::new(&engine, HostState::default());
+
+ let instance = linker
+ .instantiate(&mut store, &module)
+ .map_err(|e| WitnessError::Other(format!("WASM instantiation failed: {e}")))?;
+
+ // Get metadata
+ let n32 = call_i32(&instance, &mut store, "getFieldNumLen32")? as usize;
+ let witness_size = call_i32(&instance, &mut store, "getWitnessSize")? as usize;
+ let input_size = call_i32(&instance, &mut store, "getInputSize")? as usize;
+
+ // Read the prime
+ call_void(&instance, &mut store, "getRawPrime", &[])?;
+ let prime = read_shared_field_element(&instance, &mut store, n32)?;
+
+ // Initialize
+ call_void(&instance, &mut store, "init", &[wasmtime::Val::I32(1)])?;
+ check_exception(&mut store)?;
+
+ // Flatten and set all input signals
+ let mut input_counter = 0usize;
+ for (name, value) in input {
+ let (h_msb, h_lsb) = fnv_hash(name);
+
+ // Get expected signal size from the circuit
+ let signal_size = call_export_i32(
+ &instance,
+ &mut store,
+ "getInputSignalSize",
+ &[
+ wasmtime::Val::I32(h_msb as i32),
+ wasmtime::Val::I32(h_lsb as i32),
+ ],
+ )?;
+
+ if signal_size < 0 {
+ return Err(WitnessError::Other(format!("signal not found: {name}")));
+ }
+ let signal_size = signal_size as usize;
+
+ let flat = flatten_signal(value);
+
+ if flat.len() < signal_size {
+ return Err(WitnessError::NotEnoughInputs(name.clone()));
+ }
+ if flat.len() > signal_size {
+ return Err(WitnessError::TooManyInputs(name.clone()));
+ }
+
+ for (i, val) in flat.iter().enumerate() {
+ let normalized = normalize(val, &prime);
+ let limbs = bigint_to_u32_array(&normalized, n32);
+
+ // Write to shared RW memory: index j gets limbs[n32-1-j]
+ for j in 0..n32 {
+ let write_fn = instance
+ .get_func(&mut store, "writeSharedRWMemory")
+ .ok_or_else(|| {
+ WitnessError::Other("writeSharedRWMemory not found".to_string())
+ })?;
+ write_fn
+ .call(
+ &mut store,
+ &[
+ wasmtime::Val::I32(j as i32),
+ wasmtime::Val::I32(limbs[n32 - 1 - j] as i32),
+ ],
+ &mut [],
+ )
+ .map_err(|e| {
+ WitnessError::Other(format!("writeSharedRWMemory failed: {e}"))
+ })?;
+ }
+
+ // Set the input signal
+ let result = call_void(
+ &instance,
+ &mut store,
+ "setInputSignal",
+ &[
+ wasmtime::Val::I32(h_msb as i32),
+ wasmtime::Val::I32(h_lsb as i32),
+ wasmtime::Val::I32(i as i32),
+ ],
+ );
+
+ if let Err(e) = result {
+ check_exception(&mut store)?;
+ return Err(WitnessError::Other(format!("setInputSignal failed: {e}")));
+ }
+
+ check_exception(&mut store)?;
+ input_counter += 1;
+ }
+ }
+
+ if input_counter < input_size {
+ return Err(WitnessError::MissingInputs);
+ }
+
+ // Read witness
+ let mut witness = Vec::with_capacity(witness_size);
+ for i in 0..witness_size {
+ let get_witness_fn = instance
+ .get_func(&mut store, "getWitness")
+ .ok_or_else(|| WitnessError::Other("getWitness not found".to_string()))?;
+ get_witness_fn
+ .call(&mut store, &[wasmtime::Val::I32(i as i32)], &mut [])
+ .map_err(|e| WitnessError::Other(format!("getWitness failed: {e}")))?;
+
+ let val = read_shared_field_element(&instance, &mut store, n32)?;
+ witness.push(val);
+ }
+
+ Ok(witness)
+ }
+}
+
+// ---- Helper functions ----
+
+/// FNV-1a 64-bit hash of a string, returning (MSB_u32, LSB_u32).
+fn fnv_hash(s: &str) -> (u32, u32) {
+ let mut hash: u64 = 0xCBF2_9CE4_8422_2325;
+ for byte in s.bytes() {
+ hash ^= byte as u64;
+ hash = hash.wrapping_mul(0x0100_0000_01B3);
+ }
+ let msb = (hash >> 32) as u32;
+ let lsb = hash as u32;
+ (msb, lsb)
+}
+
+/// Normalize a BigInt modulo prime (result in [0, prime)).
+fn normalize(val: &BigInt, prime: &BigInt) -> BigInt {
+ let r = val % prime;
+ if r < BigInt::zero() { r + prime } else { r }
+}
+
+/// Convert a non-negative BigInt into a big-endian array of `size` u32 limbs.
+fn bigint_to_u32_array(val: &BigInt, size: usize) -> Vec {
+ let mut res = vec![0u32; size];
+ let radix = BigInt::from(0x1_0000_0000u64);
+ let mut rem = val.clone();
+
+ for limb in res.iter_mut().rev() {
+ if rem.is_zero() {
+ break;
+ }
+ let (q, r) = num_integer_div_rem(&rem, &radix);
+ *limb = bigint_to_u32(&r);
+ rem = q;
+ }
+
+ res
+}
+
+fn num_integer_div_rem(a: &BigInt, b: &BigInt) -> (BigInt, BigInt) {
+ use num_traits::Signed;
+ let q = a / b;
+ let r = a - &q * b;
+ if r.is_negative() {
+ (q - BigInt::from(1), r + b)
+ } else {
+ (q, r)
+ }
+}
+
+fn bigint_to_u32(val: &BigInt) -> u32 {
+ let (_, bytes) = val.to_bytes_le();
+ let mut buf = [0u8; 4];
+ let len = bytes.len().min(4);
+ buf[..len].copy_from_slice(&bytes[..len]);
+ u32::from_le_bytes(buf)
+}
+
+/// Read a field element from shared RW memory as a BigInt.
+fn read_shared_field_element(
+ instance: &Instance,
+ store: &mut Store,
+ n32: usize,
+) -> Result {
+ let mut arr = vec![0u32; n32];
+ let mut results = [wasmtime::Val::I32(0)];
+ for j in 0..n32 {
+ let read_fn = instance
+ .get_func(&mut *store, "readSharedRWMemory")
+ .ok_or_else(|| WitnessError::Other("readSharedRWMemory not found".to_string()))?;
+ read_fn
+ .call(&mut *store, &[wasmtime::Val::I32(j as i32)], &mut results)
+ .map_err(|e| WitnessError::Other(format!("readSharedRWMemory failed: {e}")))?;
+ arr[n32 - 1 - j] = results[0].unwrap_i32() as u32;
+ }
+
+ Ok(from_u32_array_be(&arr))
+}
+
+/// Convert a big-endian u32 array to BigInt.
+fn from_u32_array_be(arr: &[u32]) -> BigInt {
+ let mut result = BigInt::zero();
+ let radix = BigInt::from(0x1_0000_0000u64);
+ for &limb in arr {
+ result = result * &radix + BigInt::from(limb);
+ }
+ result
+}
+
+/// Flatten a SignalValue into a list of BigInt values.
+fn flatten_signal(value: &SignalValue) -> Vec {
+ match value {
+ SignalValue::Single(v) => vec![v.clone()],
+ SignalValue::Array(arr) => arr.iter().flat_map(flatten_signal).collect(),
+ }
+}
+
+/// Get the message string from WASM by repeatedly calling `getMessageChar`.
+fn get_message_from_wasm(caller: &mut wasmtime::Caller<'_, HostState>) -> String {
+ let get_char = match caller.get_export("getMessageChar") {
+ Some(wasmtime::Extern::Func(f)) => f,
+ _ => return String::new(),
+ };
+
+ let mut msg = String::new();
+ let mut results = [wasmtime::Val::I32(0)];
+ loop {
+ if get_char.call(&mut *caller, &[], &mut results).is_err() {
+ break;
+ }
+ let c = results[0].unwrap_i32() as u32;
+ if c == 0 {
+ break;
+ }
+ if let Some(ch) = char::from_u32(c) {
+ msg.push(ch);
+ }
+ }
+ msg
+}
+
+/// Check if the WASM module raised an exception via the host state.
+fn check_exception(store: &mut Store) -> Result<(), WitnessError> {
+ let state = store.data();
+ if let Some(code) = state.exception {
+ let detail = if state.error_message.is_empty() {
+ String::new()
+ } else {
+ format!(": {}", state.error_message.trim())
+ };
+
+ // Reset
+ store.data_mut().exception = None;
+ store.data_mut().error_message.clear();
+
+ match code {
+ 1 => Err(WitnessError::Other(format!("signal not found{detail}"))),
+ 2 => Err(WitnessError::Other(format!("too many signals set{detail}"))),
+ 3 => Err(WitnessError::Other(format!("signal already set{detail}"))),
+ 4 => Err(WitnessError::AssertionFailed(format!(
+ "assert failed{detail}"
+ ))),
+ 5 => Err(WitnessError::Other(format!("not enough memory{detail}"))),
+ 6 => Err(WitnessError::Other(format!(
+ "input signal array access exceeds size{detail}"
+ ))),
+ _ => Err(WitnessError::Other(format!(
+ "unknown WASM error (code {code}){detail}"
+ ))),
+ }
+ } else {
+ Ok(())
+ }
+}
+
+// ---- Convenience wrappers for calling WASM exports ----
+
+fn call_void(
+ instance: &Instance,
+ store: &mut Store,
+ name: &str,
+ args: &[wasmtime::Val],
+) -> Result<(), WitnessError> {
+ let func = instance
+ .get_func(&mut *store, name)
+ .ok_or_else(|| WitnessError::Other(format!("WASM export '{name}' not found")))?;
+ func.call(&mut *store, args, &mut [])
+ .map_err(|e| WitnessError::Other(format!("{name} failed: {e}")))?;
+ Ok(())
+}
+
+fn call_i32(
+ instance: &Instance,
+ store: &mut Store,
+ name: &str,
+) -> Result {
+ call_export_i32(instance, store, name, &[])
+}
+
+fn call_export_i32(
+ instance: &Instance,
+ store: &mut Store,
+ name: &str,
+ args: &[wasmtime::Val],
+) -> Result {
+ let func = instance
+ .get_func(&mut *store, name)
+ .ok_or_else(|| WitnessError::Other(format!("WASM export '{name}' not found")))?;
+ let mut results = [wasmtime::Val::I32(0)];
+ func.call(&mut *store, args, &mut results)
+ .map_err(|e| WitnessError::Other(format!("{name} failed: {e}")))?;
+ Ok(results[0].unwrap_i32())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn fnv_hash_known_values() {
+ let (msb, lsb) = fnv_hash("in");
+ assert_ne!(msb, 0);
+ assert_ne!(lsb, 0);
+
+ // Same input must produce same hash
+ let (msb2, lsb2) = fnv_hash("in");
+ assert_eq!(msb, msb2);
+ assert_eq!(lsb, lsb2);
+
+ // Different inputs produce different hashes
+ let (msb3, lsb3) = fnv_hash("out");
+ assert!(msb != msb3 || lsb != lsb3);
+ }
+
+ #[test]
+ fn bigint_to_array_roundtrip() {
+ let val = BigInt::from(12345678u64);
+ let arr = bigint_to_u32_array(&val, 8);
+ let back = from_u32_array_be(&arr);
+ assert_eq!(val, back);
+ }
+
+ #[test]
+ fn bigint_to_array_large() {
+ let val = BigInt::from(u64::MAX);
+ let arr = bigint_to_u32_array(&val, 8);
+ let back = from_u32_array_be(&arr);
+ assert_eq!(val, back);
+ }
+
+ #[test]
+ fn bigint_to_array_zero() {
+ let val = BigInt::zero();
+ let arr = bigint_to_u32_array(&val, 8);
+ assert!(arr.iter().all(|&x| x == 0));
+ let back = from_u32_array_be(&arr);
+ assert_eq!(val, back);
+ }
+
+ #[test]
+ fn flatten_nested_signal() {
+ let sig = SignalValue::Array(vec![
+ SignalValue::Single(BigInt::from(1)),
+ SignalValue::Array(vec![
+ SignalValue::Single(BigInt::from(2)),
+ SignalValue::Single(BigInt::from(3)),
+ ]),
+ ]);
+ let flat = flatten_signal(&sig);
+ assert_eq!(
+ flat,
+ vec![BigInt::from(1), BigInt::from(2), BigInt::from(3)]
+ );
+ }
+
+ #[test]
+ fn normalize_negative() {
+ let prime = BigInt::from(7);
+ assert_eq!(normalize(&BigInt::from(-1), &prime), BigInt::from(6));
+ assert_eq!(normalize(&BigInt::from(0), &prime), BigInt::from(0));
+ assert_eq!(normalize(&BigInt::from(10), &prime), BigInt::from(3));
+ }
+}
diff --git a/crates/circomkit-witness/tests/wasm_integration.rs b/crates/circomkit-witness/tests/wasm_integration.rs
new file mode 100644
index 0000000..f73ae08
--- /dev/null
+++ b/crates/circomkit-witness/tests/wasm_integration.rs
@@ -0,0 +1,62 @@
+#[cfg(feature = "witness-wasm")]
+mod tests {
+ use std::path::PathBuf;
+
+ use circomkit_core::signals;
+ use circomkit_witness::{WasmWitnessCalculator, WitnessCalculator};
+ use num_bigint::BigInt;
+
+ fn test_data_dir() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/data/multiplier_30")
+ }
+
+ fn wasm_path() -> PathBuf {
+ test_data_dir().join("multiplier_30.wasm")
+ }
+
+ #[test]
+ fn calculate_witness_multiplier_30() {
+ let calc = WasmWitnessCalculator::new(wasm_path()).unwrap();
+
+ // 30 inputs of 2 => output = 2^30 = 1073741824
+ let input = signals! {
+ "in" => vec![2_i64; 30],
+ };
+
+ let witness = calc.calculate(&input).unwrap();
+
+ // witness[0] is always 1 (the "one" wire)
+ assert_eq!(witness[0], BigInt::from(1));
+
+ // witness[1] should be the output: 2^30
+ assert_eq!(witness[1], BigInt::from(1073741824u64));
+
+ assert!(witness.len() > 30);
+ }
+
+ #[test]
+ fn calculate_witness_all_ones() {
+ let calc = WasmWitnessCalculator::new(wasm_path()).unwrap();
+
+ let input = signals! {
+ "in" => vec![1_i64; 30],
+ };
+
+ let witness = calc.calculate(&input).unwrap();
+ assert_eq!(witness[0], BigInt::from(1));
+ assert_eq!(witness[1], BigInt::from(1));
+ }
+
+ #[test]
+ fn wrong_input_count_fails() {
+ let calc = WasmWitnessCalculator::new(wasm_path()).unwrap();
+
+ // Only 5 inputs instead of 30
+ let input = signals! {
+ "in" => vec![2_i64; 5],
+ };
+
+ let result = calc.calculate(&input);
+ assert!(result.is_err());
+ }
+}
diff --git a/crates/circomkit/Cargo.toml b/crates/circomkit/Cargo.toml
new file mode 100644
index 0000000..105c562
--- /dev/null
+++ b/crates/circomkit/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "circomkit"
+description = "Circom testing & development toolkit"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+circomkit-core.workspace = true
+circomkit-witness.workspace = true
+circomkit-prove.workspace = true
+circomkit-test.workspace = true
+log.workspace = true
+serde_json.workspace = true
+num-bigint.workspace = true
+thiserror.workspace = true
+
+[features]
+default = []
+# Native proving backends (forwarded to circomkit-prove).
+prove-arkworks = ["circomkit-prove/prove-arkworks"]
+prove-lambdaworks = ["circomkit-prove/prove-lambdaworks"]
diff --git a/crates/circomkit/src/circomkit/compile.rs b/crates/circomkit/src/circomkit/compile.rs
new file mode 100644
index 0000000..65801c9
--- /dev/null
+++ b/crates/circomkit/src/circomkit/compile.rs
@@ -0,0 +1,100 @@
+use std::path::PathBuf;
+
+use circomkit_core::error::{CoreError, Result};
+use circomkit_core::functions::{compile_circuit, instantiate_circuit};
+use circomkit_core::types::R1CSInfo;
+use circomkit_core::utils::read_r1cs_info;
+
+use super::Circomkit;
+
+impl Circomkit {
+ /// Generate the main component `.circom` file for a circuit.
+ pub fn instantiate(&self, circuit: &str) -> Result {
+ let resolved = self.resolve(circuit)?;
+ let main_path = self.paths.circuit_main(circuit);
+ instantiate_circuit(&resolved.circuit, &main_path, &resolved.version)?;
+ log::info!("instantiated {circuit} at {}", main_path.display());
+ Ok(main_path)
+ }
+
+ /// Check whether build artifacts are up-to-date relative to the source file.
+ ///
+ /// Returns `true` if the R1CS file exists and is newer than both the source
+ /// `.circom` file and the generated main component file.
+ fn is_build_fresh(&self, circuit: &str, source_file: &str) -> bool {
+ let r1cs_path = self.paths.circuit_r1cs(circuit);
+ let source_path = self.paths.circuit_source(source_file);
+ let main_path = self.paths.circuit_main(circuit);
+
+ let r1cs_mtime = match r1cs_path.metadata().and_then(|m| m.modified()) {
+ Ok(t) => t,
+ Err(_) => return false,
+ };
+
+ // Check source .circom file mtime
+ if let Ok(source_mtime) = source_path.metadata().and_then(|m| m.modified()) {
+ if source_mtime > r1cs_mtime {
+ return false;
+ }
+ }
+
+ // Check generated main file mtime
+ if let Ok(main_mtime) = main_path.metadata().and_then(|m| m.modified()) {
+ if main_mtime > r1cs_mtime {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ /// Compile a circuit. Auto-instantiates if the main file doesn't exist.
+ ///
+ /// When `recompile` is `false` (the default), compilation is skipped if the
+ /// build artifacts are newer than the source file. Set `recompile` to `true`
+ /// to always recompile.
+ pub fn compile(&self, circuit: &str) -> Result {
+ let resolved = self.resolve(circuit)?;
+ let main_path = self.paths.circuit_main(circuit);
+
+ if !main_path.exists() {
+ self.instantiate(circuit)?;
+ }
+
+ let out_dir = self.paths.circuit_dir(circuit);
+
+ if !resolved.compiler.recompile && self.is_build_fresh(circuit, &resolved.circuit.file) {
+ log::info!("skipping compilation for {circuit} (build is up-to-date)");
+ return Ok(out_dir);
+ }
+
+ compile_circuit(&resolved.compiler, &main_path, &out_dir)?;
+ log::info!("compiled {circuit} to {}", out_dir.display());
+ Ok(out_dir)
+ }
+
+ /// Get circuit info from the R1CS file.
+ pub fn info(&self, circuit: &str) -> Result {
+ let r1cs_path = self.paths.circuit_r1cs(circuit);
+ if !r1cs_path.exists() {
+ return Err(CoreError::FileNotFound(r1cs_path));
+ }
+ read_r1cs_info(&r1cs_path)
+ }
+
+ /// Remove all build artifacts for a circuit.
+ pub fn clear(&self, circuit: &str) -> Result<()> {
+ let dir = self.paths.circuit_dir(circuit);
+ if dir.exists() {
+ std::fs::remove_dir_all(&dir)?;
+ log::info!("cleared {}", dir.display());
+ }
+
+ let main_path = self.paths.circuit_main(circuit);
+ if main_path.exists() {
+ std::fs::remove_file(&main_path)?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/circomkit/src/circomkit/mod.rs b/crates/circomkit/src/circomkit/mod.rs
new file mode 100644
index 0000000..6c194ca
--- /dev/null
+++ b/crates/circomkit/src/circomkit/mod.rs
@@ -0,0 +1,56 @@
+mod compile;
+mod prove;
+mod testing;
+
+use std::path::Path;
+
+use circomkit_core::config::{CircomkitConfig, ResolvedCircuitConfig};
+use circomkit_core::error::{CoreError, Result};
+use circomkit_core::pathing::CircomkitPaths;
+use circomkit_core::types::CircuitSignals;
+use circomkit_prove::SnarkjsBackend;
+
+/// The main orchestrator for Circomkit operations.
+///
+/// Holds the configuration and provides high-level methods for the full
+/// circuit development lifecycle: compile, setup, prove, verify, and test.
+pub struct Circomkit {
+ pub config: CircomkitConfig,
+ pub paths: CircomkitPaths,
+}
+
+impl Circomkit {
+ /// Create a new Circomkit instance from a config.
+ pub fn new(config: CircomkitConfig) -> Result {
+ config.validate()?;
+ let paths = CircomkitPaths::new(&config);
+ Ok(Self { config, paths })
+ }
+
+ /// Load Circomkit from a `circomkit.json` file.
+ pub fn from_file(path: &Path) -> Result {
+ let config = CircomkitConfig::from_file(path)?;
+ Self::new(config)
+ }
+
+ /// Resolve a circuit's config (merging per-circuit overrides with global).
+ pub(crate) fn resolve(&self, circuit: &str) -> Result {
+ self.config.resolve_circuit(circuit)
+ }
+
+ /// Create a SnarkjsBackend for the current protocol.
+ pub(crate) fn snarkjs(&self) -> SnarkjsBackend {
+ SnarkjsBackend::new("snarkjs", self.config.prover.protocol)
+ }
+
+ /// Load circuit input signals from the inputs directory.
+ pub fn load_input(&self, circuit: &str, input: &str) -> Result {
+ let path = self.paths.input_json(circuit, input);
+ if !path.exists() {
+ return Err(CoreError::FileNotFound(path));
+ }
+ let content = std::fs::read_to_string(&path)?;
+ let signals: CircuitSignals = serde_json::from_str(&content)?;
+ Ok(signals)
+ }
+}
diff --git a/crates/circomkit/src/circomkit/prove.rs b/crates/circomkit/src/circomkit/prove.rs
new file mode 100644
index 0000000..e0be8ce
--- /dev/null
+++ b/crates/circomkit/src/circomkit/prove.rs
@@ -0,0 +1,215 @@
+use std::path::{Path, PathBuf};
+
+use circomkit_core::enums::ProvingBackendKind;
+use circomkit_core::error::{CoreError, Result};
+use circomkit_core::functions::get_calldata;
+use circomkit_core::types::CircuitSignals;
+use circomkit_prove::{ProvingBackend, SetupBackend, make_proving_backend};
+use circomkit_witness::make_witness_calculator;
+
+use super::Circomkit;
+
+/// Output from a trusted setup.
+pub struct SetupOutput {
+ pub pkey_path: PathBuf,
+ pub vkey_path: PathBuf,
+}
+
+impl Circomkit {
+ /// Get or download the PTAU file for a circuit.
+ pub fn ptau(&self, circuit: &str) -> Result {
+ let info = self.info(circuit)?;
+ let ptau_name = circomkit_core::utils::ptau_name_for_constraints(info.constraints);
+
+ if let Some(path) =
+ circomkit_core::utils::ptau_path_if_exists(&ptau_name, &self.config.prover.ptau_dir)
+ {
+ return Ok(path);
+ }
+
+ circomkit_core::utils::download_ptau(&ptau_name, &self.config.prover.ptau_dir)
+ }
+
+ /// Run trusted setup for a circuit.
+ pub fn setup(&self, circuit: &str, ptau_path: Option<&Path>) -> Result {
+ let resolved = self.resolve(circuit)?;
+ let protocol = resolved.prover.protocol;
+
+ let ptau = match ptau_path {
+ Some(p) => p.to_path_buf(),
+ None => self.ptau(circuit)?,
+ };
+
+ let pkey_path = self.paths.pkey(circuit, protocol);
+ let vkey_path = self.paths.vkey(circuit, protocol);
+
+ self.snarkjs()
+ .setup(
+ &self.paths.circuit_r1cs(circuit),
+ &ptau,
+ &pkey_path,
+ &resolved.prover,
+ )
+ .map_err(|e| CoreError::CompilationFailed(format!("setup failed: {e}")))?;
+
+ self.snarkjs()
+ .export_vkey(&pkey_path, &vkey_path)
+ .map_err(|e| CoreError::CompilationFailed(format!("vkey export failed: {e}")))?;
+
+ log::info!("setup complete for {circuit}");
+ Ok(SetupOutput {
+ pkey_path,
+ vkey_path,
+ })
+ }
+
+ /// Export verification key from an existing proving key.
+ pub fn vkey(&self, circuit: &str) -> Result {
+ let protocol = self.config.prover.protocol;
+ let pkey_path = self.paths.pkey(circuit, protocol);
+ let vkey_path = self.paths.vkey(circuit, protocol);
+
+ self.snarkjs()
+ .export_vkey(&pkey_path, &vkey_path)
+ .map_err(|e| CoreError::CompilationFailed(format!("vkey export failed: {e}")))?;
+
+ Ok(vkey_path)
+ }
+
+ /// Export Solidity verifier contract.
+ pub fn contract(&self, circuit: &str) -> Result {
+ let protocol = self.config.prover.protocol;
+ let pkey_path = self.paths.pkey(circuit, protocol);
+ let sol_path = self.paths.verifier_sol(circuit, protocol);
+
+ self.snarkjs()
+ .export_contract(&pkey_path, &sol_path, protocol)
+ .map_err(|e| CoreError::CompilationFailed(format!("contract export failed: {e}")))?;
+
+ log::info!("contract exported to {}", sol_path.display());
+ Ok(sol_path)
+ }
+
+ /// Compute a witness for a circuit with the given input.
+ pub fn witness(
+ &self,
+ circuit: &str,
+ input: &str,
+ data: Option<&CircuitSignals>,
+ ) -> Result {
+ let input_data = match data {
+ Some(d) => d.clone(),
+ None => self.load_input(circuit, input)?,
+ };
+
+ let wasm_path = self.paths.circuit_wasm(circuit);
+ let calc = make_witness_calculator(self.config.witness.calculator, &wasm_path, None)
+ .map_err(|e| CoreError::CompilationFailed(format!("witness calculator: {e}")))?;
+
+ let witness = calc
+ .calculate(&input_data)
+ .map_err(|e| CoreError::CompilationFailed(format!("witness calculation: {e}")))?;
+
+ let wtns_path = self.paths.witness_path(circuit, input);
+ if let Some(parent) = wtns_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ circomkit_core::utils::write_witness_file(&wtns_path, &witness)?;
+
+ log::info!("witness computed for {circuit}/{input}");
+ Ok(wtns_path)
+ }
+
+ /// Generate a proof for a circuit with the given input.
+ ///
+ /// The proving backend is selected from the resolved circuit config
+ /// (`prover.backend`), capability-checked against the protocol and curve.
+ /// snarkjs uses its one-shot `full_prove`; native backends (arkworks,
+ /// lambdaworks) compute a witness first, then prove from it.
+ pub fn prove(
+ &self,
+ circuit: &str,
+ input: &str,
+ data: Option<&CircuitSignals>,
+ ) -> Result {
+ let resolved = self.resolve(circuit)?;
+ let protocol = resolved.prover.protocol;
+ let kind = resolved.prover.backend;
+ let prime = resolved.compiler.prime;
+
+ let pkey_path = self.paths.pkey(circuit, protocol);
+
+ // Capability check (protocol + curve) happens inside the factory.
+ let backend = make_proving_backend(kind, protocol, prime)
+ .map_err(|e| CoreError::CompilationFailed(format!("prove failed: {e}")))?;
+
+ let output = if kind == ProvingBackendKind::Snarkjs {
+ // One-shot: snarkjs computes the witness and proves in one subprocess.
+ let input_data = match data {
+ Some(d) => d.clone(),
+ None => self.load_input(circuit, input)?,
+ };
+ let wasm_path = self.paths.circuit_wasm(circuit);
+ backend
+ .full_prove(&input_data, &wasm_path, &pkey_path)
+ .map_err(|e| CoreError::CompilationFailed(format!("prove failed: {e}")))?
+ } else {
+ // Native backends prove from a pre-computed witness.
+ let wtns_path = self.witness(circuit, input, data)?;
+ let r1cs_path = self.paths.circuit_r1cs(circuit);
+ backend
+ .prove(&wtns_path, &r1cs_path, &pkey_path)
+ .map_err(|e| CoreError::CompilationFailed(format!("prove failed: {e}")))?
+ };
+
+ let proof_path = self.paths.proof_path(circuit, input, protocol);
+ let public_path = self.paths.public_signals_path(circuit, input);
+
+ if let Some(parent) = proof_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ std::fs::write(&proof_path, serde_json::to_string_pretty(&output.proof)?)?;
+ std::fs::write(
+ &public_path,
+ serde_json::to_string_pretty(&output.public_signals)?,
+ )?;
+
+ log::info!("proof generated for {circuit}/{input}");
+ Ok(proof_path)
+ }
+
+ /// Verify a proof for a circuit with the given input.
+ pub fn verify(&self, circuit: &str, input: &str) -> Result {
+ let protocol = self.config.prover.protocol;
+ let vkey_path = self.paths.vkey(circuit, protocol);
+ let proof_path = self.paths.proof_path(circuit, input, protocol);
+ let public_path = self.paths.public_signals_path(circuit, input);
+
+ let vkey: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&vkey_path)?)?;
+ let proof: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(&proof_path)?)?;
+ let public_signals: Vec =
+ serde_json::from_str(&std::fs::read_to_string(&public_path)?)?;
+
+ let ok = self
+ .snarkjs()
+ .verify(&vkey, &public_signals, &proof)
+ .map_err(|e| CoreError::CompilationFailed(format!("verify failed: {e}")))?;
+
+ Ok(ok)
+ }
+
+ /// Generate calldata for a circuit proof.
+ pub fn calldata(&self, circuit: &str, input: &str, pretty: bool) -> Result {
+ let protocol = self.config.prover.protocol;
+ let proof_path = self.paths.proof_path(circuit, input, protocol);
+ let public_path = self.paths.public_signals_path(circuit, input);
+
+ let proof: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(&proof_path)?)?;
+ let public_signals: Vec =
+ serde_json::from_str(&std::fs::read_to_string(&public_path)?)?;
+
+ get_calldata(&proof, &public_signals, pretty)
+ }
+}
diff --git a/crates/circomkit/src/circomkit/testing.rs b/crates/circomkit/src/circomkit/testing.rs
new file mode 100644
index 0000000..371ff6c
--- /dev/null
+++ b/crates/circomkit/src/circomkit/testing.rs
@@ -0,0 +1,46 @@
+use circomkit_core::config::CircuitConfig;
+use circomkit_core::enums::Protocol;
+use circomkit_prove::SnarkjsBackend;
+use circomkit_test::{ProofTester, WitnessTester};
+use circomkit_witness::make_witness_calculator;
+
+use super::Circomkit;
+
+impl Circomkit {
+ /// Create a WitnessTester for a circuit.
+ pub fn witness_tester(
+ &self,
+ circuit: &str,
+ config: CircuitConfig,
+ ) -> std::result::Result {
+ // Ensure circuit is compiled
+ self.compile(circuit)
+ .map_err(circomkit_test::TestError::Core)?;
+
+ let wasm_path = self.paths.circuit_wasm(circuit);
+ let calc = make_witness_calculator(self.config.witness.calculator, &wasm_path, None)?;
+
+ let _ = config;
+ Ok(WitnessTester::new(
+ calc,
+ self.paths.circuit_r1cs(circuit),
+ self.paths.circuit_sym(circuit),
+ ))
+ }
+
+ /// Create a ProofTester for a circuit.
+ pub fn proof_tester(
+ &self,
+ circuit: &str,
+ protocol: Protocol,
+ ) -> std::result::Result {
+ let backend = Box::new(SnarkjsBackend::new("snarkjs", protocol));
+ ProofTester::new(
+ backend,
+ self.paths.circuit_wasm(circuit),
+ self.paths.pkey(circuit, protocol),
+ self.paths.vkey(circuit, protocol),
+ protocol,
+ )
+ }
+}
diff --git a/crates/circomkit/src/lib.rs b/crates/circomkit/src/lib.rs
new file mode 100644
index 0000000..758517b
--- /dev/null
+++ b/crates/circomkit/src/lib.rs
@@ -0,0 +1,22 @@
+pub use circomkit_core as core;
+pub use circomkit_prove as prove;
+pub use circomkit_test as test;
+pub use circomkit_witness as witness;
+
+mod circomkit;
+
+pub use circomkit::Circomkit;
+
+// Re-export key types for convenience
+pub use circomkit_core::config::{CircomkitConfig, CircuitConfig};
+pub use circomkit_core::enums::{Prime, Protocol};
+pub use circomkit_core::error::CoreError;
+pub use circomkit_core::pathing::CircomkitPaths;
+pub use circomkit_core::types::R1CSInfo;
+pub use circomkit_core::types::{CircuitSignals, SignalValue, Witness};
+pub use circomkit_prove::{ProofOutput, ProvingBackend, SnarkjsBackend};
+pub use circomkit_test::{ProofTester, WitnessTester};
+pub use circomkit_witness::{WitnessCalculator, WitnessError};
+
+// Re-export the signals! macro
+pub use circomkit_core::signals;
diff --git a/crates/circomkit/tests/circuits/main/arrays_2_3.circom b/crates/circomkit/tests/circuits/main/arrays_2_3.circom
new file mode 100644
index 0000000..13b39d3
--- /dev/null
+++ b/crates/circomkit/tests/circuits/main/arrays_2_3.circom
@@ -0,0 +1,5 @@
+// auto-generated by circomkit
+pragma circom 2.1.0;
+include "../arrays.circom";
+
+component main {public[in1D, in2D]} = Arrays(2, 3);
diff --git a/tests/circuits/test/error_rt.circom b/crates/circomkit/tests/circuits/main/errors.circom
similarity index 79%
rename from tests/circuits/test/error_rt.circom
rename to crates/circomkit/tests/circuits/main/errors.circom
index 8bafc66..1ae676d 100644
--- a/tests/circuits/test/error_rt.circom
+++ b/crates/circomkit/tests/circuits/main/errors.circom
@@ -1,6 +1,5 @@
// auto-generated by circomkit
-pragma circom 2.0.0;
-
+pragma circom 2.1.0;
include "../errors.circom";
component main = Errors();
diff --git a/tests/circuits/test/multiplier_3.circom b/crates/circomkit/tests/circuits/main/multiplier_3.circom
similarity index 81%
rename from tests/circuits/test/multiplier_3.circom
rename to crates/circomkit/tests/circuits/main/multiplier_3.circom
index 968860f..0054997 100644
--- a/tests/circuits/test/multiplier_3.circom
+++ b/crates/circomkit/tests/circuits/main/multiplier_3.circom
@@ -1,6 +1,5 @@
// auto-generated by circomkit
-pragma circom 2.0.0;
-
+pragma circom 2.1.0;
include "../multiplier.circom";
component main = Multiplier(3);
diff --git a/crates/circomkit/tests/e2e/common.rs b/crates/circomkit/tests/e2e/common.rs
new file mode 100644
index 0000000..a6bf558
--- /dev/null
+++ b/crates/circomkit/tests/e2e/common.rs
@@ -0,0 +1,51 @@
+//! Shared test helpers for e2e integration tests.
+//!
+//! Requires `circom` and `snarkjs` to be installed and on PATH.
+
+use std::path::PathBuf;
+use std::sync::{Mutex, Once};
+
+use circomkit::Circomkit;
+
+/// Root of the workspace (two levels up from this crate's manifest dir).
+pub fn workspace_root() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
+}
+
+/// PTAU file used by the tests (supports up to 2^8 = 256 constraints).
+pub const TEST_PTAU: &str = "powersOfTau28_hez_final_08.ptau";
+
+static ENSURE_PTAU: Once = Once::new();
+
+/// Global lock to serialize tests that share filesystem state (CWD, build dir).
+static TEST_LOCK: Mutex<()> = Mutex::new(());
+
+/// Download the test PTAU file if it doesn't exist.
+fn ensure_ptau() {
+ ENSURE_PTAU.call_once(|| {
+ let root = workspace_root();
+ let ptau_dir = root.join("tests/ptau");
+ let ptau_path = ptau_dir.join(TEST_PTAU);
+ if !ptau_path.exists() {
+ circomkit::core::utils::download_ptau(TEST_PTAU, &ptau_dir)
+ .expect("failed to download test PTAU");
+ }
+ });
+}
+
+/// Load Circomkit from the test config.
+///
+/// Sets CWD to the workspace root so relative paths in the config resolve correctly.
+/// Downloads the PTAU file if it doesn't exist.
+///
+/// Returns the Circomkit instance AND the lock guard — hold onto the guard
+/// for the duration of your test to prevent concurrent filesystem access.
+pub fn test_circomkit() -> (Circomkit, std::sync::MutexGuard<'static, ()>) {
+ let guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
+ let root = workspace_root();
+ std::env::set_current_dir(&root).expect("failed to set CWD to workspace root");
+ ensure_ptau();
+ let config_path = root.join("tests/circomkit.json");
+ let ck = Circomkit::from_file(&config_path).expect("failed to load test config");
+ (ck, guard)
+}
diff --git a/crates/circomkit/tests/e2e/compile.rs b/crates/circomkit/tests/e2e/compile.rs
new file mode 100644
index 0000000..726d7db
--- /dev/null
+++ b/crates/circomkit/tests/e2e/compile.rs
@@ -0,0 +1,151 @@
+use circomkit::Circomkit;
+use circomkit::core::config::CircomkitConfig;
+
+use super::common::{test_circomkit, workspace_root};
+
+#[test]
+fn compile_multiplier() {
+ let (ck, _guard) = test_circomkit();
+ let out = ck.compile("multiplier_3").unwrap();
+ assert!(out.exists());
+ assert!(ck.paths.circuit_r1cs("multiplier_3").exists());
+ assert!(ck.paths.circuit_wasm("multiplier_3").exists());
+ assert!(ck.paths.circuit_sym("multiplier_3").exists());
+}
+
+#[test]
+fn compile_arrays() {
+ let (ck, _guard) = test_circomkit();
+ let out = ck.compile("arrays_2_3").unwrap();
+ assert!(out.exists());
+}
+
+#[test]
+fn compile_errors() {
+ let (ck, _guard) = test_circomkit();
+ let out = ck.compile("errors").unwrap();
+ assert!(out.exists());
+}
+
+#[test]
+fn info_multiplier() {
+ let (ck, _guard) = test_circomkit();
+ ck.compile("multiplier_3").unwrap();
+ let info = ck.info("multiplier_3").unwrap();
+
+ // Multiplier(3) with optimization=0 has 15 constraints
+ assert_eq!(info.constraints, 15);
+ assert!(info.wires > 0);
+ assert!(info.prime_name.is_some());
+}
+
+#[test]
+fn clear_removes_artifacts() {
+ let (ck, _guard) = test_circomkit();
+ ck.compile("errors").unwrap();
+ let dir = ck.paths.circuit_dir("errors");
+ assert!(dir.exists());
+
+ ck.clear("errors").unwrap();
+ assert!(!dir.exists());
+}
+
+#[test]
+fn source_mtime_not_modified_by_compile() {
+ let (ck, _guard) = test_circomkit();
+ let source_path = ck.paths.circuit_source("multiplier");
+ assert!(source_path.exists(), "source .circom file must exist");
+
+ let mtime_before = source_path.metadata().unwrap().modified().unwrap();
+ ck.compile("multiplier_3").unwrap();
+ let mtime_after = source_path.metadata().unwrap().modified().unwrap();
+
+ assert_eq!(
+ mtime_before, mtime_after,
+ "compilation must not modify the source .circom file's mtime"
+ );
+}
+
+#[test]
+fn skips_compilation_when_build_is_fresh() {
+ let (ck, _guard) = test_circomkit();
+
+ // Start clean so we own the artifact timestamps
+ ck.clear("multiplier_3").unwrap();
+
+ // First compile to create artifacts
+ ck.compile("multiplier_3").unwrap();
+ let r1cs_path = ck.paths.circuit_r1cs("multiplier_3");
+ assert!(r1cs_path.exists());
+ let mtime_after_first = r1cs_path.metadata().unwrap().modified().unwrap();
+
+ // Second compile should skip (recompile defaults to false)
+ // Sleep briefly so we can detect if the file was rewritten
+ std::thread::sleep(std::time::Duration::from_millis(100));
+ ck.compile("multiplier_3").unwrap();
+ let mtime_after_second = r1cs_path.metadata().unwrap().modified().unwrap();
+
+ assert_eq!(
+ mtime_after_first, mtime_after_second,
+ "r1cs mtime should not change when build is fresh"
+ );
+}
+
+#[test]
+fn compile_custom_templates() {
+ let (ck, _guard) = test_circomkit();
+ let out = ck.compile("custom_mul").unwrap();
+ assert!(out.exists());
+ assert!(ck.paths.circuit_r1cs("custom_mul").exists());
+ assert!(ck.paths.circuit_wasm("custom_mul").exists());
+
+ // Verify the generated main file includes pragma custom_templates
+ let main_path = ck.paths.circuit_main("custom_mul");
+ let main_source = std::fs::read_to_string(&main_path).unwrap();
+ assert!(
+ main_source.contains("pragma custom_templates;"),
+ "generated main must include custom_templates pragma"
+ );
+}
+
+#[test]
+fn compile_tags() {
+ let (ck, _guard) = test_circomkit();
+ let out = ck.compile("bounded_add_8").unwrap();
+ assert!(out.exists());
+ assert!(ck.paths.circuit_r1cs("bounded_add_8").exists());
+ assert!(ck.paths.circuit_wasm("bounded_add_8").exists());
+
+ // Verify the generated main file uses circom 2.2.0 (from per-circuit override)
+ let main_path = ck.paths.circuit_main("bounded_add_8");
+ let main_source = std::fs::read_to_string(&main_path).unwrap();
+ assert!(
+ main_source.contains("pragma circom 2.2.0;"),
+ "generated main must use overridden circom version for tags support"
+ );
+}
+
+#[test]
+fn recompiles_when_forced() {
+ let mut config =
+ CircomkitConfig::from_file(&workspace_root().join("tests/circomkit.json")).unwrap();
+ config.compiler.recompile = true;
+ let ck = Circomkit::new(config).unwrap();
+
+ // First compile
+ ck.compile("multiplier_3").unwrap();
+ let r1cs_path = ck.paths.circuit_r1cs("multiplier_3");
+ let mtime_after_first = r1cs_path.metadata().unwrap().modified().unwrap();
+
+ // Sleep so mtime will differ if recompiled
+ std::thread::sleep(std::time::Duration::from_millis(1100));
+
+ // Second compile with recompile=true should rewrite artifacts
+ ck.compile("multiplier_3").unwrap();
+ let mtime_after_second = r1cs_path.metadata().unwrap().modified().unwrap();
+
+ assert!(
+ mtime_after_second > mtime_after_first,
+ "r1cs should be rewritten when recompile=true"
+ );
+}
diff --git a/crates/circomkit/tests/e2e/config.rs b/crates/circomkit/tests/e2e/config.rs
new file mode 100644
index 0000000..a925e76
--- /dev/null
+++ b/crates/circomkit/tests/e2e/config.rs
@@ -0,0 +1,54 @@
+use circomkit::core::config::CircomkitConfig;
+
+use super::common::test_circomkit;
+
+#[test]
+fn load_test_config() {
+ let (ck, _guard) = test_circomkit();
+ assert_eq!(ck.config.circuits.len(), 5);
+ assert!(ck.config.circuits.contains_key("multiplier_3"));
+ assert!(ck.config.circuits.contains_key("arrays_2_3"));
+ assert!(ck.config.circuits.contains_key("errors"));
+ assert!(ck.config.circuits.contains_key("custom_mul"));
+ assert!(ck.config.circuits.contains_key("bounded_add_8"));
+}
+
+#[test]
+fn resolve_circuit_config() {
+ let (ck, _guard) = test_circomkit();
+ let resolved = ck.config.resolve_circuit("multiplier_3").unwrap();
+ assert_eq!(resolved.circuit.template, "Multiplier");
+ assert_eq!(resolved.circuit.file, "multiplier");
+}
+
+#[test]
+fn nonexistent_circuit_errors() {
+ let (ck, _guard) = test_circomkit();
+ let result = ck.config.resolve_circuit("nonexistent");
+ assert!(result.is_err());
+}
+
+#[test]
+fn json_schema_is_valid() {
+ let schema = CircomkitConfig::json_schema();
+ let json = serde_json::to_string_pretty(&schema).unwrap();
+ assert!(json.contains("CircomkitConfig"));
+ assert!(json.contains("circuits"));
+ assert!(json.contains("prover"));
+ assert!(json.contains("compiler"));
+}
+
+#[test]
+fn paths_resolve_correctly() {
+ let (ck, _guard) = test_circomkit();
+ let r1cs = ck.paths.circuit_r1cs("multiplier_3");
+ assert!(r1cs.to_string_lossy().contains("multiplier_3"));
+ assert!(r1cs.to_string_lossy().ends_with(".r1cs"));
+}
+
+#[test]
+fn load_input_from_file() {
+ let (ck, _guard) = test_circomkit();
+ let input = ck.load_input("multiplier_3", "default").unwrap();
+ assert!(input.contains_key("in"));
+}
diff --git a/crates/circomkit/tests/e2e/main.rs b/crates/circomkit/tests/e2e/main.rs
new file mode 100644
index 0000000..45ab4ea
--- /dev/null
+++ b/crates/circomkit/tests/e2e/main.rs
@@ -0,0 +1,13 @@
+//! End-to-end integration tests for Circomkit.
+//!
+//! These tests exercise the full pipeline: compilation, witness calculation,
+//! proof generation, and verification.
+//!
+//! Requires `circom` and `snarkjs` to be installed and on PATH.
+
+mod common;
+
+mod compile;
+mod config;
+mod prove;
+mod witness;
diff --git a/crates/circomkit/tests/e2e/prove.rs b/crates/circomkit/tests/e2e/prove.rs
new file mode 100644
index 0000000..7bada7d
--- /dev/null
+++ b/crates/circomkit/tests/e2e/prove.rs
@@ -0,0 +1,111 @@
+use circomkit::Circomkit;
+use circomkit::signals;
+
+use super::common::{TEST_PTAU, test_circomkit, workspace_root};
+
+fn setup_multiplier(ck: &Circomkit) {
+ ck.compile("multiplier_3").unwrap();
+ let ptau_path = workspace_root().join("tests/ptau").join(TEST_PTAU);
+ ck.setup("multiplier_3", Some(&ptau_path)).unwrap();
+}
+
+#[test]
+fn full_prove_and_verify() {
+ let (ck, _guard) = test_circomkit();
+ setup_multiplier(&ck);
+
+ let input = signals! { "in" => vec![2_i64, 4, 10] };
+ let proof_path = ck
+ .prove("multiplier_3", "prove_test", Some(&input))
+ .unwrap();
+ assert!(proof_path.exists());
+
+ let ok = ck.verify("multiplier_3", "prove_test").unwrap();
+ assert!(ok, "proof should verify");
+}
+
+#[test]
+fn verify_rejects_tampered_signals() {
+ let (ck, _guard) = test_circomkit();
+ setup_multiplier(&ck);
+
+ let input = signals! { "in" => vec![2_i64, 4, 10] };
+ ck.prove("multiplier_3", "tamper_test", Some(&input))
+ .unwrap();
+
+ // Tamper with the public signals file
+ let public_path = ck.paths.public_signals_path("multiplier_3", "tamper_test");
+ std::fs::write(&public_path, "[\"999\"]").unwrap();
+
+ let ok = ck.verify("multiplier_3", "tamper_test").unwrap();
+ assert!(!ok, "tampered proof should not verify");
+}
+
+/// The orchestrator surfaces capability errors before doing any work: the test
+/// config compiles on bn128, but lambdaworks only supports bls12381.
+#[test]
+fn prove_rejects_unsupported_backend_curve() {
+ use circomkit::core::enums::ProvingBackendKind;
+
+ let (mut ck, _guard) = test_circomkit();
+ ck.config.prover.backend = ProvingBackendKind::Lambdaworks;
+
+ let input = signals! { "in" => vec![2_i64, 4, 10] };
+ let err = ck
+ .prove("multiplier_3", "cap_test", Some(&input))
+ .expect_err("lambdaworks on bn128 must be rejected");
+
+ let msg = err.to_string().to_lowercase();
+ assert!(
+ msg.contains("lambdaworks"),
+ "error should name the backend: {msg}"
+ );
+ assert!(
+ msg.contains("curve"),
+ "error should mention the curve: {msg}"
+ );
+}
+
+/// The orchestrator honors `prover.backend`: switching to the native Arkworks
+/// (BN254) prover produces a snarkjs-format proof that snarkjs then verifies.
+#[cfg(feature = "prove-arkworks")]
+#[test]
+fn arkworks_backend_through_orchestrator() {
+ use circomkit::core::enums::ProvingBackendKind;
+
+ let (mut ck, _guard) = test_circomkit();
+ setup_multiplier(&ck);
+
+ // Route proving through the native Arkworks backend (BN254 / bn128).
+ ck.config.prover.backend = ProvingBackendKind::Arkworks;
+
+ let input = signals! { "in" => vec![2_i64, 4, 10] };
+ let proof_path = ck.prove("multiplier_3", "ark_e2e", Some(&input)).unwrap();
+ assert!(proof_path.exists());
+
+ // Same proving key snarkjs set up + snarkjs-format proof => snarkjs verifies it.
+ let ok = ck.verify("multiplier_3", "ark_e2e").unwrap();
+ assert!(ok, "snarkjs should verify the arkworks-generated proof");
+}
+
+#[test]
+fn witness_then_prove() {
+ let (ck, _guard) = test_circomkit();
+ setup_multiplier(&ck);
+
+ // Compute witness separately first
+ let input = signals! { "in" => vec![3_i64, 5, 7] };
+ let wtns_path = ck
+ .witness("multiplier_3", "witness_test", Some(&input))
+ .unwrap();
+ assert!(wtns_path.exists());
+
+ // Then prove (will recompute witness via snarkjs, but the file exists)
+ let proof_path = ck
+ .prove("multiplier_3", "witness_test", Some(&input))
+ .unwrap();
+ assert!(proof_path.exists());
+
+ let ok = ck.verify("multiplier_3", "witness_test").unwrap();
+ assert!(ok);
+}
diff --git a/crates/circomkit/tests/e2e/witness.rs b/crates/circomkit/tests/e2e/witness.rs
new file mode 100644
index 0000000..f2fe136
--- /dev/null
+++ b/crates/circomkit/tests/e2e/witness.rs
@@ -0,0 +1,174 @@
+use circomkit::signals;
+use num_bigint::BigInt;
+
+use super::common::test_circomkit;
+
+fn multiplier_tester() -> (circomkit::WitnessTester, std::sync::MutexGuard<'static, ()>) {
+ let (ck, guard) = test_circomkit();
+ let config = ck.config.circuits["multiplier_3"].clone();
+ let tester = ck.witness_tester("multiplier_3", config).unwrap();
+ (tester, guard)
+}
+
+#[test]
+fn expect_pass_multiplier() {
+ let (tester, _guard) = multiplier_tester();
+
+ let input = signals! { "in" => vec![2_i64, 4, 10] };
+ let output = signals! { "out" => 80_i64 };
+ tester.expect_pass(&input, Some(&output)).unwrap();
+}
+
+#[test]
+fn expect_pass_different_values() {
+ let (tester, _guard) = multiplier_tester();
+
+ let input = signals! { "in" => vec![3_i64, 5, 7] };
+ let output = signals! { "out" => 105_i64 };
+ tester.expect_pass(&input, Some(&output)).unwrap();
+}
+
+#[test]
+fn expect_fail_input_contains_one() {
+ let (tester, _guard) = multiplier_tester();
+
+ // Multiplier rejects inputs containing 1 (IsZero check)
+ let input = signals! { "in" => vec![1_i64, 4, 10] };
+ let err = tester.expect_fail(&input).unwrap();
+ assert!(!err.is_empty());
+}
+
+#[test]
+fn compute_output() {
+ let (tester, _guard) = multiplier_tester();
+
+ let input = signals! { "in" => vec![2_i64, 3, 5] };
+ let output = tester.compute(&input, &["out"]).unwrap();
+
+ assert_eq!(
+ output["out"],
+ circomkit::SignalValue::Single(BigInt::from(30))
+ );
+}
+
+#[test]
+fn constraint_count() {
+ let (tester, _guard) = multiplier_tester();
+ // Multiplier(3) with optimization=0: 15 constraints
+ tester.expect_constraint_count(15, true).unwrap();
+}
+
+#[test]
+fn constraint_count_at_least() {
+ let (tester, _guard) = multiplier_tester();
+ tester.expect_constraint_count(5, false).unwrap();
+}
+
+#[test]
+fn arrays_pass() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["arrays_2_3"].clone();
+ let tester = ck.witness_tester("arrays_2_3", config).unwrap();
+
+ let input = signals! {
+ "in" => 1_i64,
+ "in1D" => vec![2_i64, 3],
+ "in2D" => vec![
+ vec![4_i64, 5, 6],
+ vec![7_i64, 8, 9]
+ ],
+ };
+
+ // Arrays circuit just constrains relationships, no explicit output
+ tester.expect_pass(&input, None).unwrap();
+}
+
+#[test]
+fn arrays_fail_bad_sequence() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["arrays_2_3"].clone();
+ let tester = ck.witness_tester("arrays_2_3", config).unwrap();
+
+ // in1D should be consecutive (in1D[0]+1 == in1D[1])
+ let input = signals! {
+ "in" => 1_i64,
+ "in1D" => vec![2_i64, 5],
+ "in2D" => vec![
+ vec![4_i64, 5, 6],
+ vec![7_i64, 8, 9]
+ ],
+ };
+
+ tester.expect_fail(&input).unwrap();
+}
+
+#[test]
+fn errors_pass_valid() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["errors"].clone();
+ let tester = ck.witness_tester("errors", config).unwrap();
+
+ // in != 1, so in=0 is fine
+ let input = signals! { "in" => 0_i64, "inin" => vec![3_i64, 5] };
+ let output = signals! { "out" => 15_i64 }; // 0 + (3 * 5) = 15
+ tester.expect_pass(&input, Some(&output)).unwrap();
+}
+
+#[test]
+fn errors_fail_assert() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["errors"].clone();
+ let tester = ck.witness_tester("errors", config).unwrap();
+
+ // in=1 triggers assert(in != 1)
+ let input = signals! { "in" => 1_i64, "inin" => vec![3_i64, 5] };
+ tester.expect_fail(&input).unwrap();
+}
+
+#[test]
+fn errors_fail_wrong_array_size() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["errors"].clone();
+ let tester = ck.witness_tester("errors", config).unwrap();
+
+ // inin expects 2 elements, giving 1 should fail
+ let input = signals! { "in" => 0_i64, "inin" => vec![3_i64] };
+ let result = tester.calculate_witness(&input);
+ assert!(result.is_err());
+}
+
+#[test]
+fn custom_templates_pass() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["custom_mul"].clone();
+ let tester = ck.witness_tester("custom_mul", config).unwrap();
+
+ let input = signals! { "in1" => 3_i64, "in2" => 7_i64 };
+ let output = signals! { "out" => 21_i64 };
+ tester.expect_pass(&input, Some(&output)).unwrap();
+}
+
+#[test]
+fn custom_templates_wrong_output() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["custom_mul"].clone();
+ let tester = ck.witness_tester("custom_mul", config).unwrap();
+
+ // 3 * 7 = 21, not 42
+ let input = signals! { "in1" => 3_i64, "in2" => 7_i64 };
+ let wrong_output = signals! { "out" => 42_i64 };
+ let result = tester.expect_pass(&input, Some(&wrong_output));
+ assert!(result.is_err());
+}
+
+#[test]
+fn tags_bounded_add_pass() {
+ let (ck, _guard) = test_circomkit();
+ let config = ck.config.circuits["bounded_add_8"].clone();
+ let tester = ck.witness_tester("bounded_add_8", config).unwrap();
+
+ // 100 + 50 = 150, both fit in 8 bits (max 255)
+ let input = signals! { "a" => 100_i64, "b" => 50_i64 };
+ let output = signals! { "out" => 150_i64 };
+ tester.expect_pass(&input, Some(&output)).unwrap();
+}
diff --git a/examples/bun-sha256/.gitignore b/examples/bun-sha256/.gitignore
deleted file mode 100644
index 6b58145..0000000
--- a/examples/bun-sha256/.gitignore
+++ /dev/null
@@ -1,122 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
-
-# builds
-build
-dist
-
-# circuit-specific powers of tau are ignored
-*.ptau
-# universal ptaus not ignored
-!ptau/*
-# temporary ptaus are ignored
-tmp.ptau
-
-# is this still a thing lol
-.DS_Store
-
-# ignore auto generated test circuits
-circuits/test
-ptau
diff --git a/examples/bun-sha256/.vscode/extensions.json b/examples/bun-sha256/.vscode/extensions.json
deleted file mode 100644
index 5fc8eb4..0000000
--- a/examples/bun-sha256/.vscode/extensions.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "recommendations": ["iden3.circom", "oven.bun-vscode"]
-}
diff --git a/examples/bun-sha256/.vscode/settings.json b/examples/bun-sha256/.vscode/settings.json
deleted file mode 100644
index 963048a..0000000
--- a/examples/bun-sha256/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "jestrunner.jestCommand": "bun test"
-}
diff --git a/examples/bun-sha256/README.md b/examples/bun-sha256/README.md
deleted file mode 100644
index e3b49bc..0000000
--- a/examples/bun-sha256/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Circomkit with Bun
-
-In this example, we use [Bun](https://bun.sh/) with the [SHA256](https://en.wikipedia.org/wiki/SHA-2) circuit.
-
-## Installation
-
-Simply do:
-
-```sh
-bun install
-```
-
-## Usage
-
-You can see an example within [`src/index.ts`](./src/index.ts). Run it with:
-
-```sh
-bun start
-```
-
-For the CLI, you can test the entire flow as follows:
-
-1. Compile circuit: `bunx circomkit compile sha256_32`
-2. Prove with default input: `bunx circomkit prove sha256_32 default`
-3. Verify the proof: `bunx circomkit verify sha256_32 default`
-4. Create contract: `bunx circomkit contract sha256_32`
-5. Create calldata: `bunx circomkit calldata sha256_32 default`
-
-Notice that we use `bunx` instead of `npx` to use Bun!
-
-## Testing
-
-Run tests with:
-
-```sh
-bun test
-```
diff --git a/examples/bun-sha256/bun.lockb b/examples/bun-sha256/bun.lockb
deleted file mode 100755
index 8caf48cb022823e3e766ecacc13c0e9bf2b70fc2..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 38543
zcmeHw2Urx#voGMHf|3;!6G)P<T5fcW2IRfWZ4cjoDqk#W=-}~PEu6<71)8SWDU0vPN(=$tHs2fEJ_(uNR
za3fB5l$w8JxHLp&D32Wy$PMK%19|)~mXH}`EG;1>CYJLketUz9QJ?cVyUv>r;+?6y
z&>^9V^vAnT=7kMS93yG^_ZnyfA+RYC>->*2PLv4aW_#x8-
zVtf;}00prT7Zd9W^{*lA0%;|r(vaR2rGYGg5ZiTyyb_d?_N0Chl*>W+Fi4TlGyzi(
z%rX<>01ph)SRs^5=NV1)oh6&vheC>cd?1yD6nTVl1;T%}V+XTBLx9ffj!o@?SpsHY
zC@bg@AU&2~MmQVV>w`Q<*FSI?iXrwH>aqPa0WUm)B@B)SVX^%T
zNU>ZBGAMr%q*y**lu*x43A<)ghmQpLOIF_MsNTJI;8Z7G*F)@AQbka5>mu#_iB=-OjM5TFx0$x
z(y3;<;|Vr@deXzAk4DYu^hHBYNmXO4)7R%-rQ=`pnt$NkE6u`)Wxa^RjxG
zVy`ZTF4su9Zp}Eac_EnL-uY^uPM_y@*B`(9?1&uFp46AFyK-{QHpgH#PfYvv{!1Z^
zPEHqn&9}Xjb`4Wcip-oVdBx&C3zfZ}DdWK|^Y#7uI(
zJ$mZ9;>r{LD;f1u%isR9-L)t4j7KSZmkldqv#;p9=dXScoG;^9erb%+*)2UU6?XHV
zu4(7kMb7$$XU_EzA2OdQ=Z)Llf7d7kNHP
zR4*!07$H7O>*7@H9l^5Cd5@fiZtR*@t5vl6ZD;P&>U`DHY7IBkqFnd!ySbi@Irh;?
za^Q$vW}UT0j~F#&iFGP7MZHk5_+ateRfB_awNA@c?Q}jE@b;d@6|H!eYRAIW$9A)v
z7=Oj+J?b^<^ttd5yOhM~w;o(1<@2Jhly)m{+$^iNvQq7k+S2E0Bm6Ul`X|vFoGj>(Vc&P7llL(x-RB+~l09Vy)5Yo6b2rP!j9^a^cc~>#1GteOTjpq|0;%
zpLbE;hGgcxIhwdKz4J5cEwX9HB9|R^?2$P$ec6r)rgwUuelY&fxolo~{EO_qrz?-g
zymBua-r$gP$1~@`(tQb^I~7Oy9nSTXf4@LpMVn`#knu{T$6vRD0OZA~F
z?^oL$ji$LwoR=tDTJ(8swWjf*!%rrUQnM`_rm7J0DodqMSu;~B&Om=5$Ny4C-IF7Y
z=Y5!29dsp8@pb=XBd=xJhwW^nc(xZ_xn~^DDoLA>IqfDFmaG3#z`Gs>ZHNP5K9W4#
z4%!t+`w$TxK^-8rS1y7t1&k%|heWKsys<68j{$}@B0QGs;y>*O1fL3cClMaYtlNRb
zCItTp@D4Qou*|kA5WEFk*a9BNG}CMk0fL_{!Xt*1{SJONfav=PX
z;#FYMJB##(bKrOFCkpUn{E;WpXip$=9svAM5r4#y^0sJ@cAo$bYX&uc5d-RIS0H#p
z*hi!OfD9!&WZM-8J{SPQ0Z;P3+y6a)Hw8S(jaWjbEg?v|uYgDW!DB$Xu}GWX?Er}S
zYp>pjTm&BpczY2ZZLqz8;7E=5PmLzNByJhSO)5BS0MO!
zzz+jFf{;J#_1hH)z8LU^fXDG`Z{Lvt!AroRf$JyETP$nu7!bS@;K}?!{_T~4;Fkm5
z4)`Muo^O5|UjlfvA3~3$K&sV1+S$RvN!EXO-umtSrvjd=UpV%^+y9$1JPhS;^B)8Q
z=R)iM@7mu2z#9V|Sht%uwk7&I2zb(eG6%4}tw8W~B0RAjQr=#?J{S$IzlcXHuEXsK
zgbxSsp85GijBi*=;kL%`$uN%Th2_TuGX(jp$)BbJN<=35Jd|6ss70v_}2#UfpT
zPXavZpX9NwJ%O|@0=yyMQFffWsKfRIg8u|~Tt5+mSX@Ke6A0c6HoRp0hNZN*Uv1GK
z_z=M3`Uz%C>Ax);sVDetfFB}~AM4;)tX+ZN#o>Vs_rC~28Gd*E^8r5+@Th-`AWpji
z;r|-&!$o)mlJd5Qkao6kZix3U`0gvl9!IQL!+OPj5_@jUyEW%@1d*=Yb
z_fQlQGY34$ll0e|n`%hAQGg#T;*YYE^7i7F1D>q^i1}Up718kR#UdXfrvw-*&L1p8
ze0ymS{4l_i@yD{?o&O5}Pw)Tl;vWNkj7a~;pU4H5tp?K08x9;iMR){|@>apLc835z
ziiU3sN9vpTz=nCO2#@yjyY@R4@DphKe^-A6fcK{1iF|FPM1ED}ru{$4juCE$?Ft0%
z3;6Lg{&)`1oR(T#$^OZ_A!%DO5Prdc$NML?$FkqW
zrvjeb{}78hXip&guL6EBP5-34EgGbqe2=E}8}-)Sdl%9scs}6C{inUM)A4zLNBIdo
zl46_I0%`XR@Z|aZKjClPvt|85e!n|@QGmAw`B8t!|99Fwj%f7qF)SrYIABFMwmH1`+GkA!7JhiuL^=B5!SonAd@bbaWwNpr=@`2N6?!
zQEC7wwud#4DmQ`@1E$yx_J&jndt@rmQ>+KurTT5vR4s-n$`4~o1x&HsAc&|RM~E04
zA)>v)+Cv3QQ4TnZpxTe1O2x$fJ5!_&dvz+bCq@q&nMgVOpIT60=`AVBAsG)4#vDDJAZY
zuBXw(HGl+0*z@S3zVFg6e)QNJ_B4INGV%0Pb+6B^a%I2ORg_QGeCzzxn#I}iShFVJ
zZ}EsTBVs10&r7fCZ?1MEBqDLk&ie{9y0`|Cz|ak?Pv4OCJ=Z96b3(H8z=m|!(?cu7
zrm06S>vDCy?lX&?(sP!*s1>_5VUMrZo5t*+Jqi-nRj=pyhUTdceP8c2fJT?BM@XA7
zWa0vqnTJms=vCh)9J+V+H*s9(CFgYMgk)?JmG2nliI4zIrd6^E~cAiI;{9>!rrcWAuH0(
z4fDL;Lv3SoXe&VtBYVQ8mBOWSUFZ?Pz@}f}2bfLVn!)TlJ%71aM4c;HS
zz2L9XUfvsx$0_)V_c)t=cf0zQtsNitPP{&AW*Uty{XCITQL~Gu5}s78y7}R;+OB@-
zWs@!EZYkZep?6JL!-0Y~W6CF$giZQf`*eI>gZ~*Z_2{~_(l^9PtpCn`TAKfGo1rI-
zF5WjtVC+pD5-y>Aa^if45snRKmd)$bb7#K{){W^8>*gQINKx7#%Qq{o%ehnulm~@m@p%!$>zsJ3%OA<)?V;61#B!mR_Zg_U?S?
zRQT|6*mpsL1lv@ybD8se-7PsiIGHDdi|RiX&mR&t;bd8mu&c20)n{`W-7bVEqzqw?
zKKtKhAI$9adP|V|+Vt$usm5{FE4y^4@X#8%exb*Km3>ZT>liBB?~-{jqwkSJZ@oV>
z2D?1Eaci%2o`sdG&LbLK`hJz6rEGVrFd%Km`L#*AHw-!^Zy$I1P2<~fvo~LhNxpx4
zQEJhdybnh4g*tl!d(~tZURkwCH(}0}zWdIvmQSnkza&4EMwfn$!0@%=U)Xs#v~t+1
zwJrzLOAM!8Oz!_ATy=3D?;Nh}q@%)V-U>Py=d~5W2lYEVX6dKw8moZes{=0BE_s&v
zbY1na`!u@r{XHY;nDfRdaxQ5v&p1D@(lYVZjegKk?cSM6^}ol)`+ssvT;VqCTBmBa
z+lO4ced)Ylyu$&x0I_xNJ}23AF&@49Yc+k`=;yACxT)U5_g89|`Hop+dy8FvYsf77
zH8;P$E@4Jjdt?~r_(?jK?+Z7w^ZNMcUKibgoRYT^^)EIm-F6*zygFsIgl7d!UO1j@
z4h;9lf!*Kl5@(#SUb7--$?mvWsY=R+-e0?XXwL*CJ*gh<-)c^u^BF#_Kx2!pe0pU}
z)-79?2gwrimRfDfog-mzok61u$EMAJQP!w9#OXCtF0*haC;QmdU_T?P!0^`v^)9RR
zrAFx-Ty%94&+F2m$AP2oOe&GEA2wO#!n#{F1|BIVx>?$6m0w1u3&)YofuWNh`>>n8
zgsp|SiE-h?<4fl>cIvNh!#*~ZA$DcM9h8bcZ2xpb9Pq@
zDbP>Nk?fF&-^7yp1wPM^z(~|z(c8o?H}ZSGkaHT1)3@6Vx}tvO*3g&v(wozlPE0Gz
zukzKFemZK?bICO~#Mio5yg#0qW#!dZa>?|0Q(k)|B+}^Oa~}ze2EDq4kx3yPe}K+cZdF`u(Yy#`~g!g;UB`
zDQ?T5(WSo!XFQDGIJQQ~#OHW?g=6~RYW<{)onva2t3Ts)8xru!`TEwAUv$=19&+T)
z7R&4Mg?7r?
zDZ6gYERI-}wD)R_4aHW$uJ7tX$deMuJqxit2cNNJz-s=x_BSe596E&v{FutI|vr
z)`yhb?tgo{&JN!u>EwL4;L5lVM~3#N$*FdFw;}I~+Edef6^BBj0QRZU37?JC>LzyBbHct(VP$RO_3&>l
zhsMk_u#-+Pf9;t^zh98&VR3mb)r_?nX
zb@l#SqgO8R?o`}g1%H{>n^oP0_U_4=i%xY91}m<4HfZM>v&|9bM~UC+YA7>s-l?GO
zR*GLo+0p3sCPX1+T-i4}x4)yCSNU?SA-v#e1Kh9po$x#2Cga|F@`Hp3x#_W%52j2_
zk=Q8Qa{HLcm$(-TZ#`4qaHL$)(IC+$AVpD;MibK~~w_VpWJKufe^*yEuOO)yo|`
z^MiGSW52zsGS|#9Gp2phl};UB>e;aDRc1k&l7VD=Zdv^f>3~(7XU9LP)h*UuxI@#2
zMpuJQckQN4wdUNVD&_MRZd!G0W=$Dm#W}YV*FT&-^UaN+z4W$0rHSvz@?+DTg2EoJ
zK5Oq6l4>Psl3>2<#OkRt4K|p6rqR`;(~Td>y!Cvf%*3t>pIg>b>rh4G$!wlRHH^6?|I#(Tzq|i%!?-&8NFJ
z9_$UgmUO~xvF~a}w}LUnySNXIkJ%)ZFY&HshK1tGOqqaFbJvDR9#iPd8gzQg+G9(4
zCrfL*?Rlhejq4>EU2Qtu9g|dU@B^acR&<)h<|LmwoxR6xe3et^jJl_HTur;C&95j|
zJN2S>n&S93+B3CeOHz-i=|1WzrTQ`>S4>;0Xa@d9g{)_IZbkxQoVDtNEo$nVOZIc<3Nm7z4cx^%i-wQ651s@z#@P-E%Jp8j_Gto)ob
z&u9H7XBbm0$HV_SE)1y=k(OML-`nGhUr)r+&z#+7tb$AU?`|ZRrn|l2;VwU
zJ$>E+Zg^3KgtHfKo8KUnWA`^U>ghyJnR%4gEoS29xmjUT(;hx?t>}EVW6Y`5-J-)6
zD&9FqpBGF*6jBDG;z*~p8OCSr#+I;igJvvPJKyiyU|S2*>*FScEmd_}Uv|M{41egc
z3e65mu^!1@%Gr^3co8A2GcuL7s#PmzM9}2Lb5;@6}7_xB4$^;>=P&IaGCm6*L?m}Y%JW&fl+y@bM0ll5{bLyN4AUN{v-qf5?h
zkv1bHWTA@e?X9H+3zp1roLMzxD1Y?an-d>9b-ngT`j*(xn%;&_KlM2yy}_(5!6G&|
zC?{8K?*4#g?mw6Q1dZkqO4EM7My3?)xaD&`_4_D4?myRa%eF-^7M%s+O?p5i2
zF1>bn^zH5r_pUZ%-aGRl;(gYL$1Kw!XFV{H4zxQStmva-ddY#_=ZnFO9
zxsvbB*&LtzYDsa(d*&v#d*ji|^Eb}=>(H0mZ?xUSjOgbYW`roD44>8P7k<~T+%5ff
zGIisew~N*K)RiJHN-Ff(0UivKUrA+?T*kt;B)1sLw$lFK7;5&C$73YwC^(<$rE?1vi
zOCKj~qTf)Xu&BgI^|R6QtrwPj@Af(=F7DR+xW0xRoozXZ_m^opd3wgEm&D$jO_LYi
z6*ULO$jQd(Vd)E#hjn|QF6jIDYigFlyLmZE^<}FK4)z*3;W8)qb4F=q_@=y+pfzbl
zGd}3vjR-wzfAdSsnLg5t`$|jX~^g|dEC7LI%_^}u(=kcAXR)tZ~fH+5@*f?)9Avxz2?9e7||o8
zU`q9VO|cWUH6OckKNvqh?EbOGo1PD)$9EX}V&H4bz%^XQO@?~D9^S9*WSO2%XFCj5
z>2Yg+l}lM?opIVUx;D*JLBAL06d!2dUEDHo=-H`rJ~7fPWSNO(eizrDoow+?>T%a9
znetI1C)Tk_u0I>nePib@yK+{RHQE}^W8_FPlh41H@Q_BAyf;DejJ_STrMF1ANaSSg
z)w+4;i1^NPlfRm9H%3^hOY+jx*12iPUYoSme}Bh`pY5zv<$}~1SNyy?_Ipw>*DQed
zX1yx?zKQQ&NMKBo)9byvJm4C?zCn0A(w}$9LFS%O2lj6JQl8w^kB&Ru)h*1r6R@P)
z{L6MqHzSAcf2QDiC+C@t3A4k3r^-_0c{F)#2~kKHE;sIXJhMMS;hWESk2&Y241F`m
z+*kWe2Wia}QxB~g+>OJuG5_>^(18A%jF)aOdl|8PP<+h_tyk5Z)`pBP>6`uGD2=Wi
zoo@2lzx8(v?SFWY#@1KX6Z?YhV)Ki2(h*^6Vl*3;m0Tl&mju_b1v>qF&SBYV9VO*ZRNW~WW^xjoc|=5Po2cB$gs
z*?A{)(DK5yGtGiyWM`ZCpk5#+?)0jd(HQo
zW$;*kKx&!a*m2>4`&!kb^vYHYDhiy@RlH5;%DWRt2m!D*^zW-S9jk#PhZKteB;K}6qxkoR-e_iWSyqb
z9ZaYD{JHwRSr)2O($r~QHk6Lhgs^bn@t2|tClD^
zRdk&wS@%sozjtjq-?cV%9{#48?9YbK>E2YF@KJ3=0ozPXWwLUX+tk4Ps2wvW-}n$Q
z->&4*fsvOkJmi0QqoTEYuXEati4&dP*|D9UEq^N~CSG&&k*iHN6B^y2bha>L4d+&X!H=Md;&
zFj(T8Y}PkX-C
zDrq!mT+)Neh)q_WgBD+um-98NwZEC2G~CYJmEkq<@3&v1V{2BuGa509Mt1}u3Mr$9
z@TP@!XDQk0h&k0kPb_o2bcfHcd{?oKsg*C8r+?_UYW!pI=LT^T);6eGEUCDlB$K64
z8SRvP#k)g}%gDid%V~67>2$X-ydNvZG#0WwHCKI*+CFlbNyWiYGB!GWCBC|6iI2Lx
zc!QfzQF8O0-i^jaKFYP?d4JtLlXhwFbBEG~*Jk@K&!N#BNvB)2YP8;C^!B@t&&nDTIFK5c~yIOI!MEfoaJ$Z}1?u?<+J^N9k
zoyrBfi+#-KbSpOBgA6
zu5fZ~*F}kSV=c3;(ByTa(~UlC{PlTo>Y_vP5k>xs3c_SPe5NN2Oy3ePq1ME2-!|L#
zD^G48V|$-cGvapSx5_8iUvAK|V>hmQruVXLLca95zlt9E
zpT22Elh?hOD%kJxGa4oLqAzKk8}g)j>b-fVhddahyw>x$uI_zK_hBxdz8zki);ZRB
z{`HfwwcGc+-)`oV>k}P&YW$#mH!pX&saNubMt2;Y?pxV&;yvCg@^>D6e^&13VZNl|
zjXi7M)rLh5NZJ__oKzJ&>2|eg(em$iMs`_uI;~Eo?(H_8Iq#PB(_1gQZi2YkEE-+>
z4uS;6>hBl0a~%9Vj!v;nlKybrCaSO6%vaXCf_A8cUom-kHKpg}E}8?1CYSAvU*BVu
z*@FwN^Ce?0P10A+i(HhhF1_jqU_O6jFwBhe4(bUL@6rS}W@mS8OgB{`Sz~iF*c&
zK6sz~Ov{LUK~Q?Gw`17uUY_Ij78aiW!sv8v^Z@H=p^HN{CG2v!eq}$6?nFA>4J*H|
zE1Pd?=yx+!dwOp0y!o5v>6Gk$zYHxRrfY%
z{}Z*d+;wV7GCkhY=z7rU_Ocs#^8NPMO5fE<2~RTn=m<-6^19!>f7JR~oyYf!bC0gR
z#d?0*#;4Pw4F|OIrzu~G>u55l%&1t#e{-Pw*WKo98r?~Bx?Z98>(`gu7?67HnAVFT
z7hQYrn8iWe-c9=E-A&d0Y-;ag#>G?j>q$Ag?K#=|>c~}lpRCrat#I4qrnK*o9dA%M
z{W%N2TOollH2#9*mn=iasgdidGK(s5;@@rW^(ypn*`-d0x5MeqBbVj-{WMOeRDjWi0QaYEgdhTQskidf3CKd%SkNdpvf#
zTH20+`-YAWM+69#NHJIDEA$CF*Gaj1z(KoZ_uN?C8Md2m99OVSO>y2MN7I8Bo$kAj
zR|_gFUhmtje&uS>QtzyTbE8tiB#+4M`>xW({%-wyos{6RW$JqZ(z@PR{ye>U5Mz5o
z$6Y19Awf3YhZ)@s&eP~lq0<$=ymgD5L+Vpoqr9!(I_((QV`y@2-Kt{0>JQ5XJp1}7
z@XW`;y)lhmyUJ!(E-xE@c|rF2zDLdFcfT_}+^D>L{v`p8t~Z@-;^qM|3CZadf#mr%%qA4y^iQX*117euh;*
z)rqS#y7-+D35*e=`t=g({WZ54}EN7Q6yCamu)ac{nRsmtWdGKZAeo?T3j
z4bWwdef%-6bW_DrZ$aq6&jAl#-93EUM3qLDe7A(O8L}G>s^*Lr4y|zB{VZ0|)9$fs
zPTDd?xp%dyhfDs|Wwp11molDhv%7!lbiPom_PEN)*U_m5qP3$YmEVyYwe=nSTp7QA
zB7t#5qq8~3Gx@%!%cDR;W`{ejgS6a&Cf=6Loi+IFbFE2ro&)xJ#C(fODR}1UrQhqL
zrHV|%&2DGXx1Cyk$jbfTMf!U^KSC5zMuL@MX|V6{0eO>ooxRGg)#MlEhON)D3}3BR
zwp)A8=IP58^iiy{>gzD(e&k}Q;xqbHo5r}Cxm`J+vn6p^#n+F!XnJ7L>0Wqbr59fT0+VjDp
za`dV!#<&4Y{2M%EpXyJin-)CNJTKQ~rl#bWBXWv2zkhv`c5JX;l-NpximbSMpR3;Q
zS^cF{zR$pI%O08KAMW&Uebkq&2_G7ZBFgVs|GmYsm`0aPr@Oznu;_!2PwRFkHa$ZcbAIc8s2TkLGr&Upbp62gqp%=`kOphM*cn&Df?fgl;7wq>Wa+@
zWk+*`VzKa13w$RM_Z#s4xG4-36-fFY6Z(G$L|IAvUABnBZ-)LS_4jxC_^16J4g62j
z0M1AJ*6jcHdHEY#h5ufj=7fqk%sf_@jY88u+7uKN|R>fj=7f
zqk%sf_@jY88u-7Yft8}yd7gny45V!Ze6}GsTp(nHh8hy5DuxsI9FD!3sj-@X8_VGZ
z8mO78u|l~);k-cVXSI!gHwC2O2NBQe@T?z$A4JUKdlvF74@*?m0aEfiSAS6%o