Metadata-Version: 2.4
Name: groth16py
Version: 0.1.0
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Security :: Cryptography
Classifier: Intended Audience :: Developers
License-File: LICENSE
Summary: arkworks-backed Python library for BN254 Groth16: setup, witness, prove, verify, Solidity verifier export — no Node.js
Keywords: groth16,zk,snark,arkworks,bn254,circom,ethereum
Author-email: luksgrin <lucas@spearbit.com>
License: GPL-3.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/luksgrin/groth16py
Project-URL: Repository, https://github.com/luksgrin/groth16py

# groth16py

A **general**, [arkworks](https://arkworks.rs)-backed Python library for the **BN254
Groth16** lifecycle — setup, witness generation, prove, verify, and Solidity verifier
export in **pure Rust**.

Not tied to any one circuit: the number of public inputs, the VK, and the circuit inputs
are all general. It spans **both** Groth16 generations — the modern circom-2 / snarkjs-ceremony
stack **and** the legacy circom-1 / snarkjs-0.1.x / websnark stack — so it can generate a proof
that an **already-deployed** legacy verifier accepts, with no redeploy and no Node. The [tornado
`withdraw` circuit](https://github.com/tornadocash/tornado-core/blob/master/circuits/withdraw.circom) is the driving example.

Almost every capability maps to an
existing arkworks / circom crate; this repo is *glue* — PyO3 bindings, encoding adapters, and a
Solidity template. The one deliberate exception is legacy proving, where no maintained crate
exists (see [the toolchain landscape](#the-toolchain-landscape)).


## Features

| | |
|---|---|
| **Verify** | in-process, pairing-only (no witness, no WASM); Ethereum `bytes` or snarkjs JSON proof layouts |
| **Compile** | both generations in-process — circom-2 (compiler crates) **and** circom-1 (`circom@0.0.35` run in a pure-Rust JS engine, wire-identical to the original); circomlib + sources vendored |
| **Prove (modern)** | fully in-process from `.circom` — circom-2 compiler + `wasmi`, no wasmer/protoc/Node |
| **Prove (legacy)** | import a websnark `.bin` or snarkjs-0.1.x `.json` key and produce proofs a **deployed** verifier accepts; circom-1 witness runs in a pure-Rust JS engine — no Node |
| **Setup** | single-party from `.r1cs`/`.circom` (labelled unsafe), **or import a snarkjs ceremony `.zkey`** (production path) |
| **Solidity export** | general over any BN254 VK; canonical snarkjs `(a,b,c)` or websnark `bytes` ABI; reproduces a deployed verifier's constants byte-for-byte; anvil-verified |
| **Pure Rust** | no wasmer, no protoc, no clang; `abi3` wheels install with no toolchain |


## Install

```sh
pip install groth16py     # abi3 wheels — no Rust, no Node, no build tools
```

## Quick start

A complete prove-and-verify, start to finish: prove you know two factors `a, b` of a public
product `c`, with the whole circom → witness → proof pipeline running in-process.

```python
import groth16py

# 1. A tiny circuit: c = a * b, with c the public output.
with open("mul.circom", "w") as f:
    f.write("""
pragma circom 2.0.0;
template Mul() {
    signal input a;
    signal input b;
    signal output c;
    c <== a * b;
}
component main = Mul();
""")

# 2. Setup — compiles the circuit in-process (single-party; UNSAFE for mainnet funds).
pk, vk = groth16py.setup_circom("mul.circom")

# 3. Prove a=3, b=4 — the witness is computed in-process by running the circuit.
proof = groth16py.prove(pk, {"a": 3, "b": 4})       # -> Ethereum `bytes` proof

# 4. Verify against the public output c = 12.
verifier = groth16py.Verifier(vk)
assert verifier.verify(proof, [12]) is True
assert verifier.verify(proof, [13]) is False

# 5. Emit a Solidity verifier for on-chain verification.
with open("Verifier.sol", "w") as f:
    f.write(vk.to_solidity())
```

Other entry points for the common flows:

```python
# Verify existing snarkjs/websnark proofs — no circuit needed:
vk = groth16py.VerifyingKey.from_snarkjs_file("verification_key.json")
groth16py.Verifier(vk).verify(proof_bytes, public_inputs)

# Prove against a trusted-setup ceremony key (modern production path):
pk, vk = groth16py.import_zkey("circuit.zkey", "circuit.circom")
proof = groth16py.prove(pk, inputs)

# Prove from a precomputed witness produced by any tool:
proof = groth16py.prove_wtns(pk, "circuit.wtns")
```


## The toolchain landscape

Groth16 tooling comes in two generations that are **wire-incompatible**: a proof verifies only
if the proving key, the witness, and the prover's FFT domain all agree. groth16py implements
both generations and bridges the encodings, so you can meet an existing deployment where it is.

### The tools

| Tool | Gen | Role | Artifact / convention |
|---|---|---|---|
| **circom-1** (`circom@0.0.35`, Node) | legacy | circuit compiler | one JSON (e.g. `withdraw.json`): R1CS **+ templates as `eval`-able JS** witness code |
| **circom-2** (iden3/circom, Rust) | modern | circuit compiler | `.r1cs` + a `.wasm` witness calculator |
| **snarkjs 0.1.x** (`"groth"`, Node) | legacy | setup / prove / verify / Solidity | proving-key JSON (`polsA/B/C`, `A/B1/B2/C`, `hExps`, `vk_*`); FFT generator **g=5** |
| **websnark** (browser, WASM, Node) | legacy | prover | binary proving key `.bin` (Montgomery); FFT generator **g=7** |
| **snarkjs** (modern) | modern | ceremony / prove / verify | `.zkey` (Montgomery binary); `CircomReduction` QAP |
| **arkworks** | — | the crypto core groth16py is built on | native two-adic root of unity = generator **g=5** |

The classic legacy pipeline (followed for example by [Tornado](https://github.com/tornadocash/tornado-core)): `circom-1` compiles → `snarkjs setup` makes the
proving/verification keys → `websnark buildpkey` re-encodes the PK JSON to `.bin` → the browser
`websnark` prover proves → `snarkjs generateverifier` emits `Verifier.sol`.

### The four axes that must line up

Get any one of these wrong and the pairing check fails:

1. **Witness calculator.** circom-2 emits a `.wasm` module. circom-1 emits **JavaScript**
   embedded in the circuit JSON — template functions `eval`-ed against a `ctx` host object
   (`setSignal`/`getSignal`/`callFunction`/…) with a `big-integer`-style API. There is no
   `.wasm`; running it needs a JS engine.

2. **FFT domain generator.** Setup places the R1CS constraints on the powers of a root of unity
   ω; the prover must interpolate the quotient polynomial `H` on the **same** ω. snarkjs 0.1.x
   and arkworks derive ω from generator **g=5**; websnark uses **g=7** (`buildFFT(…, 7)`). A key
   made under one and proven under the other yields an off-domain `H` → invalid proof. **The
   generator is not stored in the key.**

3. **Proving-key encoding.**

   | | field form | point at infinity | Fp2 order |
   |---|---|---|---|
   | snarkjs 0.1.x JSON | decimal, standard form | `z == 0` (projective `[x, y, z]`) | `(c0, c1)` |
   | websnark `.bin` | Montgomery, little-endian | `(0, 1)` | `(c0, c1)` |
   | modern `.zkey` | Montgomery, binary framed | `(0, 0)` | `(c0, c1)` |

   …plus the Ethereum G2 Fp2 **word swap** in proof/VK byte layouts.

4. **QAP reduction.** `LibsnarkReduction` (arkworks native), `CircomReduction` (snarkjs modern
   `.zkey`), and websnark's `H = top-N coefficients of A·B` on the g-domain are three different
   `H` layouts for the same circuit.

### How groth16py replicates each

| To replicate | How | API |
|---|---|---|
| circom-2 compile + witness | circom compiler crates → `.r1cs` + `.wasm` → [`wasmi`](https://github.com/wasmi-labs/wasmi) interpreter | `setup_circom`, `prove` |
| **circom-1 compile** | runs the actual `circom@0.0.35` compiler in [**boa**](https://github.com/boa-dev/boa) (pure-Rust JS engine, no Node) — output is **wire-identical** to the original, so it stays compatible with the deployed keys | `compile_legacy` |
| **circom-1 witness** | runs the `eval`-able templates in boa | `prove_legacy` |
| modern `.zkey` proving | `CircomReduction` | `import_zkey` + `prove` |
| **websnark / snarkjs-0.1.x proving** | a manual **g=7/g=5 NTT** Groth16 prover; the domain is **auto-detected** (build on each, keep the one that verifies against the key's own VK) | `import_websnark_pk` / `import_snarkjs_pk` + `prove_legacy` / `prove_wtns` |
| all three key encodings | encoding adapters normalize Montgomery↔standard, the infinity conventions, and the Fp2/word order into arkworks points | the importers above |
| snarkjs / websnark verifier | pure-arkworks pairing + both Solidity ABIs | `Verifier`, `to_solidity` |

Both circomlibs and the circuit sources are **vendored in the wheel** (`circomlib_path(1|2)`,
`circuit_path(name, 1|2)`), so either track runs from a bare `pip install` — no npm, no Node, no
external checkout.

### Recipes

**Modern (Track 2) — fresh circuit, one deploy** (vendored circom-2 circuit + circomlib, nothing external):

```python
pk, vk = groth16py.setup_circom(groth16py.circuit_path("withdraw", 2),
                                [groth16py.circomlib_path(2)])   # or your own .circom / import_zkey(...)
proof  = groth16py.prove(pk, inputs)                             # circom-2 witness via wasmi
open("Verifier.sol", "w").write(vk.to_solidity(style="bytes"))  # deploy this, then verify on-chain
```

**Legacy (Track 1) — target an already-deployed verifier, no redeploy, no Node, no committed artifacts:**

```python
# 1. Recompile the circom-1 circuit from source — wire-identical to circom@0.0.35, so it stays
#    compatible with the deployed keys (both circuit source + circomlib are vendored):
groth16py.compile_legacy(groth16py.circuit_path("withdraw"),   # circom-1 source
                         [groth16py.circomlib_path(1)],        # circom-1 circomlib
                         out="withdraw.json")
# 2. Import the deployed key (websnark .bin; a snarkjs-0.1.x .json works the same way):
pk, vk = groth16py.import_websnark_pk("withdraw_proving_key.bin",
                                      "withdraw_verification_key.json", "withdraw.json")
# 3. circom-1 witness in-process (boa), prove on the auto-detected FFT domain, verify:
proof = groth16py.prove_legacy(pk, "withdraw.json", inputs)    # inputs: {signal: int | nested[]}
assert groth16py.Verifier(vk).verify(proof, public_inputs)
```

Both flows are validated end-to-end on the Tornado `withdraw` circuit: Track 1's recompiled
circuit is byte-identical (constraints + wire order) to the committed one and its proof verifies
**on-chain against the deployed `Verifier.sol`** (anvil `verifyProof == true`); Track 2's proof
verifies against a freshly emitted verifier.

This is validated end-to-end: a `prove_legacy` proof for the Tornado `withdraw` circuit verifies
**on-chain against the deployed `Verifier.sol`** (anvil `verifyProof == true`; tampered inputs →
`false`), with no Node.js anywhere. If you already have a legacy-order witness from any tool, use
`prove_wtns(pk, "withdraw.wtns")` instead of `prove_legacy`.


## Build tiers

- **Wheels / `maturin`** bundle the `native` feature (circom-2 compiler + `wasmi` + `boa`) and
  the vendored circomlibs/sources, so `pip install` users get in-process `setup_circom` /
  `compile_legacy` / `prove` / `prove_legacy` with **no build tools and nothing external**.
  Building the wheel *from source* compiles the circom and boa crates (pure Rust; a few minutes,
  no protoc/clang).
- **`cargo test` / the crypto core** use the default (no-native) features — encoding, verify,
  `setup`, `prove_wtns`, and the Solidity emitter are pure arkworks, GPL-tooling-free, and build
  in seconds.


## Solidity verifier styles

The emitter is a **template substitution**, not a reimplementation, and is general over any
BN254 Groth16 VK (nPublic, IC length, and constants are all parameterized):

- `style="snarkjs"` (default) — canonical `verifyProof(uint[2], uint[2][2], uint[2], uint[N])`,
  matching `snarkjs zkey export solidityverifier`. Use for fresh deployments.
- `style="bytes"` — websnark/tornado `verifyProof(bytes, uint256[N])`, decoding `uint256[8]`
  with the Fp2 word order. Reproduces a deployed tornado verifier's constants byte-for-byte.


## Potential improvements

Two that would be genuinely fun to build:

- **Node-free trusted setup (ceremony).** Today `setup`/`setup_circom` are single-party and leak
  toxic waste (dev-grade only); production keys come from an external ceremony imported via
  `import_zkey`. A pure-Rust **phase-2 (Groth16) MPC contribution** flow — accept/verify/append
  contributions, in-process, no Node/snarkjs — would let groth16py *run* a ceremony end to end,
  not just consume its output.
- **Seeded / reproducible setup.** `setup(..., seed=…)` (a deterministic CSPRNG instead of system
  randomness) so a *dev* verifier can be regenerated bit-for-bit from source without shipping key
  blobs — handy for CI and reproducible builds. Explicitly dev-only: seeding real ceremony
  entropy would be a security footgun, and it does **not** make the *deployed* legacy keys
  reproducible (those need the original ceremony).

Smaller polish:

- **Faster / cached witness generation.** Both witness backends are pure-Rust interpreters, so
  witness computation dominates large-circuit proving: `wasmi` for circom-2, and `boa` for
  circom-1 (~3–4 s for the 28k-constraint `withdraw` circuit, much of it re-parsing the ~19 MB
  circuit JSON on every call). Caching the parsed circuit / `.r1cs` / `.wasm` (or a JIT backend)
  would cut this; `prove_wtns` with an externally-computed witness is the escape hatch when
  latency matters.
- **Faster circom-1 compilation.** `compile_legacy` runs the whole `circom@0.0.35` compiler under
  boa (~2–3 min for `withdraw`), since boa is an interpreter. It's a one-time, off-the-hot-path
  step (compile once, reuse the `.json`), so this is low priority — but a faster JS engine or a
  native front-end would help.
- **Proof/VK interop formats.** Proofs are emitted as the Ethereum `bytes` blob; a snarkjs-JSON
  proof + `publicSignals` export (and its inverse) would round out interop.
- **Ergonomics.** Richer input types (hex/decimal strings, not just ints), a fully-typed inputs
  mapping in the stubs, and finer-grained error messages.


## Security

Native setup is **single-party, not a ceremony** — never use its output for mainnet value;
import ceremony VKs (or a legacy deployed key) for production. On-curve + correct-subgroup checks
are enforced on all imported points. The proving RNG is seeded from a secure source, never a
fixed seed. Legacy proving self-checks each candidate proof against the key's own VK before
returning, so a domain/encoding mismatch fails closed rather than emitting a bad proof.


## License

**GPL-3.0.** In-process witness generation links the circom compiler (GPL-3.0), and the legacy
witness harness is derived from snarkjs 0.1.x (GPL-3.0), so the whole library is distributed
under GPL-3.0 — see [`LICENSE`](./LICENSE). The emitted Solidity verifier templates carry their
own MIT headers (Christian Reitwiessner / OKIMS) and are independent outputs.

