zorch_12

bellman-zorch

snark1

GPU Groth16 prover for bellman (BN254). The h-FFT and all five MSMs in one fused GPU call, byte-identical to groth16::create_proof.

bellman-zorch

A GPU Groth16 prover for bellman: it synthesizes the circuit with bellman's constraint system, runs the entire proving back-end — the h-polynomial FFT and all five MSMs — on the xla GPU plugin in one fused PJRT call, then finishes with bellman's exact proof assembly on the CPU. Over BN256 (= alt_bn128 = BN254, the xla kernels' curve), so it drives a real-world prover (the one behind Zcash Sapling / Filecoin) with a compiler-generated GPU core instead of hand-written ec-gpu kernels. The proof is byte-identical to groth16::create_proof.

Setup

Needs an NVIDIA GPU (CUDA), a Rust toolchain, clang/libclang (the in-tree xla-pjrt shim generates its PJRT bindings with bindgen at build time), Python 3.11, and uv. The crate has one external path dep — bellman (the zkcrypto 0.14 mainline) — expected next to this repo:

git clone https://github.com/zkcrypto/bellman ../bellman   # the ../bellman path dep

Install the matched frx 0.10 GPU stack from the public Fractalyze package index (this provides frx_plugins/xla_cuda12/xla_cuda_plugin.so plus the lax.ntt/lax.msm frx distribution used by the exporter):

uv venv --python 3.11 .venv
uv pip install --python .venv --index-strategy unsafe-best-match \
  --index-url https://fractalyze.github.io/pypi/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  frx==0.10.0.dev20260716113241 frxlib==0.10.0.dev20260716113241 \
  frx-cuda12-plugin==0.10.0.dev20260716113241 frx-cuda12-pjrt==0.10.0.dev20260716113241 \
  zk-dtypes==0.0.10 numpy==2.4.3

Point the env vars at that venv — copy-paste from the repo root:

export XLA_VENV_PYTHON=$PWD/.venv/bin/python
export XLA_PJRT_PLUGIN=$PWD/.venv/lib/python3.11/site-packages/frx_plugins/xla_cuda12/xla_cuda_plugin.so

Running

# CPU sanity, no GPU needed:
cargo test

# GPU byte-match on the 322-round MiMC (export its core, then run):
FRX_PLATFORMS=cuda,cpu "$XLA_VENV_PYTHON" export/export_bellman_core.py 1024 647 2
XLA_BELLMAN_CORE_MLIRBC=$PWD/artifacts/bellman_core_n1024_m647_i2.mlirbc \
    cargo test --test gpu_mimc -- --ignored

# Benchmark sweep (exports a core per size, then runs examples/bench.rs):
bash bench.sh                                          # default 2^13..2^18

The MiMC test prints its (n, m, num_inputs) shape, so a new circuit is just "read the shape, export the core" — the Rust is unchanged.

Usage

// Dense CRS. (Setup is Wnaf-free — halo2curves BN256 doesn't implement
// group::WnafGroup; `gk.to_parameters()` gives a bellman `Parameters` too.)
let gk = bellman_zorch::setup::generate_random_gpu_key::<Bn256, _, _>(circuit, rng)?;

// One proof — XLA_BELLMAN_CORE_MLIRBC points at a core exported for this shape:
let proof = bellman_zorch::prove::create_proof(circuit, &gk, r, s)?;

// Many proofs of one circuit — upload the proving key to the device once:
let pk = bellman_zorch::gpu::prepare(core_path, &gk);
let proof = bellman_zorch::prove::create_proof_prepared(circuit, &pk, r, s)?;

The back-end is one exported executable, bellman_core (export/export_bellman_core.py) — the h-FFT (lax.ntt, bellman's exact convention: generator 7, no bit-reverse) and the five lax.msms, fused. lax.ntt/lax.msm lower shape-specialized, so each core is fixed to one (n, m, num_inputs) shape and the proving-key points are runtime inputs.

Benchmark

GPU core vs multi-threaded bellman CPU (RTX 5090, MiMC sweep; each size's first GPU proof is asserted byte-identical). GPU = key uploaded once, reused:

roundsnCPU msGPU/keyGPU/oncespeedup
4000819229.09109.27107.570.27x.
80001638453.80120.64117.310.46x.
160003276899.73116.04108.900.92x.
3200065536187.34129.70115.501.62x.
64000131072359.21153.91128.712.79x.
130000262144674.87223.47161.044.19x.

GPU/once phase breakdown (ms/proof)

ntotalserialh2ddispatchd2hrest
8192107.570.230.21105.290.271.57
16384117.310.450.29114.060.262.25
32768108.901.070.47103.300.283.78
65536115.502.500.73105.490.256.53
131072128.713.601.20111.390.2312.29
262144161.047.212.21126.740.2924.59

Development

Install the git hooks with both stages named. Plain pre-commit install wires only the pre-commit stage, which leaves the commit-message linter inactive — a malformed commit message then sails through to CI:

pre-commit install --install-hooks --hook-type pre-commit --hook-type commit-msg

Commit messages follow Conventional Commits: a valid type, a lowercase summary with no trailing period, a header of at most 80 characters, and a body on everything but docs. A scope is optional; when used it is one of prove, setup, gpu, export, tests. The same linter runs in CI over every commit in a pull request and over the PR title.

rustfmt and clippy are not wired into the hooks: the crate takes bellman as a ../bellman path dependency, so cargo cannot read the manifest without that sibling clone and the hooks would fail for anyone who has not made one.