Advent Of Jolt (Day 4): The Spartan Sumchecks

I must admit this took me longer to parse than I expected, and there are parts I’m still a little unsure about. There have been some major API changes since I started this. I’ll resume these soon, once the API has been consolidated post streaming sumchecks

On day 3 we went over all the constraints that must be satisfied. The way the prover convinces a verifier that it satisfied all these constraints (and, consequently proves they executed the trace faithfully), is via the sum-check protocol.

Jolt has a lot of sumchekcs as illustrated in the figure below. TODO:

In this post we focus on the sumchecks highlighted in green, tagged as Spartan Sumchecks. In subsequent posts we will handle the remaining sumchecks.

Stage 1 : The Outer Sum Check

Remember the 19 R1CS constraints we discussed earlier. We add all zeros constraint to make them a total of 20 constraints. Then we can index then using two digits \((b,y)\) where \(b \in \{ 0, 1\}\), and \(y\in \{-5, \ldots, 4\}\). We choose those indices for \(y\) instead of bit string or the set \(\{0, \ldots, 9\}\) for computational reasons, the details of which we dot discuss here (maybe for a separate post). For each time step t we can write these constraints as tables which in Jolt we refer to as SpartanAz and SpartanBz, which we refer to with \(A\) and \(B\) for brevity. The picture for \(A\) is the following:

Recall that the first constraint looked like the following:.

(Load + Store) * (RamAddress - (Rs1Value + Imm)) = 0

In table language we write for this time step \(t\):

A[t,0,-4] = z_t[LOAD] + z_t[STORE] and B[t, 0, -4] = (RamAddress - (Rs1Value + Imm)).

where was z_t above is the R1CSInput struct we described on day 3. The mental picture for z_t is the following:

Now satisfying all constraints is just saying the following:

\[\widetilde{A}(t, b, y) \otimes \widetilde{B}(t, b, y) = \vec{0} \quad \forall t \in \{0,1\}^{\log T}, \forall b \in \{0,1\}, \forall y \in \{-4, ..., 5\}\]

where \(\widetilde{A}, \widetilde{B}\) is the multi-linear extension of the Spartan \(A\) and \(B\) vectors, and \(\otimes\) represents the Hadamard product.

Now we do the thing we have done since the beginning of time. We want to make sure the LHS of the above system of equations to equal the RHS as polynomials. So we sample \((\tau_{\text{cycle}}|| \tau_b|| \tau_y) \xleftarrow[]{\$}\mathsf{C}^{\log T+2}\) and check if the following sumcheck holds:

\[\sum_{t \in \{0,1\}^{\log T}} \sum_{b \in \{0,1\}} \sum_{y \in \{-4,...,5\}} \widetilde{\mathsf{eq}}\left(\tau_{\text{cycle}}, t \right) \cdot \widetilde{\mathsf{eq}}\left(\tau_b, b \right) \cdot \mathcal{L}_{y}\left(\tau_y \right) \cdot \widetilde{A}(t, b, y) \widetilde{B}(t, b, y)\]

There are some implementation details where we handle the first round specially, but let’s ignore that for now. At the end of this sum-check the verifier must be able to open

\(\widetilde{A}(r_{\text{cycle}}, r_{b}, r_{y})\cdot \widetilde{B}(r_{\text{cycle}}, r_{b}, r_{y})\) but we don’t actually commit to \(\widetilde{A}\) and \(\widetilde{B}\). We say these are virtual. That is we will open something else later that is actually committed to. If the prover did that open honestly, then we open \(\widetilde{A}\) and \(\widetilde{B}\) as well (with high probability).

And that’s all there is to STAGE 1.

At the end of stage 1, we cache a bunch of other openings. Each field in z has \(T\) values one for each time step. For example, the field LeftInput has \(T\) values, and we store \(\widetilde{\text{LeftInput}}(r_{\text{cycle}}, r_{b}, r_{y})\). We store the openings for all 36 fields.

Stage 2

Now we deal with two sumchecks ProductVirtual and InnerSumcheck. The first sumcheck validates the product constraints we discussed earlier. There’s an optimisation here where 5 checks are fused into 1 check, but beyond that everything is quite similar to the outer step.

Product Virtual Sumcheck

For all time steps \(t \in \{ 0, 1\}^{\log T}\) we want the following:

\[\begin{align} \text{LeftInstructionInput}(t) \cdot \text{RightInstructionInput}(t) &= \text{Product}(t) \\[10pt] \text{IsRdNotZero}(t) \cdot \text{WriteLookupOutputToRDFlag}(t) &= \text{WriteLookupOutputToRD}(t) \\[10pt] \text{IsRdNotZero}(t) \cdot \text{JumpFlag}(t) &= \text{WritePCtoRD}(t) \\[10pt] \text{LookupOutput}(t) \cdot \text{BranchFlag}(t) &= \text{ShouldBranch}(t) \\[10pt] \text{JumpFlag}(t) \cdot (1 - \text{NextIsNoop}(t)) &= \text{ShouldJump}(t) \end{align}\]

Sampling a random \(r_{\text{cycle}}\xleftarrow[]{\$}\mathsf{C}^{\log T} \subset \mathbb{F}^{\log T}\) from the challenge set, we get the following 5 separate sumchecks involving virtual polynomials (which verify R1CS constraints where the Cz term is non-zero, unlike the 19 constraints discussed earlier).

Remark: \(r_{\text{cycle}}\) below is the exact same verifier random samples used in the Stage 1 sumchecks.

\[\begin{align} \sum_{t \in \{ 0, 1\}^{\log T}} \widetilde{\mathsf{eq}}\left(r_{\text{cycle}}, t \right) \cdot \text{LeftInstructionInput}(t) \cdot \text{RightInstructionInput}(t) &= \text{Product}(r_{\text{cycle}}) \\[10pt] \sum_{t \in \{ 0, 1\}^{\log T}} \widetilde{\mathsf{eq}}\left(r_{\text{cycle}}, t \right) \cdot \text{IsRdNotZero}(t) \cdot \text{WriteLookupOutputToRDFlag}(t) &= \text{WriteLookupOutputToRD}(r_{\text{cycle}}) \\[10pt] \sum_{t \in \{ 0, 1\}^{\log T}} \widetilde{\mathsf{eq}}\left(r_{\text{cycle}}, t \right) \cdot \text{IsRdNotZero}(t) \cdot \text{JumpFlag}(t) &= \text{WritePCtoRD}(r_{\text{cycle}}) \\[10pt] \sum_{t \in \{ 0, 1\}^{\log T}} \widetilde{\mathsf{eq}}\left(r_{\text{cycle}}, t \right) \cdot \text{LookupOutput}(t) \cdot \text{BranchFlag}(t) &= \text{ShouldBranch}(r_{\text{cycle}}) \\[10pt] \sum_{t \in \{ 0, 1\}^{\log T}} \widetilde{\mathsf{eq}}\left(r_{\text{cycle}}, t \right) \cdot \text{JumpFlag}(t) \cdot (1 - \text{NextIsNoop}(t)) &= \text{ShouldJump}(r_{\text{cycle}}) \end{align}\]

The RHS of these equations were stored from virtual openings.

Instead of 5 separate sumchecks, create polynomials \(l(t, z)\) and \(r(t, z)\) such that:

\[\begin{align} \text{At } z = -2: \quad & l(t, -2) = \text{LeftInstructionInput}(t), \quad r(t, -2) = \text{RightInstructionInput}(t), \quad p(t, -2) = \text{Product}(t) \\[10pt] \text{At } z = -1: \quad & l(t, -1) = \text{IsRdNotZero}(t), \quad r(t, -1) = \text{WriteLookupOutputToRDFlag}(t), \quad p(t, -1) = \text{WriteLookupOutputToRD}(t) \\[10pt] \text{At } z = 0: \quad & l(t, 0) = \text{IsRdNotZero}(t), \quad r(t, 0) = \text{JumpFlag}(t), \quad p(t, 0) = \text{WritePCtoRD}(t) \\[10pt] \text{At } z = 1: \quad & l(t, 1) = \text{LookupOutput}(t), \quad r(t, 1) = \text{BranchFlag}(t), \quad p(t, 1) = \text{ShouldBranch}(t) \\[10pt] \text{At } z = 2: \quad & l(t, 2) = \text{JumpFlag}(t), \quad r(t, 2) = (1 - \text{NextIsNoop}(t)), \quad p(t, 2) = \text{ShouldJump}(t) \end{align}\]

Define \(S = \{-2, -1, 0, 1, 2\}\).

Now if we wanted to check all 5 constraints are satisfied, we simply check1:

\[\sum_{t \in \{ 0, 1\}^{\log T}} \widetilde{\mathsf{eq}}\left(r_{\text{cycle}}, t \right) \sum_{z \in S} \cdot \mathcal{L}_{z}\left(\tau \right) \cdot \ell(t,z) \cdot r(t,z) = \sum_{z \in S} \mathcal{L}_{z}\left(\tau \right) \cdot p(r_{\text{cycle}}, z)\]

The Inner Sumcheck

This uses information from outer sumcheck. In particular we re-use \(r_{\text{cycle}}, r_{b}\) and \(r_{y}\) from stage 1. Let \(C\) be the number of fields the z_t the R1CSInput struct has. The Outer sumcheck guarantees that we the constraints are satisfied, assuming \(A\) and \(B\) were properly formed. What’s stopping me from setting \(A\) and \(B\) to all zeros and satisfying the sumcheck? How do I prove that I actually used \(z_t\) to construct the rows of SpartanAz and SpartanBz? The following sumcheck essentially guarantees that SpartanAz is correctly constructed (assuming z was correctly constructed). We will get to z’s correctness later (for now assume it is so).

\[\begin{align} \sum_{z \in \{ 0, 1\}^{\log C}} \Big( \widetilde{M_A}[r_{b}, r_{y}, z]\cdot \widetilde{z}[r_{\text{cycle}}, z] + \gamma\cdot \widetilde{M_B}[r_{b}, r_{y}, z]\cdot \widetilde{z}[r_{\text{cycle}}, z] \Big) = {} & \\ \widetilde{A}[r_{\text{cycle}}, r_{b}, r_{y}] + \gamma\,\widetilde{B}[r_{\text{cycle}}, r_{b}, r_{y}]. \end{align}\]

where \(\gamma \xleftarrow[]{\$}\mathsf{C}\), and the mental model for \(M_A\) (and \(M_B\)) similarly is the following:

and zooming in

Stage 3 Sumchecks

ShiftSumcheck

let shift_sumcheck = ShiftSumcheckProver::gen(state_manager, key);

InstructionInputSumcheck

let instruction_input_sumcheck = InstructionInputSumcheckProver::gen(state_manager);

ProductVirtualInner

let product_virtual_claim_check = ProductVirtualInnerProver::new(state_manager);

  1. The actual code uses split eq poly optimization from Gruen.↩︎