Skip to content

Trusted setup, explained

View Markdown

Before anyone can prove or verify a Groth16 statement, somebody has to generate the keys: a proving key for provers and a verification key for verifiers. That generation step is called the trusted setup, and it’s the part of ZK most likely to be done wrong in a real deployment. This page explains what’s being trusted, why, and how to do it properly.

Imagine a factory that makes tamper-evident seals. To set up the production line, the factory casts a unique mould. Seals made from the mould are public, and anyone can inspect them. Once the line is set up, the mould is supposed to be smashed.

If someone secretly keeps the mould, they can stamp out seals that pass every inspection while sealing nothing at all.

A trusted setup works the same way. Generating the keys requires secret random numbers. The best known is called tau (τ), and Groth16 adds a few companions of its own. The public keys are derived from these secrets, and afterwards the secrets must be destroyed. Anyone who keeps them can forge proofs: create proofs that verify for statements that are false. That’s why these secrets are nicknamed toxic waste.

Two properties make this worse than it sounds:

  • Forgeries are undetectable. A forged proof is mathematically indistinguishable from an honest one. No verifier, on-chain or off-chain, can tell them apart.
  • The damage is silent and permanent. If the toxic waste leaks, every proof ever verified with those keys becomes suspect, and you’d never know it happened.

Knowing the toxic waste doesn’t, by itself, reveal anyone’s secret inputs from their proofs. The risk is to soundness: fake proofs, such as an under-18 user “proving” they’re over 18, or someone unlocking funds without a valid witness.

What the setup actually publishes is a long list of elliptic-curve points built from powers of τ:

[1]G1, [τ]G1, [τ²]G1, [τ³]G1, …, [τⁿ]G1
[1]G2, [τ]G2, …

[x]G1 means “the curve’s generator point G1, multiplied by x”. Because of how elliptic curves work, you can publish [τ]G1 without revealing τ: going backwards is the hard problem the whole curve’s security rests on.

This list is called the powers of tau, or a structured reference string (SRS). It’s stored in a .ptau file. Its length sets the maximum circuit size it can support; a setup with 2²⁰ powers supports circuits up to roughly that many constraints.

Groth16 setup happens in two phases:

Phase 1 (universal) Powers of tau for BLS12-381, up to size 2ⁿ
once per curve + size ─ reusable by every circuit that fits
│
▼
Phase 2 (circuit-specific) Powers of tau + your compiled circuit (R1CS)
once per circuit ─ produces this circuit's proving key + verification key
(a snarkjs .zkey file)
  • Phase 1 doesn’t depend on any circuit. You can reuse a large public ceremony. ZeroJ’s ceremony runbook, for example, uses an existing attested BLS12-381 ceremony as its primary phase-1 source.
  • Phase 2 is specific to one circuit and introduces its own toxic waste. Any change to the circuit, such as a new constraint, a different number or order of public inputs, or a changed circuit parameter, means a new phase 2 and new keys. Changing input values never does; a new user or a new proof uses the same keys.

Multi-party ceremonies: one honest participant is enough

Section titled “Multi-party ceremonies: one honest participant is enough”

If one party generates τ, everyone has to trust that party. A multi-party computation (MPC) ceremony removes that single point of trust:

key_0 ──► Alice mixes in her randomness ──► key_1
key_1 ──► Bob mixes in his randomness ──► key_2
key_2 ──► Carol mixes in her randomness ──► key_3
key_3 ──► public random beacon ──► key_final

Each participant takes the previous file, mixes in fresh secret randomness, publishes the result, and destroys their randomness. The final secret is effectively the product of every participant’s contribution. To forge proofs you would need all of those contributions. So the keys are safe as long as at least one participant was honest and really destroyed their contribution. This is the “1-of-N” trust model.

Each contribution is publicly checkable. Anyone can re-verify the whole chain, for example with snarkjs zkey verify, without trusting the coordinator. A final random beacon, a public value nobody can predict in advance such as a future block hash, closes the ceremony so the last contributor can’t bias the result.

PlonK only needs phase 1. One trusted SRS serves every PlonK circuit up to its size, and each circuit’s keys are derived from it with a public, deterministic computation, with no new toxic waste per circuit. The SRS still has to come from a trustworthy ceremony, and if its τ leaks, every circuit using it is affected at once.

ZeroJ’s PlonK support is experimental; see Groth16, PlonK & BBS.

NeedWhat ZeroJ offers
Fast local setup for tests and demosPowersOfTauBLS381.generate(...), Groth16Keys.setupInMemory(...), Groth16SetupBLS381.setup(...): single-party, in-process, insecure by design, and disabled unless you opt in
Using a real phase 1PtauImporterBLS381 imports snarkjs .ptau files
Using real Groth16 keysZkeyImporterBLS381 imports a ceremony .zkey; Groth16PkStore and ZkeyPkStoreImporter stream large keys into an mmap-loaded store
Running or joining a ceremonyThe zeroj-ceremony CLI (export-r1cs, contribute, finalize) and the ZkeyContributor library in zeroj-tools, producing snarkjs-compatible contributions
Independent verificationContributions made with ZeroJ verify with snarkjs zkey verify, so nobody has to trust ZeroJ’s own tool

The in-process setup refuses to run unless you explicitly allow it:

JVM system property: -Dzeroj.allowInsecureTrustedSetup=true
or environment: ZEROJ_ALLOW_INSECURE_TRUSTED_SETUP=true

Without the opt-in, the call throws an IllegalStateException explaining that the generator knows the toxic waste and can forge proofs. Installation shows how to set the flag for Gradle and Maven test and run tasks.

For anything beyond local testing, the flow is:

1. Compile your circuit and export it: zeroj-ceremony export-r1cs → circuit.r1cs
2. Start phase 2 from a trusted .ptau: snarkjs groth16 setup → key_0000.zkey
3. Contributors mix in randomness: zeroj-ceremony contribute (or snarkjs zkey contribute)
4. Close with a pre-announced beacon, and let anyone re-check: snarkjs zkey verify
5. Import the final key into ZeroJ: zeroj-ceremony finalize → proving-key store
6. Pin the verification key (and, on Cardano, the resulting script hash).

Running a trusted setup ceremony walks through every step.

ChangeNew setup?
New witness values, new users, new proofsNo
Different public input valuesNo
Any change to the constraintsYes, a new phase 2 (Groth16)
Different number or order of public inputsYes
A different circuit parameter (for example Merkle depth)Yes. It’s a different circuit.
Circuit grows beyond the SRS sizeYes, and a larger phase 1 too

Setup is expensive, so run it once per circuit version and cache the results. Never re-run it per proof.

Design notes: ADR-0031, Groth16 MPC trusted-setup ceremony.