groth16-webgpu
v0.1.0
Published
Groth16 (BN254) zkSNARK prover with FFT + MSM in pure WGSL/WebGPU. Zero production dependencies. Consumes snarkjs .zkey + .wtns, emits proofs that snarkjs.groth16.verify accepts.
Maintainers
Readme
groth16-webgpu
Groth16 (BN254) zkSNARK prover with FFT + MSM in pure WGSL/WebGPU. Zero production dependencies. Consumes snarkjs .zkey + .wtns files and emits proofs that snarkjs.groth16.verify accepts.
~2.3× faster than rapidsnark C++ on RSA-2048 (149k constraints): ~300 ms vs ~676 ms.
Features
- Pure GPU compute — FFT and MSM pipelines run entirely on the GPU via WebGPU/WGSL.
- Zero production deps — no snarkjs, no bigint libraries at runtime.
- snarkjs-compatible — consumes standard
.zkeyand.wtnsfiles, produces proofs that passsnarkjs.groth16.verify. - Hot-path optimization —
new()does all one-time work (shader compilation, base upload, scratch allocation);prove()is a minimal-latency hot path. - Five MSM jobs batched into a single GPU submission (G1-A, G1-B1, G2-B2, G1-C, G1-H) with concurrent execution on capable GPUs.
- Sorted-bucket Pippenger for all MSMs, with parallel ones-reduction for witness-skewed scalars.
API
import { Groth16Prover, prove } from "groth16-webgpu";Groth16Prover (reusable, recommended)
// ── Full initialization (one-time) ──
const prover = await Groth16Prover.new(zkeyBytes: Uint8Array);
// ── Hot-path proving (per-proof) ──
const result: ProveResult = await prover.prove(wtnsBytes: Uint8Array, opts?: ProveOptions);
// ── Cleanup ──
prover.dispose();Groth16Prover.new(zkeyBytes)
Parses the .zkey, acquires the WebGPU device, compiles all shaders, uploads MSM base tables and FFT twiddles, builds QAP CSR tables, and pre-allocates all scratch buffers.
Returns a Groth16Prover instance with an initProfile property containing timing breakdown:
interface InitProfile {
zkey_parse: number; // Parse the .zkey on the host
device_init: number; // Acquire WebGPU device
shader_compile: number; // Compile shaders + build pipelines
upload_bases: number; // Upload MSM base tables
upload_twiddles: number;// Upload FFT twiddles
build_csr: number; // Build QAP CSR tables
prepare_msm: number; // Pre-allocate MSM scratch
total: number; // Total init time
}prover.prove(wtnsBytes, opts?)
Hot path. Parses the witness, DMAs it to the pre-allocated GPU buffers, runs BuildABC → Coset FFT chain → Batched MSM → host assembly.
Options:
interface ProveOptions {
r?: bigint; // Optional blinding factor (random by default)
s?: bigint; // Optional blinding factor (random by default)
verbose?: boolean; // Print detailed profiling and debug output
}Returns:
interface ProveResult {
proof: ProofPoints;
publicSignals: string[];
profile: ProveProfile;
}
interface ProofPoints {
pi_a: [string, string];
pi_b: [[string, string], [string, string]];
pi_c: [string, string];
protocol: "groth16";
curve: "bn128";
}
interface ProveProfile {
io_parsing: number;
gpu_prep: number;
build_abc: number;
fft: number;
msm: MsmStageProfile;
assembly: number;
}The proof object is in the exact format expected by snarkjs.groth16.verify.
prover.dispose()
Releases all GPU buffers (bases, twiddles, CSR tables, scratch, pipelines). The prover must not be used after disposal.
prove() (one-shot convenience)
const result = await prove(zkeyBytes, wtnsBytes, opts?);Creates a temporary Groth16Prover, runs one proof, disposes. Convenient for one-off use but incurs init cost every call.
Quick Start
import { Groth16Prover } from "groth16-webgpu";
import * as snarkjs from "snarkjs";
const zkeyBytes = await Bun.file("circuit.zkey").arrayBuffer();
const wtnsBytes = await Bun.file("circuit.wtns").arrayBuffer();
const vkey = JSON.parse(await Bun.file("vkey.json").text());
// Initialize (one-time: shader compile, base upload, scratch alloc)
const prover = await Groth16Prover.new(new Uint8Array(zkeyBytes));
// Prove (hot path)
const { proof, publicSignals, profile } = await prover.prove(new Uint8Array(wtnsBytes));
console.log(`Proved in ${profile.total.toFixed(1)} ms`);
// Verify (via snarkjs)
const verified = await snarkjs.groth16.verify(vkey, publicSignals, proof);
console.log("Verified:", verified);
prover.dispose();Requirements
- Browser: Chrome 113+ or any browser with WebGPU support (
navigator.gpurequired). - Node/Bun: Not directly supported (WebGPU not available in Node). For local benchmarks, use the
bench-local.tsCPU comparison script or the browser-based demo. - Environment: macOS (Metal), Windows (D3D12), or Linux (Vulkan) with a WebGPU-capable GPU.
Commands (from packages/groth16-webgpu)
| Command | Description |
|---|---|
| bun test | Pure-TS suite (field/FFT/curve/prove-vs-golden), no GPU |
| bun run test:wgsl | Headless WGSL unit tests |
| bun run test:browser | Small circuit e2e (WebGPU + snarkjs verify) |
| bun run bench:rsa | RSA-2048 benchmark |
| bun run build:shaders | Regenerate src/gpu/shaders.ts from shaders/*.wesl |
| bun run gen:rsa | Regenerate RSA fixture |
Pipeline
.zkey ──► parse ──► BuildABC (QAP) ──► Coset FFT chain ──► 5× MSM (G1+G2) ──► Host assembly ──► Proof
.wtns ──► ↑ ↑
(GPU: CSR tables) (GPU: twiddle factors)Internally mirrors the snarkjs Groth16 prover pipeline exactly. A pure-TS bigint reference prover (src/reference/) serves as the correctness oracle and matches snarkjs bit-for-bit; golden vectors in fixtures/golden.json.
Benchmark (RSA-2048, 149k constraints)
| Stage | Time (ms) | |---|---| | IO & Parser | ~0.0 | | GPU Buffer Prep | ~5 | | BuildABC (QAP) | ~0.2 | | Coset FFT Chain | ~1.2 | | MSM (4× G1 + 1× G2) | ~260 | | Host Assembly | ~3 | | Total | ~270 |
Measured on MacBook Pro (Apple Silicon, Chrome/WebGPU, warm, best-of-run). ~2.5× faster than rapidsnark C++ (~676 ms).
License
MIT
