Zero-Knowledge: Constructing a SNARK Proof (Part 2/3)

Twin origami cranes engraved with pairing equations, meeting at a luminous focal beam

Part 1 took a program and reduced it to a single polynomial check: for any random $\tau$, a valid witness satisfies $T(\tau) = H(\tau) \cdot Z(\tau)$, where $T(t) = L(t) \cdot R(t) - O(t)$. That is a clean algebraic statement, but it is not yet a proof. Two gaps remain. The prover must evaluate at a $\tau$ they do not know, and the evaluation must come from the QAP's actual polynomials, not arbitrary ones cooked up to pass the check.

This post closes both gaps, and a few more we'll uncover along the way. By the end, we'll have a full SNARK: eight curve points, five pairing checks, and a proof that reveals nothing about the witness.

Moving Evaluation into Curve Space

The first gap closes by hiding $\tau$ inside curve points. A trusted setup ceremony picks a random scalar $\tau$ and publishes its powers in two prime-order elliptic-curve groups:

$$G_1,\ \tau G_1,\ \tau^2 G_1,\ \ldots,\ \tau^d G_1$$

in $\mathbb{G}_1$, plus the parallel powers $G_2, \tau G_2, \ldots, \tau^d G_2$ in $\mathbb{G}_2$, then destroys the scalar itself. A group of participants each contributes randomness to $\tau$, and as long as one of them destroys their share honestly, nobody can reconstruct it. The destroyed randomness is called toxic waste. What survives is the list of points above: the Common Reference String (CRS), following the same pattern as the trusted setup from the Verkle post.

Trusted setup burns τ into the CRS rails of G₁ and G₂ points; four polynomial coefficients pair with gold points and converge into a wax-sealed commitment P(τ)·G₁

Anyone who holds the coefficients of a polynomial $P$ can now compute $P(\tau) \cdot G_1$ without learning $\tau$: multiply each published $\tau^k G_1$ by the coefficient $p_k$ and sum. The result is a commitment to the polynomial's value at the hidden point, and it is the core tool that lets the prover hand the verifier witness-dependent values without revealing the witness.

Binding the Commitments to the QAP Polynomials

Let's start with three commitments: one per aggregate polynomial $L$, $R$, and $O$. Take $\pi_A$, the commitment to $L(\tau)$, which per Part 1 should be computed as the witness-weighted sum of per-variable polynomials $L_i$:1

$$\pi_A = L(\tau) \cdot G_1 = \sum_i s_i \cdot L_i(\tau) \cdot G_1$$

where $s_i$ are the witness values. Recall that both prover and verifier know the $L_i$. The Knowledge of Exponent (KoE) assumption is what forces the prover to publish a $\pi_A$ computed using the equation above.

Given a pair $(P, \alpha P)$, a curve point and itself shifted by an unknown scalar $\alpha$, KoE says the only way to produce another pair with the same shift is to scale the original pair by a second known scalar $c$, as $(cP, c \cdot \alpha P) = (cP, \alpha \cdot cP)$.2 KoE extends naturally to linear combinations: publish $(P_1, \alpha P_1), \ldots, (P_n, \alpha P_n)$ and the prover can return another $\alpha$-shifted pair $(C, \alpha C)$ if and only if $C = \sum_j c_j P_j$ for coefficients $c_j$ the prover knows.

So the prover must build from the published $P_j$'s, which is the property we need to bind $\pi_A$ to the $L_i$: for each variable $i$, the setup publishes the pair $\big(L_i(\tau) \cdot G_1,\ \alpha_A \cdot L_i(\tau) \cdot G_1\big)$ with a secret scalar $\alpha_A$. The prover combines these pairs with witness weights $s_i$, using the plain halves to form $\pi_A$ and the $\alpha_A$-shifted halves to form $\pi_A'$.

With these $\alpha$-shifted pairs and $\alpha_A \cdot G_2$ also in the CRS, the verifier can run a pairing check confirming $(\pi_A, \pi_A')$ has the right shift:3

$$e(\pi_A',\ G_2) = e(\pi_A,\ \alpha_A \cdot G_2)$$

Both sides reduce to $e(\pi_A, G_2)^{\alpha_A}$ by bilinearity, so the check passes iff $\pi_A' = \alpha_A \cdot \pi_A$. The same construction with fresh scalars $\alpha_B$ and $\alpha_C$ gives $(\pi_B, \pi_B')$ and $(\pi_C, \pi_C')$. $\pi_B$ is built in $G_2$ rather than $G_1$ so it can later pair with $\pi_A$; $\pi_C$ stays in $G_1$. In summary:

$$\begin{aligned} \pi_A &= \sum_i s_i \cdot L_i(\tau) \cdot G_1 \\ \pi_B &= \sum_i s_i' \cdot R_i(\tau) \cdot G_2 \\ \pi_C &= \sum_i s_i'' \cdot O_i(\tau) \cdot G_1 \end{aligned} \tag{1}$$

with α-shifted companions $\pi_A' = \alpha_A \cdot \pi_A,\ \pi_B' = \alpha_B \cdot \pi_B,\ \pi_C' = \alpha_C \cdot \pi_C$.

Nothing yet forces $s_i = s_i' = s_i''$: we could have three candidate witnesses passed in parallel.

Binding the Commitments to a Single Witness

That freedom lets the prover pass divisibility at $\tau$ without a genuine witness. The fix is a fourth commitment $\pi_K$ the verifier uses to check whether the three witness sets agree.

For each variable $i$, the CRS publishes a bundled point scaled by a secret $\beta$:

$$\beta \big(L_i(\tau) + R_i(\tau) + O_i(\tau)\big) \cdot G_1$$

β keeps the bundle tied: the prover can't split it into $L_i, R_i, O_i$ pieces to apply different weights. Picking a single weight $r_i$ per variable, they form:

$$\pi_K = \beta \sum_i r_i \cdot \big(L_i(\tau) + R_i(\tau) + O_i(\tau)\big) \cdot G_1 \tag{2}$$

With $\beta \cdot G_1$ and $\beta \cdot G_2$ also in the CRS, the verifier runs a pairing check:

$$e(\pi_K, G_2) = e(\pi_A + \pi_C,\ \beta \cdot G_2) \cdot e(\beta \cdot G_1,\ \pi_B)$$

$\pi_B$ pairs separately since it lives in $G_2$ while $\pi_A, \pi_C$ stay in $G_1$. By bilinearity, both sides reduce to the same power of $e(G_1, G_2)$. Matching exponents, canceling β, and equating per term in the sum:

$$r_i \cdot \big(L_i(\tau) + R_i(\tau) + O_i(\tau)\big) = s_i \cdot L_i(\tau) + s_i' \cdot R_i(\tau) + s_i'' \cdot O_i(\tau), \quad \forall i$$

Assuming the per-variable polynomials $L_i, R_i, O_i$ are linearly independent, the check pushes $r_i$ to match each of $s_i, s_i', s_i''$ for every $i$.

$\alpha$ binds each proof element to the QAP polynomials; β binds the three elements to the same witness. We now have a consistent prover. One gap remains: their witness must satisfy the gate constraints.

Binding the Commitments to a Satisfying Witness

Part 1 showed that the quotient $H(t) = (L(t) \cdot R(t) - O(t)) / Z(t)$ exists as a polynomial iff every gate is satisfied, where $Z$ is the target polynomial vanishing at all gate roots. The prover commits $H(\tau)$ using the CRS powers of $\tau$:

$$\pi_H = H(\tau) \cdot G_1 = \sum_j h_j \cdot (\tau^j \cdot G_1) \tag{3}$$

With $Z(\tau) \cdot G_2$ also in the CRS, the verifier runs:

$$e(\pi_A,\ \pi_B) = e(\pi_H,\ Z(\tau) \cdot G_2) \cdot e(\pi_C,\ G_2)$$

Both sides reduce to powers of $e(G_1, G_2)$; by bilinearity, matching exponents gives:

$$L(\tau) \cdot R(\tau) = H(\tau) \cdot Z(\tau) + O(\tau)$$

That is the QAP identity $L \cdot R - O = H \cdot Z$ evaluated at $\tau$, now lifted into curve space. By Schwartz-Zippel, if the identity holds at a hidden random $\tau$, it holds as polynomials with overwhelming probability.

A Working Core, Two Gaps Left

Eight commitments and five pairing checks:

Eight proof-element wax seals (six gold G₁, two silver G₂) wired to five pairing-check clauses, color-coded by family — KoE in gold, β-consistency in terracotta, divisibility in crimson

What we have is a working SNARK, modulo a CRS-level patch that closes a remaining soundness gap (ρ, covered in the appendix), and a second scalar γ included in the CRS for soundness-proof reasons.4 Two gaps remain on the main path:

  • Public inputs. Nothing yet pins the claimed output to what the verifier expects. The next section adds a public/private split.
  • Zero knowledge. The commitments still leak information about the witness. The final section adds blinding to close that gap.

Pinning the Public Variables

The typical SNARK claim reads "this program, run on my private inputs, produces this public output." The verifier has the output; they want a proof the run was honest. The five pairing checks, though, accept any consistent witness: the prover can pick whatever output they like and still pass.

Split the witness vector:

$$\mathbf{s} = [\underbrace{s_0, s_1, \ldots, s_\ell}_{\text{public}} \mid \underbrace{s_{\ell+1}, \ldots, s_n}_{\text{private}}]$$

The public half holds the constant 1 and the claimed output. The private half holds the prover's inputs and every intermediate value. From here on, $\pi_A$ sums only over private indices; $\pi_B$ and $\pi_C$ stay as defined, since the verifier needs only one handle on the public values:5

$$\pi_A = \sum_{i \text{ private}} s_i \cdot L_i(\tau) \cdot G_1$$

The missing public half falls to the verifier. The CRS exposes $L_i(\tau) \cdot G_1$ for each public index, and the verifier combines them with the claimed values on their side:

$$L_\text{pub}(\tau) \cdot G_1 = \sum_{i \text{ public}} s_i \cdot L_i(\tau) \cdot G_1$$

Wherever the verification equations used the full $L(\tau) \cdot G_1$, they now use $L_\text{pub}(\tau) \cdot G_1 + \pi_A$: the verifier's half plus the prover's, reassembled on the curve.

The divisibility check becomes:

$$e\big(L_\text{pub}(\tau) \cdot G_1 + \pi_A,\ \pi_B\big) = e(\pi_H,\ Z(\tau) \cdot G_2) \cdot e(\pi_C,\ G_2)$$

Same argument as before forces the QAP identity, now on $L = L_\text{pub} + L_\text{priv}$: it holds iff $\pi_A, \pi_B, \pi_C, \pi_H$ were computed from a witness matching the verifier's $L_\text{pub}$.6

Blinding the Commitments

Everything above is sound: any proof the verifier accepts means a valid witness exists for the claim. Soundness alone doesn't imply privacy of the witness. $\pi_A, \pi_B, \pi_C$ are deterministic functions of the witness, and thus an attacker with a candidate witness can confirm or rule it out by recomputing $\pi_A$. Zero-knowledge demands the proof reveal nothing about the witness.

The fix: randomize each commitment with a shift the verification equations absorb. The prover picks fresh $\delta_L, \delta_R, \delta_O$ per proof:7

$$\begin{aligned} \pi_A &= L(\tau) \cdot G_1 + \delta_L \cdot Z(\tau) \cdot G_1 \\ \pi_B &= R(\tau) \cdot G_2 + \delta_R \cdot Z(\tau) \cdot G_2 \\ \pi_C &= O(\tau) \cdot G_1 + \delta_O \cdot Z(\tau) \cdot G_1 \end{aligned}$$

The divisibility check already uses $Z(\tau) \cdot G_2$. Adding $Z(\tau) \cdot G_1$ to the CRS lets the prover form each shift on the curve.

Divisibility survives. Using $L \cdot R - O = H \cdot Z$, the blinded expression factors:

$$\begin{aligned} &\underbrace{(L + \delta_L Z)}_{L_{\text{new}}}\underbrace{(R + \delta_R Z)}_{R_{\text{new}}} - \underbrace{(O + \delta_O Z)}_{O_{\text{new}}} \\ &\quad = Z \cdot \underbrace{(H + \delta_L R + \delta_R L - \delta_O + \delta_L \delta_R Z)}_{H_{\text{new}}} \end{aligned}$$

The prover now computes $\pi_H = H_{\text{new}}(\tau) \cdot G_1$. The α- and β-checks balance too: the CRS grows with matching shifted elements ($\alpha_A Z(\tau) G_1$, $\beta Z(\tau) G_1$, etc.), so the companion proof elements ($\pi_A', \pi_B', \pi_C', \pi_K$) absorb corresponding shifts.

Same witness yields three identical gold wax-seal commitments without blinding (attacker's lens matches), versus three distinct seals with independent blinding droplets (attacker's lens sees only a fog of candidates)

For any fixed witness, $\pi_A, \pi_B, \pi_C$ are uniformly distributed in their groups (δ is uniform, $Z(\tau) \neq 0$). Random points carry no information about the witness that produced them.

What's Next

Together with ρ and γ, the scheme we just derived is Pinocchio/BCTV14 (2013–2014), the construction Zcash shipped with its original shielded pool in 2016. Modern refinements have taken the same pattern far: a private-payment circuit runs about 100,000 constraints and fits in a proof a few hundred bytes long; a zkEVM rollup aggregates tens of millions of constraints and lands around the same size after wrapping.8

Part 3 picks up two threads. First, how modern protocols (Groth16, PLONK, STARKs) reshape this pattern to shrink proofs, replace per-circuit setup with a universal one, or drop pairings entirely. Second, what production ZK systems prove in practice: Zcash's shielded transfers, zkEVM rollups, and newer applications.


Appendix

Two appendices: ρ (linear dependence among the $L_i, R_i, O_i$) and a reference card in literature notation.

Why ρ is needed

The body's β-check argument relied on linear independence of ${L_i, R_i, O_i}$. That assumption doesn't hold in practice: with $N$ variables and $d$ gates, the $3N$ per-variable polynomials live in a $d$-dimensional space, so once $3N > d$ (most circuits), linear relations are the norm.

What fails when independence fails. The β-check reduces to:

$$\sum_i (r_i - s_i) L_i(t) + (r_i - s_i') R_i(t) + (r_i - s_i'') O_i(t) \equiv 0$$

Under independence, the only solution is $s_i = s_i' = s_i'' = r_i$: a unique witness. Dependence opens non-trivial solutions; divisibility rejects some but not all, and any that passes both checks is an invalid proof the verifier accepts.

The ρ fix. Two secret scalars $\rho_A$ and $\rho_B$ scale the three sides differently: every $L_i$ becomes $\rho_A L_i$, every $R_i$ becomes $\rho_B R_i$, every $O_i$ becomes $\rho_A \rho_B O_i$. The commitments pick up the tags:

$$\pi_A = \rho_A L(\tau) \cdot G_1,\quad \pi_B = \rho_B R(\tau) \cdot G_2,\quad \pi_C = \rho_A \rho_B O(\tau) \cdot G_1$$

The β-identity, rewritten with these tags:

$$\sum_i (r_i - s_i) \rho_A L_i(t) + (r_i - s_i') \rho_B R_i(t) + (r_i - s_i'') \rho_A \rho_B O_i(t) \equiv 0$$

Since $\rho_A, \rho_B$ are secret and algebraically independent, distinct ρ-monomials can't cancel: each coefficient must vanish separately. That forces $r_i = s_i$ (from $\rho_A$), $r_i = s_i'$ (from $\rho_B$), $r_i = s_i''$ (from $\rho_A \rho_B$). Single witness, restored.9

The other checks stay balanced. The α-checks scale uniformly with $\rho_A$ or $\rho_B$ on each side, so the KoE relations still hold. Divisibility picks up a common $\rho_A \rho_B$ factor on both sides: the CRS publishes $\rho_A \rho_B Z(\tau) \cdot G_2$ instead of $Z(\tau) \cdot G_2$, and the factor cancels.

Reference: full BCTV14 specification in literature notation

Papers use bracket notation $[x]_j = x \cdot G_j$ and call the per-variable polynomials $A_i, B_i, C_i$ rather than $L_i, R_i, O_i$. Greek scalars ($\alpha, \beta, \gamma, \rho, \delta, \tau$) keep their names across the literature. This appendix restates the full protocol in that notation and maps each piece back to the body.

Assumes ρ (from the earlier appendix) and γ are live. Under those, body expressions pick up ρ-factors (the body's $\pi_A$ becomes $\rho_A L_\text{priv}(\tau) \cdot G_1$) and the β-check is γ-gated. The map below translates this post's notation to the literature's.

Notation map. Left column is the post's notation; right column is what you'll see in BCTV14, Groth16, and most derivatives.

This postLiteratureWhat it is
$L_i, R_i, O_i$$A_i, B_i, C_i$per-variable polynomials
$L, R, O$$A, B, C$aggregate (witness-weighted) polynomials
$P(\tau) \cdot G_j$$[P(\tau)]_j$commit to $P$ in group $j$
$\pi_A$ (private-only)$[A_{\text{mid}}]_1$prover's A-side commitment
$\pi_B, \pi_C$$[B]_2, [C]_1$B-side and C-side commitments
$\pi_A', \pi_B', \pi_C'$$[A'_{\text{mid}}]_1, [B']_1, [C']_1$α-shifted companions (all in $G_1$)
$\pi_K$$[K]_1$β-bundle combination tying the witness across L, R, O
$\pi_H$$[H]_1$quotient commitment
$L_\text{pub}(\tau) \cdot G_1$$\mathbf{vk}_x$verifier's public-input reconstruction
$\mathbf{s}$$\mathbf{a}$, $\mathbf{w}$ (varies)witness / assignment vector
$Z(\tau)$$Z(\tau)$, $t(\tau)$ (varies)target polynomial at τ

One subtle point on $\pi_B'$: the body writes $\pi_B' = \alpha_B \pi_B \in G_2$ for brevity, while BCTV14 puts $[B']_1$ in $G_1$ via a separate KoE pair. Both check the same α-shift relation; the PK below carries the $G_1$ elements BCTV14's construction needs.

Setup secrets (destroyed after the ceremony):

  • $\tau$: hidden evaluation point
  • $\alpha_A, \alpha_B, \alpha_C$: KoE shifts
  • $\beta$: witness-consistency scalar
  • $\gamma$: β-check gate
  • $\rho_A, \rho_B$: tag L, R, O distinctly so cross-family terms in the β-identity can't cancel

The prover picks $\delta_L, \delta_R, \delta_O$ fresh per proof.

Proving key (used by the prover):

  • Powers of $\tau$: $[\tau^k]_1, [\tau^k]_2$ for $k = 0, \ldots, d$
  • Per private variable $i$, A-side: $[\rho_A A_i(\tau)]_1, [\alpha_A \rho_A A_i(\tau)]_1$
  • Per private variable $i$, B-side: $[\rho_B B_i(\tau)]_1, [\rho_B B_i(\tau)]_2, [\alpha_B \rho_B B_i(\tau)]_1$
  • Per private variable $i$, C-side: $[\rho_A \rho_B C_i(\tau)]_1, [\alpha_C \rho_A \rho_B C_i(\tau)]_1$
  • Per private variable $i$, β-bundle: $[\beta(\rho_A A_i(\tau) + \rho_B B_i(\tau) + \rho_A \rho_B C_i(\tau))]_1$

Verification key (used by the verifier):

  • $[1]_2$
  • $[\alpha_A]_2, [\alpha_B]_1, [\alpha_C]_2$
  • $[\gamma]_2, [\beta\gamma]_1, [\beta\gamma]_2$
  • $[\rho_A \rho_B Z(\tau)]_2$
  • Per public variable $i$ (including $i = 0$ for the constant 1): $\mathbf{vk}_{\text{IC},i} = [\rho_A A_i(\tau)]_1$. The verifier forms $\mathbf{vk}_x = \sum_{i \in \text{pub}} s_i \cdot \mathbf{vk}_{\text{IC},i}$.

Proof (8 group elements):

$$\pi = \big([A_{\text{mid}}]_1,\ [A'_{\text{mid}}]_1,\ [B]_2,\ [B']_1,\ [C]_1,\ [C']_1,\ [K]_1,\ [H]_1\big)$$

Verification equations (5 pairing checks):

  1. α for A (KoE): $e([A'_{\text{mid}}]_1, [1]_2) = e([A_{\text{mid}}]_1, [\alpha_A]_2)$
  2. α for B (KoE): $e([B']_1, [1]_2) = e([\alpha_B]_1, [B]_2)$
  3. α for C (KoE): $e([C']_1, [1]_2) = e([C]_1, [\alpha_C]_2)$
  4. β-consistency (γ-gated): $e([K]_1, [\gamma]_2) = e([A_{\text{mid}}]_1 + [C]_1, [\beta\gamma]_2) \cdot e([\beta\gamma]_1, [B]_2)$
  5. Divisibility: $e(\mathbf{vk}_x + [A_{\text{mid}}]_1, [B]_2) = e([H]_1, [\rho_A \rho_B Z(\tau)]_2) \cdot e([C]_1, [1]_2)$

For the exact ZK-blinding CRS elements and public-input encoding, see libsnark's r1cs_ppzksnark reference implementation.


  1. Pinocchio and BCTV14 write the per-variable polynomials as $A_i, B_i, C_i$. This post uses $L_i, R_i, O_i$ to stay consistent with Part 1 and with the aggregate $L, R, O$.

  2. KoE goes beyond standard discrete-log hardness. DL hardness says "you can't find $\alpha$ given $\alpha G$", which is falsifiable: anyone can try. KoE says something stronger: whenever a prover returns a correctly α-shifted pair $(C, \alpha C)$, there exists an algorithm (an extractor) that recovers the coefficients the prover used to build $C$. That's a claim about the existence of an algorithm, not a computational bound, so no experiment can disprove it. If KoE breaks, SNARKs break.

  3. A pairing $e(P, Q)$ takes two curve points and produces an element of a target group $\mathbb{G}_T$ where multiplication is available; bilinearity gives $e(aP, bQ) = e(P, Q)^{ab}$. The Verkle post has the full derivation.

  4. γ is a CRS-generation artifact of the soundness reduction inherited from GGPR/Pinocchio, not a protocol-level defense. The full story is in GGPR §5.3 (EUROCRYPT 2013; ePrint 2012/215); in the generic group model γ isn't strictly needed (Gabizon 2019).

  5. Splitting all three sides would triple the per-public-input CRS points without adding leverage. The reference appendix has the full post-to-literature notation map.

  6. Some claims also fix inputs on the verifier side. The same mechanism carries them: "public" here just means "the verifier fixes the value."

  7. Three independent scalars, not one shared. A shared δ would correlate the commitments: solve for δ from one, predict the others.

  8. zkEVMs typically prove the bulk of their work with a STARK (fast, no trusted setup, large proof), then re-prove that STARK verification inside a Groth16 circuit. The "wrap" collapses the large STARK proof down to a small on-chain one.

  9. The per-monomial identities require each of the three families ($L_i$, $R_i$, $O_i$, each taken over all $i$) to be linearly independent on its own. Internal dependence within a family (e.g., among the $L_i$) can still exist, but it's only a concern when the dependence involves public-input indices or crosses the public/private boundary, which QAP construction rules out via mechanisms outside this post's scope.

This post was written in collaboration with Claude.