Instruction execution

One distinguishing feature of Jolt among zkVM architectures is how it handles instruction execution, i.e. proving the correct input/output behavior of every RISC-V instruction in the trace. This is primarily achieved through the Shout lookup argument.

Large Lookup Tables and Prefix-Suffix Sumcheck

The Shout instance for instruction execution must query a massive lookup table -- effectively of size , since the lookup query is constructed from two 32-bit operands. This parameter regime is discussed in the Twist and Shout paper (Section 7), which proposes the use of sparse-dense sumcheck. However, upon implementation it became clear that sparse-dense sumcheck did not generalize well to the full RISC-V instruction set.

Instead, Jolt introduces a new algorithm: prefix-suffix sumcheck, described in the appendix of Proving CPU Executions in Small Space. Like the sparse-dense algorithm, it requires some structure in the lookup table:

  • The lookup table must have an MLE that is efficiently evaluable by the verifier. The JoltLookupTable trait encapsulates this MLE.
  • The lookup index can be split into a prefix and suffix, such that MLEs can be evaluated independently on the two parts and then recombined to obtain the desired lookup entry.
  • Every prefix/suffix MLE is efficiently evaluable (constant time) on Boolean inputs

The prefix-suffix sumcheck algorithm can be conceptualized as a careful application of the distributive law to reduce the number of multiplications required for a sumcheck that would otherwise be intractably large.

To unpack this, consider the read-checking sumcheck for Shout, as presented in the paper.

Naively, this would require multiplications, which is far too large given and . But suppose has prefix-suffix structure. The key intuition of prefix-suffix structure is captured by the following equation:

You can think of and as the high-order and low-order "bits" of , respectively, obtained by splitting at some partition index. The PrefixSuffixDecomposition trait specifies which prefix/suffix MLEs to evaluate and how to combine them.

We will split four times at four different indices, and these will induce the four phases of the prefix-suffix sumcheck. Each of the four phases encompasses 16 rounds of sumcheck (i.e. 16 of the address variables ), so together they comprise the first rounds of the read-checking sumcheck.

Given our prefix-suffix decomposition of , we can rewrite our read-checking sumcheck as follows:

Note that we have replaced with just . Since is degree 1 in each variable, we will treat it as a single multilinear polynomial while we're binding those variables (the first rounds). The equation as written above depicts the first phase, where is the first 16 variables of , and is the last 48 variables of .

Rearranging the terms in the sum, we have:

Note that the summand is degree 2 in , the variables being bound in the first phase:

  1. appears in
  2. also appears in appears in the paranthesized expression in

Written in this way, it becomes clear that we can treat the first 16 rounds as a mini-sumcheck over just the 16 variables, and with just multilinear terms. If we can efficiently compute the coefficients of each mulitlinear term, the rest of this mini-sumcheck is efficient. Each evaluation of can be computed in constant time, so that leaves the parenthesized term:

This can be computed in : since is one-hot, we can do a single iteration over and only compute the terms of the sum where . We compute a table of evaluation a priori, and can be evaluated in constant time on Boolean inputs.

After the first phase, the high-order 16 variables of will have been bound. We will need to use a new sumcheck expression for the next phase:

Now are random values that the first 16 variables were bound to, and are the next 16 variables of . Meanwhile, now represents the last 32 variables of .

This complicates things slightly, but the algorithm follows the same blueprint as in phase 1. This is a sumcheck over the 16 variables, and there are two multilinear terms. We can still compute each evaluation of in constant time, and we can still compute the parenthesized term in time (observe that there is exactly one non-zero coefficient of per cycle ).

After the first rounds of sumcheck, we are left with:

which we prove using the standard linear-time sumcheck algorithm. Note that here is a virtual polynomial.

Prefix and Suffix Implementations

Jolt modularizes prefix/suffix decomposition using two traits:

  • SparseDensePrefix (under prefixes/)
  • SparseDenseSuffix (under suffixes/)

Each prefix/suffix used in a lookup table implements these traits.

Multiplexing Between Instructions

An execution trace contains many different RISC-V instructions. Note that there is a many-to-one relationship between instructions and lookup tables -- multiple instructions may share a lookup table (e.g., XOR and XORI). To manage this, Jolt uses the InstructionLookupTable trait, whose lookup_table method returns an instruction's associated lookup table, if it has one (some instructions do not require a lookup).

Boolean lookup table flags indicate which table is active on a given cycle. At most one flag is set per cycle. These flags allow us to "multiplex" between all of the lookup tables:

where the sum is over all lookup tables .

Only one table's flag $\textsf{flag}}_{\ell}(j)$ is 1 at any given cycle , so only that table's contributes to the sum.

This term becomes a drop-in replacement for as it appears in the Shout read-checking sumcheck:

Note that each here has prefix-suffix structure.

raf Evaluation

The -evaluation sumcheck in Jolt deviates from the description in the Twist/Shout paper. This is mostly an artifact of the prefix-suffix sumcheck, which imposes some required structure on the lookup index (i.e. the "address" variables ).

Case 1: Interleaved operands

Consider, for example, the lookup table for XOR x y. Intuitively, the lookup index must be crafted from the bits of x and y. A first attempt might be to simply concatenate the bits of x and y, i.e.:

Unfortunately, there is no apparent way for this formulation to satisfy prefix-suffix structure. Instead we will interleave the bits of x and y, i.e.

With this formulation, the prefix-suffix structure is easily apparent. Suppose the prefix-suffix split index is 16, so:

Then XOR x y has the following prefix-suffix decomposition:

By inspection, effectively computes the 8-bit XOR of the high-order bits of x and y, while computes the 24-bit XOR of the low-order bits of x and y. Then the full result XOR x y is obtained by concatenating (adding) the two results.

Now that we've confirmed that we have something with prefix-suffix structure, we can write down the -evaluation sumcheck expression. Instead of a single polynomial, here we have two lookup operands x and y, which are called LeftLookupOperand and RightLookupOperand in code. These are the values that appear in Jolt's R1CS constraints. The point of the -evaluation sumcheck is to relate these (non-one-hot) polynomials to their one-hot counterparts. In the context of instruction execution Shout, this means the polynomial.

Since we have two -like polynomials, we have two sumcheck instances:

This captures the interleaving behavior we described above: LeftLookupOperand is the concatenation of the "odd bits" of , while RightLookupOperand is the concatenation of the "even bits" of .

Case 2: Single operand

Many RISC-V instructions are similar to XOR, in that interleaving the operand bits lends itself to prefix-suffix structure. However, there are some instructions where this is not the case. The execution of some arithmetic instructions, for example, are handled by computing the corresponding operation in the field, and applying a range-check lookup to the result to truncate potential overflow bits.

In this case, the lookup index corresponds to the (single) value y being range-checked, so we do not interleave its bits with another operand. Instead, we just have:

In this case, we have the following sumchecks:

LeftLookupOperand is 0 and RightLookupOperand will be the concatenation of all the bits of .

In order to handle Cases 1 and 2 simultaneously, we can use the same "multiplexing" technique as in the read-checking sumcheck. We use a flag polynomial to indicate which case we're in:

where , , and are circuit flags.

Prefix-suffix structure

These sumchecks have similar structure to the read-checking sumcheck. A side-by-side comparison of the read-checking sumcheck with the sumchecks for Case 1:

As it turns out, and have prefix-suffix structure, so we can also use the prefix-suffix algorithm for these sumchecks. Due to their similarities, we batch the read-checking and these -evaluation sumchecks together in a bespoke fashion.

Other sumchecks

ra virtualization

The lookup tables used for instruction execution are of size , so we set as our decomposition parameter such that .

Similar to the RAM Twist instance, we opt to virtualize the polynomial. In other words, Jolt simply carries out the read checking and evaluation sumchecks as if .

At the conclusion of these sumchecks, we are left with claims about the virtual polynomial. Since the polynomial is uncommitted, Jolt invokes a separate sumcheck that expresses an evaluation in terms of the constituent polynomials. The polynomials are, by definition, a tensor decomposition of , so the " virtualization" sumcheck is the following:

Since the degree of this sumcheck is , using the standard linear-time sumcheck prover algorithm would make this sumcheck relatively slow. However, we employ optimizations specific to high-degree sumchecks, adapting techniques from the Karatsuba and Toom-Cook multiplication algorithms.

One-hot checks

Jolt enforces that the polynomials used for instruction execution are one-hot, using a Booleanity and Hamming weight sumcheck as described in the paper. These implementations follow the Twist and Shout paper closely, with no notable deviations.