Everything Groth16 is. Everything it isn't. In one system.
Groth16 is the gold standard: 192-byte proofs, 3-pairing verification, battle-tested at scale. But it has hard limits -- a per-circuit trusted setup, no recursion, no aggregation, no post-quantum path, and no simulation-extractability.
Every alternative gives up what makes Groth16 great. PLONK and Marlin trade proof size for universality. STARKs trade proof size for transparency. Halo2 trades simplicity for recursion.
UniGroth is a research extension of ark-groth16 that adds each of these capabilities as its own path — universal setup, simulation-extractability, folding, aggregation, and a post-quantum direction — while keeping a small classical core. They are separate proof objects with separate sizes, not a single artifact that has everything at once. Read the scope notes before quoting any headline.
| Groth16 | PLONK | Marlin | Halo2 | STARKs | UniGroth | |
|---|---|---|---|---|---|---|
| Proof size | 192 B | 1-2 KB | 2-5 KB | 5-15 KB | 50-200 KB | 192-256 B |
| Verification | 3 pairings | 10+ | 15+ | Variable | Fast (hash) | 3-5 pairings |
| Trusted setup | Per-circuit | Universal | Universal | Transparent | None | Universal |
| Simulation-extractable | No | No | No | No | N/A | Yes |
| Folding / IVC | No | No | No | No | Varies | Yes |
| Proof aggregation | No | No | No | No | Varies | Yes |
| Post-quantum path | No | No | No | No | Yes | Yes |
| Public input PoK | No | No | No | No | No | Yes |
| VK compression | No | No | No | No | N/A | Yes |
| Custom gates | No | Yes | No | Yes | Yes | Yes |
| On-chain verifier gen | Manual | Manual | No | No | No | Auto |
Each cell above is a capability the library implements somewhere. They do not all apply to one proof at the same time — see the scope notes directly below.
UniGroth is research software, built on arkworks as an extension of ark-groth16. Audit before any production or mainnet use. A few clarifications so the claims are read correctly:
- Proof size is curve and feature dependent. The classical Groth16 core is 2 G1 + 1 G2: ~128 bytes on BN254, ~192 bytes on BLS12-381. The simulation-extractable, aggregated, and post-quantum paths are larger (up to ~256 B for SE; SnarkPack aggregates are O(log N), i.e. kilobytes; the PQ schemes are 256–516 B). There is no single 192-byte object that carries every feature at once.
- The features are separate paths, not one artifact. Universal setup, folding (ProtoStar), aggregation (SnarkPack), and the PQ schemes are distinct code paths with distinct proof objects. The default prove/verify path uses
circuit_specific_setupand the classical core. The auto-generated Solidity verifier checks that classical core only — it does not verify the universal, SE, aggregated, or PQ variants. - Simulation-extractability, not "forgery resistance". Plain Groth16 is already knowledge-sound: you cannot forge a proof of a false statement. The property UniGroth adds is simulation-extractability — non-malleability when an attacker can see other valid proofs.
- The post-quantum module is a commitment-and-binding scaffold, not yet a sound PQ argument.
pq_innerbinds the witness and public inputs with SHA-256 and is deterministic and tamper-evident. It does not yet prove, in zero knowledge, that a witness satisfies a circuit. "Binius" and "Plonky3" name the target FRI / sumcheck designs; they are not implemented as such. Do not rely on this module for post-quantum security. - Benchmarks are scoped. The speed figures are from our suite against
ark-groth16, classical core only, on a CI runner. They do not describe the universal, aggregated, or PQ paths. Rerun the Benchmarks workflow to measure your own hardware.
Requirements: Rust stable 1.70+ (install)
git clone https://github.com/MeridianAlgo/UniGroth.git
cd UniGroth/UniGroth
cargo build --release
cargo testAll 156 tests pass. Zero warnings.
[dependencies]
unigroth = { git = "https://github.com/MeridianAlgo/UniGroth.git" }use unigroth::Groth16;
use ark_bn254::Bn254;
use ark_snark::SNARK;
// Setup -- one ceremony, reusable for any circuit of this shape
let (pk, vk) = Groth16::<Bn254>::circuit_specific_setup(my_circuit, &mut rng)?;
// Prove -- simulation-extractable by default, near-zero overhead
let proof = Groth16::<Bn254>::prove(&pk, my_circuit, &mut rng)?;
// Verify -- same 3-pairing check as vanilla Groth16
let ok = Groth16::<Bn254>::verify(&vk, &public_inputs, &proof)?;
assert!(ok);The prove() call automatically applies ROM-based simulation-extractability blinding. Zero configuration.
Numbers below are reproducible in CI: the Benchmarks workflow runs cargo bench on every change to the prover and publishes the raw output to the run summary. The figures here are from a 4-core GitHub runner, BLS12-381 and BN254, opt-level=3. Your own hardware will differ; rerun the workflow to measure it.
Measured prove and verify time, same circuit on both provers.
| Operation | Circuit | ark-groth16 | UniGroth | Result |
|---|---|---|---|---|
| Prove | 2^12 | 71.3 ms | 61.9 ms | 1.15x faster |
| Prove | 2^16 | 676 ms | 652 ms | 1.04x faster |
| Verify | 2^12 | 2.02 ms | 2.18 ms | ~parity (1.08x slower) |
| Verify | 2^16 | 2.02 ms | 2.24 ms | ~parity (1.10x slower) |
| Proof size | — | 128 bytes | 128-161 bytes | Same core |
UniGroth proves faster than ark-groth16 and verifies at parity, while adding features ark-groth16 does not have. The proving win comes from the optimizations below.
Measured (in the benchmark suite):
| Optimization | What it does | Speedup |
|---|---|---|
h_query_scalars |
O(n) accumulator replaces O(n log n) .pow([i]) loop |
22-32x (grows with n) |
| Quotient FFTs | compute h on the n coset, not a 2n coset |
1.5-1.7x |
| Parallel MSM | rayon Pippenger partitioning, 4 cores | ~2x per-scalar at 2^16 vs 2^10 |
Implemented and unit-tested, not yet in the end-to-end harness:
| Optimization | What it does | Expected |
|---|---|---|
| Sparse QAP (CSR) | skips zero entries in the constraint matrices | ~3-5x on sparse circuits |
| Batch affine conversion | Montgomery batch inversion | ~2.5x |
| Proof aggregation (N=32) | 1 multi-pairing instead of 32 | ~32x verify |
One KZG ceremony covers all circuits of a given size. No per-circuit ceremony.
use unigroth::{KZG, UniversalParams, UniversalSRS};
// One ceremony per deployment -- anyone can update (updatable CRS)
let srs: UniversalSRS<Bn254> = KZG::<Bn254>::setup(max_degree, &mut rng)?;
let srs = KZG::<Bn254>::update_srs(&srs, &mut rng);
// Each circuit derives its keys from the shared SRS
let params = UniversalParams::from_srs(&srs, circuit_size);Every proof is simulation-extractable by default. An adversary who sees simulated proofs cannot forge new ones.
Two modes:
- ROM blinding (default) -- near-zero overhead, SHA-256 hash mixed into randomness
- BG18 explicit blinding -- +96 bytes, full algebraic security proof
use unigroth::security::{SEConfig, SEMode};
let config = SEConfig { mode: SEMode::ROM }; // or SEMode::BG18
let proof = unigroth::security::make_sim_extractable(raw_proof, &pk, &config, &mut rng);Compress N independent proofs into one constant-size aggregate. Verification cost drops from N pairings to one.
use unigroth::{aggregate_proofs, verify_aggregated};
let agg = aggregate_proofs(&proofs, &vks, &mut rng)?;
let ok = verify_aggregated(&agg, &all_public_inputs)?;ProtoStar-style accumulation with full relaxed R1CS decision predicate. Fold multiple instances into one, verify only the final accumulator.
use unigroth::{FoldingEngine, IVC};
let mut ivc = IVC::new(circuit_params);
for step_input in inputs {
ivc.step(step_input, &mut rng)?;
}
let final_proof = ivc.finalize(&mut rng)?;Auto-generate a gas-efficient Solidity verifier from any verifying key. Uses EIP-196/197 BN254 precompiles -- ~250k gas to verify on-chain.
use unigroth::solidity::generate_solidity_verifier;
let contract = generate_solidity_verifier(&vk)?;
std::fs::write("Verifier.sol", contract)?;
// Deploy and call verifyProof(a, b, c, inputs)UniGroth is the only Groth16-class system with a concrete post-quantum migration path. Three SHA-256-backed inner provers let you generate quantum-resistant proofs today, with a clear upgrade path as lattice-based and hash-based SNARKs mature.
Groth16, PLONK, Marlin, and Halo2 all rely on the hardness of the discrete logarithm problem over elliptic curves. A sufficiently powerful quantum computer running Shor's algorithm breaks all of them. STARKs are post-quantum but produce 50-200 KB proofs. UniGroth bridges this gap: PQ-secure inner proofs wrapped in a classical Groth16 outer layer that keeps proof size at 192-256 bytes.
┌─────────────────────────────────────────────┐
│ Classical Groth16 Outer Layer │
│ (192-256 byte succinct proof) │
│ 3-pairing verification │
├─────────────────────────────────────────────┤
│ Post-Quantum Inner Prover Layer │
│ ┌───────────┬───────────┬────────────────┐ │
│ │ Binius │ Plonky3 │ Hybrid │ │
│ │ Binary- │ FRI-based │ Plonky3 inner │ │
│ │ tower │ Merkle │ + Groth16 │ │
│ │ SHA-256 │ SHA-256 │ outer wrap │ │
│ └───────────┴───────────┴────────────────┘ │
├─────────────────────────────────────────────┤
│ SHA-256 Commitment Layer │
│ Witness binding · Public input binding │
│ Deterministic · Tamper-evident │
└─────────────────────────────────────────────┘
| Scheme | Basis | Proof Size (128-bit) | Best For |
|---|---|---|---|
| Binius | Binary-tower field + SHA-256 hash chains | 256 bytes | Smallest PQ proofs, latency-sensitive |
| Plonky3 | FRI + SHA-256 Merkle commitments | 512 bytes | Strongest security margin, FRI maturity |
| Hybrid | Plonky3 inner + Groth16 outer compression | 516 bytes | On-chain deployment with PQ inner security |
All three schemes support 128, 192, and 256-bit security levels.
use unigroth::{prove_pq, verify_pq, PqConfig, PqScheme};
// Choose your scheme: Binius (fastest), Plonky3 (FRI-based), or Hybrid
let config = PqConfig::new(PqScheme::Binius); // 128-bit security by default
// Prove — deterministic, bound to both witness and public inputs
let proof = prove_pq(&config, &witness, &public_inputs);
// Verify — recomputes commitments and checks binding
assert!(verify_pq(&config, &proof, &public_inputs));Aggregate multiple PQ proofs into a single Merkle-chained digest for batch verification:
use unigroth::{aggregate_pq_proofs, prove_pq, PqConfig, PqScheme};
let config = PqConfig::new(PqScheme::Binius);
let proofs: Vec<_> = witnesses.iter()
.map(|w| prove_pq(&config, w, &public_inputs))
.collect();
let aggregated = aggregate_pq_proofs(&proofs, &config);
// aggregated = header || Merkle root || per-proof SHA-256 digestsEvery PQ proof is cryptographically bound via SHA-256:
- Witness binding — proof commits to the full witness; changing any byte invalidates it
- Public input binding — proof is tied to specific public inputs; verification rejects mismatches
- Determinism — same (witness, public_inputs) always produces the same proof
- Tamper detection — any modification to proof bytes causes verification failure
- Domain separation — each scheme uses distinct tags to prevent cross-scheme attacks
| Phase | What Changes | What Stays |
|---|---|---|
| Today | Deploy with classical Groth16 (192 bytes) | — |
| Phase 1 | Switch inner prover to Binius/Plonky3 | Outer Groth16 layer, on-chain verifier |
| Phase 2 | Replace outer layer with hash-based SNARK | PQ inner proofs, proof aggregation |
| Phase 3 | Full lattice-based designated-verifier | Complete PQ stack |
See docs/post-quantum.md for the full post-quantum documentation.
Build circuits without writing raw R1CS:
use unigroth::CircuitBuilder;
use ark_bn254::Fr;
let mut builder = CircuitBuilder::<Fr>::new();
let x = builder.witness(Some(Fr::from(3u64)));
let y = builder.witness(Some(Fr::from(4u64)));
let xy = builder.mul(x, y);
builder.public_output(xy);
let circuit = builder.build();Compress a verifying key from O(n) to O(1) group elements using KZG commitments. Critical for zkEVM deployments with thousands of public inputs.
use unigroth::{compress_vk, verify_with_compressed_vk, create_vk_opening};
let cvk = compress_vk(&vk)?;
let opening = create_vk_opening(&vk, &cvk, &public_inputs)?;
let ok = verify_with_compressed_vk(&cvk, &proof, &public_inputs, &opening)?;For circuits too large to fit in memory -- process MSMs in chunks with bounded peak memory:
use unigroth::{StreamingConfig, create_streaming_proof};
let config = StreamingConfig::from_memory_budget(4 * 1024 * 1024 * 1024); // 4 GB
let proof = create_streaming_proof(&pk, circuit, &config, &mut rng)?;Custom gates (Poseidon, EC add, boolean, bit decomposition), lookup tables, and Plonkish-to-R1CS conversion:
use unigroth::{PlonkishConstraintSystem, CustomGateRegistry, LookupTable};
let mut cs = PlonkishConstraintSystem::new();
cs.register_gate(CustomGateRegistry::poseidon());
cs.add_lookup_table(LookupTable::range(16)); // 16-bit range check
let r1cs = plonkish_to_r1cs_constraints(&cs);Multi-curve recursion with SHA-256 chain integrity:
use unigroth::{create_recursive_proof, verify_recursive_chain, RecursionConfig, CurvePair};
let config = RecursionConfig { curve_pair: CurvePair::BLS12_377_BW6_761, depth: 4 };
let recursive_proof = create_recursive_proof(&inner_proofs, &config)?;
assert!(verify_recursive_chain(&recursive_proof, &config)?);Parallel multi-circuit batch proving and verification:
use unigroth::{batch_prove, batch_verify, BatchConfig};
let config = BatchConfig { num_threads: 8 };
let proofs = batch_prove(&circuits, &pks, &config, &mut rng)?;
let ok = batch_verify(&proofs, &vks, &public_inputs)?;running 137 tests
... aggregation, batch, circuit_builder, circuits, folding, kzg,
key_compression, optimizations, plonkish, pq_inner, public_input_pok,
recursion, security, solidity, streaming, wasm_verifier ...
test result: ok. 137 passed; 0 failed <- unit tests
test result: ok. 6 passed; 0 failed <- full_pipeline_test
test result: ok. 11 passed; 0 failed <- groth16_comparison (head-to-head)
test result: ok. 1 passed; 0 failed <- mimc (real MiMC hash circuit)
test result: ok. 1 passed; 0 failed <- phrase_test (advanced features)
---------------------------------------------------
Total: 156 passed | 0 failed | 0 warnings | 0 clippy lints
| Property | Mechanism | Status |
|---|---|---|
| Knowledge soundness | AGM (Algebraic Group Model) | Implemented |
| Zero-knowledge | Standard Groth16 randomization | Implemented |
| Simulation-extractability | BG18 blinding or ROM hash blinding | Implemented |
| Subversion zero-knowledge | Proof rerandomization at proving time | Implemented |
| Public input binding | Schnorr proof-of-knowledge | Implemented |
| Toxic waste zeroing | black_box zeroing after keygen |
Implemented |
| Post-quantum resistance | SHA-256-backed Binius/Plonky3/Hybrid | Implemented |
This is research software. Audit before deploying to mainnet.
| Curve | Use Case |
|---|---|
| BN254 | Ethereum on-chain verification (EIP-196/197) |
| BLS12-381 | Zcash, Ethereum consensus |
| BLS12-377 | Celo, inner curve for BW6-761 recursion |
| BW6-761 | Outer curve for BLS12-377 recursive composition |
| MNT4-298 | Two-cycle recursion |
Application Layer
(zkEVM, zkML, Private Transactions)
|
Flexible Arithmetization
SAP / Plonkish + Custom Gates + Lookups
|
Folding & Recursion Engine
ProtoStar IVC + Full Decision Predicate
|
Universal Polynomial Commitments
KZG (Powers-of-Tau)
|
Groth16-Style Compression Core
Linear Interactive Proof + Pairing Encoding
(192-256 byte final proof)
| Module | Lines | Purpose |
|---|---|---|
lib.rs |
264 | SNARK trait impl, module exports |
kzg.rs |
416 | KZG polynomial commitments, UniversalSRS |
universal_setup.rs |
426 | Circuit-agnostic key derivation |
sap.rs |
370 | Square Arithmetic Programs |
plonkish.rs |
896 | Custom gates, lookups, Plonkish-to-R1CS |
folding.rs |
1156 | ProtoStar folding, IVC, relaxed R1CS decision predicate |
security.rs |
824 | Simulation-extractability, Subversion ZK |
optimizations.rs |
1205 | Dynark FFT, parallel MSM, compression, CSR |
pq_inner.rs |
950 | Post-quantum provers (Binius, Plonky3, Hybrid) |
aggregation.rs |
308 | SnarkPack N-to-1 proof aggregation |
public_input_pok.rs |
340 | Schnorr PoK for public inputs |
streaming.rs |
371 | Streaming prover for large circuits |
batch.rs |
278 | Parallel batch proving |
solidity.rs |
336 | Solidity verifier contract generation |
wasm_verifier.rs |
297 | WASM verifier code generation |
key_compression.rs |
402 | VK compression via KZG |
circuit_builder.rs |
462 | Circuit builder SDK |
circuits.rs |
503 | Poseidon, Merkle tree, range check circuits |
recursion.rs |
274 | Recursive proof composition |
constraints.rs |
591 | R1CS gadgets (feature: r1cs) |
prover.rs |
307 | Core proof generation |
verifier.rs |
113 | Core verification |
generator.rs |
231 | Setup / key generation |
data_structures.rs |
148 | Proof, ProvingKey, VerifyingKey types |
Total: ~11,870 lines across 28 source files.
UniGroth/
UniGroth/ <- Rust library (production)
src/ <- 28 source modules
tests/ <- 4 integration test suites (19 tests)
benches/ <- Criterion benchmarks
scripts/ <- Dev tooling
src/ <- JS/Circom reference implementation
phrase.circom <- Circom phrase-knowledge circuit
verifier.sol <- Reference Solidity verifier
Every push and PR runs:
cargo fmt --check-- zero formatting driftcargo clippy -- -D warnings-- zero warningscargo build --verbose-- clean compilationcargo test --verbose-- all 156 tests pass
| Paper | Year | What UniGroth Uses |
|---|---|---|
| Groth16 | 2016 | Core protocol |
| BG18 | 2018 | Simulation-extractability |
| ABPR19 | 2019 | Updatable universal CRS |
| SnarkPack | 2022 | Proof aggregation |
| Nova | 2022 | Relaxed R1CS folding |
| ProtoStar | 2023 | Generic accumulation |
| Binius | 2023 | Binary-field PQ proofs |
| Polymath | 2024 | SAP-based proofs |
| Dynark | 2025 | FFT optimizations |
| Feature | Default | Description |
|---|---|---|
parallel |
Yes | Multi-threaded proving via rayon |
std |
Yes | Standard library support |
r1cs |
No | Constraint system gadgets for recursive verification |
solidity |
No | Solidity + WASM verifier contract generation |
universal |
No | Universal setup extensions |
sap |
No | SAP arithmetization |
gpu |
No | GPU MSM dispatch (icicle backend) |
wasm |
No | WASM compilation target |
See CONTRIBUTING.md.
Dual-licensed under MIT and Apache 2.0.
Built on arkworks-rs/groth16 by MeridianAlgo.