Prover Implementation
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 , sampled during the univariate skip, the witness , and the constraint array .
Computation of h polynomials
We recall the definitions of the polynomials . For each and :
First, the prover should precompute . 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 , for each and each , there is at most one for which . We can thus set equal to for precisely the appropriate value of .
The case is slightly more tricky. Here, the case is exceptional. In that case, for each , for each for which .
Putting everything together, we get the following algorithm. We write for the length-64 flattening of .
- allocate empty, , all-zero arrays , and , containing -elements.
- for do:
- for do: set
- for do: set .
- for do: set .
- for do: update .
- return , and .
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 .
Computation of g polynomials
We now turn to the polynomials , for . We reproduce their definitions here. For each , and :
Our prover's first goal will be to compute the all of the values used above. Now across all possible , all three , and all 64 , these three quantities represent 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 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 only for those triples for which that value is nonzero.
To do this, we will create a kind of dictionary datastructure. We will keep a list, indexed by , called . For each , will itself be a dictionary, whose keys will be triples , and whose values will be -elements. For each , the keys of will be precisely those triples for which the column is not identically zero; for each such key, the value will be . (In the case of many constraint arrays, the keys of will be not pairs but triples, containing also an index into the list of constraint arrays.)
The following algorithm populates this datastructure:
- precompute , the tensor expansion of .
- let be a length- array, whose elements are dictionaries, initialized to empty.
- for each do:
- load the th list from the constraint system.
- for each do:
- update .
- return .
For each , we understand the dictionary as a defaultdict
, in the sense that if the key is not yet present in , the operation will implicitly create the key—with value 0—before executing.
With these in hand, we're ready to compute the polynomials . To compute these, we run the following algorithm:
- for each , initialize to be an empty, array of field elements.
- for each do:
- for each key–value pair in do:
- for each do:
- if do:
- .
- if do:
- for each do:
- for each key–value pair in do:
- return for all three .
We claim that for each , the resulting array contains precisely the table of values of on . 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 -variate multilinears:
and
The first is just a standard partial specialization. The prover just needs to tensor-expand the 6-dimensional challenge , and then, for each , take the subset sum .
For the second, we can again use our . We claim that the following algorithm computes the required table of values:
- initialize , an all-zero, length- array of -elements.
- compute in advance the tensor-expansion .
- for do:
- for each key–value pair in do:
- update .
- for each key–value pair in do:
- return .
This completes the precomputation; yields the table of values of the second multilinear in 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 , for all three , 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 , initialize a single scalar , starting at 0.
- precompute the tensor expansions , and .
- for each do:
- load the th list from the constraint system.
- for each do:
- update .
- return for all .
This completes our treatment of the shift reduction.