Implementing the shift reduction in a sparsity-aware way
We now explain the implementation of our shift reduction's prover. We covered a lot of ground in the previous page, where described the protocol's overall two-phase sumcheck structure.
Here, we describe more explicitly how the prover can precompute the various tables of values that it needs in order to carry out those sumchecks. The point is to exploit the sparsity of the constraint system, and to get something whose performance is close to the witness size.
There are actually a few ways to do this, which are essentially similar in spirit and have slightly different tradeoffs at the implementation level. We are going to describe one which is conceptually simple and gets close to what we do.
The First Phase
We now describe the prover's implementation of the first phase of the shift reduction. The prover's inputs are the challenges ri, rx′ sampled during the univariate skip, the witness w, and the constraint array z.
Computation of h polynomials
We recall the definitions of the polynomials hop(J,S). For each j∈B6 and s∈B6:
hop(j,s)=i∈B6∑δD(ri,i)⋅shift-indop(i,j,s).
First, the prover should precompute (δD(ri,i))i∈B6. As we've already seen, the prover can do this efficiently using a technique related to Lagrange interpolation.
We recall now what the shift indicators mean. We note that for each op∈{sll,srl}, for each j∈B6 and each s∈B6, there is at most one i∈B6 for which shift-indop(i,j,s)=1. We can thus set hop(j,s) equal to δD(ri,i) for precisely the appropriate value of i.
The case op=sra is slightly more tricky. Here, the case j=63 is exceptional. In that case, for each s∈B6, shift-indop(i,j,s)=1 for eachi∈B6 for which {i}≥64−{s}.
Putting everything together, we get the following algorithm. We write delta for the length-64 flattening of (δD(ri,i))i∈B6.
In the last line of the algorithm above, we do a "running prefix sum" trick. This completes our description of the prover's computation of the polynomials hop(J,S).
Computation of g polynomials
We now turn to the polynomials gop(J,S), for op∈{sll,srl,sra}. We reproduce their definitions here. For each op∈{sll,srl,sra}, j∈B6 and s∈B6:
Our prover's first goal will be to compute the all of the values Zcons,op(rx′,y,s) used above. Now across all possible y∈Bℓwords, all three op, and all 64 s∈B6, these three quantities represent nwords⋅3⋅64 values, which is 192 times as large as the witness, and so too much to compute and store naïvely. When we are assessing claims on multiple constraint arrays z at once—as we are in practice—this problem becomes worse (linearly in the number of constraint arrays).
On the other hand, very few of these combinations will actually have nonzero values, because of sparsity. Our goal is to compute and store only the ones that we need. We will compute and store Zcons,op(rx′,y,s)only for those triples (y,op,s) for which that value is nonzero.
To do this, we will create a kind of dictionary datastructure. We will keep a list, indexed by y∈{0,…,nwords−1}, called index. For each y∈{0,…,nwords−1}, index[y] will itself be a dictionary, whose keys will be triples (op,s), and whose values will be F2128-elements. For each y, the keys of index[y] will be precisely those triples (op,s) for which the column Zcons,op(X,y,s) is not identically zero; for each such key, the value index[y][(op,s)] will be Zcons,op(rx′,y,s). (In the case of many constraint arrays, the keys of index[y] will be not pairs but triples, containing also an index into the list of constraint arrays.)
The following algorithm populates this datastructure:
precompute tensor:=(eq(rx′,x))x∈Bℓand, the tensor expansion of rx′.
let index be a length-nwords array, whose elements are dictionaries, initialized to empty.
for each x∈{0,…,ncons−1}do:
load the xth list Lxz from the constraint system.
for each (y,op,s)∈Lxzdo:
update index[y][(op,s)]+=tensor[x].
return index.
For each y, we understand the dictionary index[y] as a defaultdict, in the sense that if the key (op,s) is not yet present in index[y], the operation index[y][(op,s)]+=tensor[x] will implicitly create the key—with value 0—before executing.
With these in hand, we're ready to compute the polynomials gop(J,S). To compute these, we run the following algorithm:
for each op∈{sll,srl,sra}, initialize gop to be an empty, 26×26 array of field elements.
for each y∈{0,…,nwords−1}do:
for each key–value pair ((op,s),value)inindex[y]do:
for each j∈{0,…,63}do:
ifw[y]j=?1do:
gop[j][s]+=value.
return gop for all three op∈{sll,srl,sra}.
We claim that for each op∈{sll,srl,sra}, the resulting array gop contains precisely the table of values of gop(J,S) on B6×B6. This completes our description of the first phase.
The Second Phase
As we've seen, to prepare for the second phase of the shift reduction, the prover needs to prepare the tables of values of two different ℓwords-variate multilinears:
w(rj,Y)
and
op∑hop(rj,rs)⋅Zcons,op(rx′,Y,rs).
The first is just a standard partial specialization. The prover just needs to tensor-expand the 6-dimensional challenge rj, and then, for each y∈{0,…,nwords−1}, take the subset sum ∑j=063w[y]j⋅eq(rj,j).
For the second, we can again use our index. We claim that the following algorithm computes the required table of values:
initialize Z, an all-zero, length-nwords array of F2128-elements.
compute in advance the tensor-expansion (eq(rs,s))s∈B6.
fory∈{0,…,nwords−1}do:
for each key–value pair ((op,s),value)inindex[y]do:
update Z[y]+=hop(rj,rs)⋅value⋅eq(rs,s).
return Z.
This completes the precomputation; Z yields the table of values of the second multilinear in Y above. The prover can at this point proceed with the second phase of the sumcheck.
Constraint Matrices
We touch on one final topic, which is how the verifier can evaluate Zcons,op(rx′,ry,rs), for all three op, himself. As we saw earlier, this is the last unexplained thing that the verifier needs to do at the very end.
The correctness of the following algorithm essentially follows from the standard properties of multilinear evaluation. The point is to do everything in a way that exploits sparsity.
for each op∈{sll,srl,sra}, initialize a single scalar mop, starting at 0.
precompute the tensor expansions (eq(rx′,x))x∈Bℓand, (eq(ry,y))y∈Bℓwords and (eq(rs,s))s∈B6.
for each x∈{0,…,ncons−1}do:
load the xth list Lxz from the constraint system.
for each (y,op,s)∈Lxzdo:
update mop+=eq(rx′,x)⋅eq(ry,y)⋅eq(rs,s).
return mop for all op∈{sll,srl,sra}.
This completes our treatment of the shift reduction.