In Module 2 we saw that satisfying an R1CS reduces to a single polynomial identity , and that pairings let us check such an identity on encrypted values.
That gave us a SARK: a succinct, non-interactive, sound argument of knowledge — but not yet zero-knowledge, and not really efficient. In this module we close all three gaps and build a full, working Groth16 prover and verifier in Rust.
Mental model. Groth16 has three phases. First, setup builds an encrypted lookup table for one fixed circuit. Then the prover combines that table with a private witness to form a proof. Finally, the verifier checks one pairing equation using only public data. The rest of this module just makes those three sentences precise.
All Rust snippets in this module come from the companion repository github.com/bsaepfl/zksnarks-101-labs. Clone it locally if you want to run, tweak, or extend the code yourself — every cell on this page is pre-executed with the output shown directly below it.
We'll follow three steps:
- Trusted setup — sample one secret evaluation point and a few extra random setup scalars (called blinders), publish encrypted evaluations (the Structured Reference String), then throw the secrets away.
- Zero-knowledge blinding — randomize every proof with fresh per-proof values so two proofs of the same witness look completely different.
- Groth16 verification — one pairing equation checks correctness, knowledge-soundness, and zero-knowledge all at once.
Running example. We keep the circuit from Module 2, so you can trace every cell against wire values , that you already know by heart.
1. Trusted Setup & the Structured Reference String
Goal of this section: turn the public QAP data of the circuit into reusable encrypted material. After setup, the prover will no longer manipulate plain values like or directly; it will only combine their encoded versions from the SRS.
The verifier cannot see the secret witness, but she needs a way to evaluate the QAP polynomials at a secret point . The trick: let a trusted setup party sample together with blinding scalars , , , , publish encrypted evaluations, and then destroy the secrets. Anything published lives on an elliptic curve, so no one can recover the underlying scalars.
This is the same kind of hidden evaluation point that we called in Module 2. We rename it here only to avoid a clash with the later Groth16 blinding scalar used in the proof itself.
These setup secrets appear all at once, so it helps to separate their roles before looking at the SRS itself. None of them belong to the prover's witness or the verifier's public input: they exist only during setup and are discarded afterwards.
A blinder is just an extra random secret scalar added for cryptographic reasons, not because the original circuit needs it. The computation does not involve at all. These values are introduced by the setup to hide structure, separate public from private terms, and prevent the prover from mixing pieces of the proof in invalid ways.
| Secret | Role |
|---|---|
| The secret evaluation point for the QAP. This is the same hidden point as Module 2's ; we write here so it does not collide with the later proof-randomness scalar . | |
| Extra setup trapdoors that tie the prover's encrypted linear combinations to the expected left/right QAP structure. They are part of the knowledge-soundness machinery. | |
| Separates the public-input contribution from the rest. This is why verifier-side terms use . | |
| Blinds the prover-only terms, including the private witness part and the quotient/H-query. This keeps the proof well-formed without exposing the witness directly. |
What the SRS contains
The SRS is much easier to read as a few families of published curve points rather than as one giant formula. For our QAP, the setup publishes:
Before reading the table, here is the notation. Writing means “encode the scalar as a point in the first curve group ,” and means the same in . Concretely:
So is not the plain scalar ; it is that value hidden inside a group element. The index ranges over wires of the circuit, and braces such as mean “the whole family of such encodings for every wire.”
| Family | Published elements | Why they matter |
|---|---|---|
| Setup constants | These are the encrypted versions of the setup secrets themselves. | |
| Selector queries | They let the prover and verifier assemble and from witness coefficients. | |
| Public-input queries | These are the terms the verifier can safely use for the public wires only. | |
| Private-witness queries | These are the corresponding prover-only terms for the private witness wires. | |
| H-query | This lets the prover encode the quotient polynomial in the exponent. |
The only new composite term here is . It packages together the three selector families into the combination Groth16 needs later in the proof.
Mental picture. The SRS is an encrypted lookup table for this circuit. It lets the prover recover exactly the encoded quantities needed at the hidden point , without ever learning itself.
The verification key is a small subset: . The proving key contains everything else.
Why the division by and ? These extra blinders lock the prover into combining proving-key terms in exactly the right way. Multiplying two encrypted factors in the pairing equation reintroduces the scalars, which only cancel if the prover used the advertised decomposition — otherwise verification fails.
2. Wire Layout and the QAP Recap
Now that setup has published the encrypted queries, what does the prover actually plug into them? Only two things matter: which wires are public versus private, and the quotient polynomial witnessing that the QAP identity really holds.
Groth16 separates public wires (the verifier sees them) from private wires (the prover keeps them secret). For the natural split is:
| Index | Wire | Visibility | Value at x = 3 |
|---|---|---|---|
| 0 | (constant) | public | 1 |
| 1 | (output) | public | 36 |
| 2 | (secret input) | private | 3 |
| 3 | private | 9 | |
| 4 | private | 27 |
We encode three gates: , , and the consistency constraint . The target polynomial vanishing on all three gates is .
The QAP identity must hold exactly. Here is how the prover computes :
The coefficients look opaque because arkworks shows each field element as a 4×64-bit Montgomery representation — that's just how prints at the 256-bit level. What matters is that the division succeeded with zero remainder, so the identity holds.
3. Proving: Unblinded Core + Fresh Blinding
Goal of this section: build a proof from the witness without revealing the witness. Groth16 does this in two layers: first compute a deterministic core from the proving key and the witness, then add fresh randomness so the final proof is unlinkable and zero-knowledge.
A Groth16 proof is just three group elements:
| Element | Intuition |
|---|---|
| Packages the prover's encrypted left-side linear combination. | |
| Packages the encrypted right-side linear combination. | |
| Packages the private-witness and quotient contribution, plus the correction terms needed so blinding does not break verification. |
The prover first builds an unblinded core that captures the witness:
Then — and this is the zero-knowledge step — it samples fresh for each proof and blinds:
The term is the clever piece: when the verifier computes , expanding the definitions produces an cross-term that must be canceled by . This makes the proof perfectly re-randomizable: two honest proofs of the same witness look statistically independent, yet both verify.
4. Verification: One Pairing Equation
Goal of this section: show that the verifier only has to rebuild the public part of the computation. Everything else is carried by the proof elements .
The verifier never sees the witness. She only receives and the public output , assembles the public-wire term , and checks a single pairing equation:
| Term | Meaning |
|---|---|
| The prover's encrypted version of the left-hand product. This is where the hidden term shows up. | |
| The setup/public-input side that the verifier can reconstruct by herself. | |
| The private-witness and quotient contribution that completes the hidden QAP identity. |
That one equation simultaneously enforces three properties:
- Correctness of the QAP identity .
- Knowledge soundness: the prover had to use the advertised terms — without them, no linear combination of SRS elements can produce a valid (this is the Knowledge-of-Exponent argument from Module 2).
- Public-input binding: wires in are divided by , wires in by , so the prover cannot swap one pool for the other.
5. Tampering: What Breaks and Why
To make the security guarantees tangible, let's try two kinds of cheating. First, lie about the public output: submit an honest proof for but claim it proves . Second, tamper with : replace it with . Both should fail:
The first failure is due to : changing changes the -side of the equation but not the -side, so the balance breaks. The second failure is due to knowledge-soundness: a random point can't be re-derived from the advertised proving-key terms, so the pairing equation refuses it.
6. Bonus: KZG-Style Polynomial Commitments
Groth16 uses a one-shot QAP evaluation in the exponent. The more general technique — used by Plonk, Halo2, and many modern SNARKs — is a polynomial commitment scheme. KZG is the canonical example. Given an SRS , committing to a polynomial is a single group element:
To prove , the prover sends the quotient commitment where . The verifier checks:
By bilinearity this reduces to , which is exactly the Euclidean division identity. A false claim has no quotient by , so it cannot pass.
Summary
| Ingredient | Role |
|---|---|
| Trusted setup | Publishes encrypted evaluations of QAP polynomials; destroys . |
| Unblinded proof | Encodes the witness as using SRS elements. |
| Blinding | Two fresh scalars per proof make it indistinguishable from a simulation — zero knowledge. |
| Pairing check | A single equation enforces QAP correctness, knowledge-soundness, and public-input binding. |
| KZG commitment | A more general building block that generalizes the idea to any polynomial opening — the foundation of modern universal SNARKs. |
You now have a complete zkSNARK — a succinct, non-interactive, and zero-knowledge argument of knowledge — built from the ground up on BLS12-381. The companion repository contains every file you need to experiment further:
- swap the circuit for your own (e.g. a Merkle membership proof);
- replace the with a real CSPRNG;
- compare performance against
ark-groth16's production implementation.