Understanding the implementation of the f(ζ)=ZH(ζ)t(ζ) check
Unlike the latest version of vanilla PlonK that implements the final check
using a polynomial opening (via Maller's optimization), we implement it
manually. (That is to say, Izaak implemented Maller's optimization for 5-wires.)
But the check is not exactly f(ζ)=ZH(ζ)t(ζ). This note
describes how and why the implementation deviates a little.
What's f and what's missing in the final equation?
If you look at how the evaluation of f(z) is computed on the prover side (or
the commitment of f is computed on the verifier side), you can see that f is
missing two things:
- the public input part
- some terms in the permutation
What is it missing in the permutation part? Let's look more closely. This is
what we have:
−1⋅⋅⋅⋅⋅⋅⋅⋅z(ζω)⋅zkpm(ζ)⋅αPERM0(w[0](ζ)+β⋅σ[0](ζ)+γ)(w[1](ζ)+β⋅σ[1](ζ)+γ)(w[2](ζ)+β⋅σ[2](ζ)+γ)(w[3](ζ)+β⋅σ[3](ζ)+γ)(w[4](ζ)+β⋅σ[4](ζ)+γ)(w[5](ζ)+β⋅σ[5](ζ)+γ)β⋅σ[6](x)
In comparison, here is the list of the stuff we needed to have:
- the two sides of the coin:
z(ζ)⋅⋅⋅⋅⋅⋅⋅⋅zkpm(ζ)⋅αPERM0(w[0](ζ)+β⋅shift[0](ζ)+γ)(w[1](ζ)+β⋅shift[1](ζ)+γ)(w[2](ζ)+β⋅shift[2](ζ)+γ)(w[3](ζ)+β⋅shift[3](ζ)+γ)(w[4](ζ)+β⋅shift[4](ζ)+γ)(w[5](ζ)+β⋅shift[5](ζ)+γ)(w[6](ζ)+β⋅shift[6](ζ)+γ)
with shift[0]=1
- and
−1⋅⋅⋅⋅⋅⋅⋅⋅z(ζω)⋅zkpm(ζ)⋅αPERM0(w[0](ζ)+β⋅σ[0](ζ)+γ)(w[1](ζ)+β⋅σ[1](ζ)+γ)(w[2](ζ)+β⋅σ[2](ζ)+γ)(w[3](ζ)+β⋅σ[3](ζ)+γ)(w[4](ζ)+β⋅σ[4](ζ)+γ)(w[5](ζ)+β⋅σ[5](ζ)+γ)(w[6](ζ)+β⋅σ[6](ζ)+γ)
- the initialization:
(z(ζ)−1)⋅L1(ζ)⋅αPERM1
- and the end of the accumulator:
(z(ζ)−1)⋅Ln−k(ζ)⋅αPERM2
You can read more about why it looks like that in
this post.
We can see clearly that the permutation stuff we have in f is clearly lacking.
It's just the subtraction part (equation 2), and even that is missing some
terms. It is missing exactly this:
−1 ⋅⋅⋅⋅⋅⋅⋅⋅z(ζω)⋅zkpm(ζ)⋅αPERM0(w[0](ζ)+β⋅σ[0](ζ)+γ)(w[1](ζ)+β⋅σ[1](ζ)+γ)(w[2](ζ)+β⋅σ[2](ζ)+γ)(w[3](ζ)+β⋅σ[3](ζ)+γ)(w[4](ζ)+β⋅σ[4](ζ)+γ)(w[5](ζ)+β⋅σ[5](ζ)+γ)(w[6](ζ)+γ)
So at the end, when we have to check for the identity
f(ζ)=ZH(ζ)t(ζ) we'll actually have to check something like
this (I colored the missing parts on the left-hand side of the equation):
f(ζ)+pub(ζ)+z(ζ)⋅zkpm(ζ)⋅αPERM0⋅(w[0](ζ)+βζ+γ)⋅(w[1](ζ)+β⋅shift[0](ζ)+γ)⋅(w[2](ζ)+β⋅shift[1](ζ)+γ)⋅(w[3](ζ)+β⋅shift[2](ζ)+γ)⋅(w[4](ζ)+β⋅shift[3](ζ)+γ)⋅(w[5](ζ)+β⋅shift[4](ζ)+γ)⋅(w[6](ζ)+β⋅shift[5](ζ)+γ)−z(ζω)⋅zkpm(ζ)⋅αPERM0⋅⋅(w[0](ζ)+β⋅σ[0](ζ)+γ)⋅⋅(w[1](ζ)+β⋅σ[1](ζ)+γ)⋅⋅(w[2](ζ)+β⋅σ[2](ζ)+γ)⋅⋅(w[3](ζ)+β⋅σ[3](ζ)+γ)⋅⋅(w[4](ζ)+β⋅σ[4](ζ)+γ)⋅⋅(w[5](ζ)+β⋅σ[5](ζ)+γ)⋅⋅(w[6](ζ)+γ)+(z(ζ)−1)⋅L1(ζ)⋅αPERM1+(z(ζ)−1)⋅Ln−k(ζ)⋅αPERM2=t(ζ)(ζn−1)
This is not exactly what happens in the code. But if we move things around a
bit, we end up with what's implemented. I highlight what changes in each steps.
First, we just move things around:
f(ζ)+pub(ζ)+z(ζ)⋅zkpm(ζ)⋅αPERM0⋅(w[0](ζ)+β⋅shift[0]ζ+γ)⋅(w[1](ζ)+β⋅shift[1]ζ+γ)⋅(w[2](ζ)+β⋅shift[2]ζ+γ)⋅(w[3](ζ)+β⋅shift[3]ζ+γ)⋅(w[4](ζ)+β⋅shift[4]ζ+γ)⋅(w[5](ζ)+β⋅shift[5]ζ+γ)⋅(w[6](ζ)+β⋅shift[6]ζ+γ)−z(ζω)⋅zkpm(ζ)⋅αPERM0⋅⋅(w[0](ζ)+β⋅σ[0](ζ)+γ)⋅(w[1](ζ)+β⋅σ[1](ζ)+γ)⋅(w[2](ζ)+β⋅σ[2](ζ)+γ)⋅(w[3](ζ)+β⋅σ[3](ζ)+γ)⋅(w[4](ζ)+β⋅σ[4](ζ)+γ)⋅(w[5](ζ)+β⋅σ[5](ζ)+γ)⋅(w[6](ζ)+γ)−t(ζ)(ζn−1)=(1−z(ζ))L1(ζ)αPERM1+(1−z(ζ))Ln−k(ζ)αPERM2
here are the actual lagrange basis calculated with the
formula here, oh and we actually use L0 in the code,
not L1, so let's change that as well:
f(ζ)++−−=pub(ζ)z(ζ)⋅zkpm(ζ)⋅αPERM0⋅(w[0](ζ)+β⋅shift[0]ζ+γ)⋅(w[1](ζ)+β⋅shift[1]ζ+γ)⋅(w[2](ζ)+β⋅shift[2]ζ+γ)⋅(w[3](ζ)+β⋅shift[3]ζ+γ)⋅(w[4](ζ)+β⋅shift[4]ζ+γ)⋅(w[5](ζ)+β⋅shift[5]ζ+γ)⋅(w[6](ζ)+β⋅shift[6]ζ+γ)+z(ζω)⋅zkpm(ζ)⋅αPERM0⋅(w[0](ζ)+β⋅σ[0](ζ)+γ)⋅(w[1](ζ)+β⋅σ[1](ζ)+γ)⋅(w[2](ζ)+β⋅σ[2](ζ)+γ)⋅(w[3](ζ)+β⋅σ[3](ζ)+γ)⋅(w[4](ζ)+β⋅σ[4](ζ)+γ)⋅(w[5](ζ)+β⋅σ[5](ζ)+γ)⋅(w[6](ζ)+γ)+t(ζ)(ζn−1)(1−z(ζ))[n(ζ−1)(ζn−1)αPERM1+n(ζ−ωn−k)ωn−k(ζn−1)αPERM2]
finally we extract some terms from the lagrange basis:
[f(ζ)+pub(ζ)+z(ζ)⋅zkpm(ζ)⋅αPERM0⋅(w[0](ζ)+β⋅shift[0]ζ+γ)⋅(w[1](ζ)+β⋅shift[1]ζ+γ)⋅(w[2](ζ)+β⋅shift[2]ζ+γ)⋅(w[3](ζ)+β⋅shift[3]ζ+γ)⋅(w[4](ζ)+β⋅shift[4]ζ+γ)⋅(w[5](ζ)+β⋅shift[5]ζ+γ)⋅(w[6](ζ)+β⋅shift[6]ζ+γ)+−z(ζω)⋅zkpm(ζ)⋅αPERM0⋅(w[0](ζ)+β⋅σ[0](ζ)+γ)⋅(w[1](ζ)+β⋅σ[1](ζ)+γ)⋅(w[2](ζ)+β⋅σ[2](ζ)+γ)⋅(w[3](ζ)+β⋅σ[3](ζ)+γ)⋅(w[4](ζ)+β⋅σ[4](ζ)+γ)⋅(w[5](ζ)+β⋅σ[5](ζ)+γ)⋅(w[6](ζ)+γ)−t(ζ)(ζn−1)]⋅(ζ−1)(ζ−ωn−k)=(1−z(ζ))[n(ζn−1)(ζ−ωn−k)αPERM1+nωn−k(ζn−1)(ζ−1)αPERM2]
with
αPERM0=α17,αPERM1=α18,αPERM2=α19
Why do we do things this way? Most likely to reduce
Also, about the code:
- the division by n in the αPERM1 and the
αPERM2 terms is missing (why?)
- the multiplication by wn−k in the αPERM2 term is
missing (why?)
As these terms are constants, and do not prevent the division by ZH to form
t, t also omits them. In other word, they cancel one another.
What about t?
In verifier.rs you can see the following code (cleaned to remove anything not
important)
// compute the witness polynomials $w_0, \cdots, w_14$ and the permutation polynomial $z$ in evaluation forms on different domains
let lagrange = index.cs.evaluate(&w, &z);
// compute parts of the permutation stuff that is included in t
let (perm, bnd) = index.cs.perm_quot(&lagrange, &oracles, &z, &alpha[range::PERM])?;
// divide contributions with vanishing polynomial
let (mut t, res) = (perm + some_other_stuff).divide_by_vanishing_poly()
// add the other permutation stuff
t += &bnd;
// t is evaluated at zeta and sent...
Notice that bnd is not divided by the vanishing polynomial. Also what's
perm? Let's answer the latter TESTREMOVEME:
perm(x)= −aPERM0⋅zkpl(x)⋅[z(x)⋅(w[0](x)+γ+x⋅β⋅shift[0])⋅(w[1](x)+γ+x⋅β⋅shift[1])⋅…z(x⋅w)⋅(w[0](x)+γ+σ[0]⋅β) ⋅(w[1](x)+γ+σ[1]⋅β)⋅…]
and bnd is:
bnd(x)=aPERM1⋅x−1z(x)−1+aPERM2⋅x−sid[n−k]z(x)−1
you can see that some terms are missing in bnd, specifically the numerator
xn−1. Well, it would have been cancelled by the division by the vanishing
polynomial, and this is the reason why we didn't divide that term by
ZH(x)=xn−1.
Also, note that the same constant terms that were missing in f are missing in
t.