Range-checks

In most cases, tables deal with U256 words, split into 32-bit limbs (to avoid overflowing the field). To prevent a malicious prover from cheating, it is crucial to range-check those limbs.

What to range-check?

One can note that every element that ever appears on the stack has been pushed. Therefore, enforcing a range-check on pushed elements is enough to range-check all elements on the stack. Similarly, all elements in memory must have been written prior, and therefore it is enough to range-check memory writes. However, range-checking the PUSH and MSTORE opcodes is not sufficient.

  1. Pushes and memory writes for "MSTORE_32BYTES" are range-checked in "BytePackingStark", except PUSH operations happening in privileged mode. See push_general_view.

  2. Syscalls, exceptions and prover inputs are range-checked in "ArithmeticStark".

  3. The inputs and outputs of binary and ternary arithmetic operations are range-checked in "ArithmeticStark".

  4. The inputs' bits of logic operations are checked to be either 1 or 0 in "LogicStark". Since "LogicStark" only deals with bitwise operations, this is enough to have range-checked outputs as well.

  5. The inputs of Keccak operations are range-checked in "KeccakStark". The output digest is written as bytes in "KeccakStark". Those bytes are used to reconstruct the associated 32-bit limbs checked against the limbs in "CpuStark". This implicitly ensures that the output is range-checked.

Note that some operations do not require a range-check:

  1. "MSTORE_GENERAL" read the value to write from the stack. Thus, the written value was already range-checked by a previous push.

  2. "EQ" reads two -- already range-checked -- elements on the stack, and checks they are equal. The output is either 0 or 1, and does therefore not need to be checked.

  3. "NOT" reads one -- already range-checked -- element. The result is constrained to be equal to , which implicitly enforces the range check.

  4. "PC": the program counter cannot be greater than in user mode. Indeed, the user code cannot be longer than , and jumps are constrained to be JUMPDESTs. Moreover, in kernel mode, every jump is towards a location within the kernel, and the kernel code is smaller than . These two points implicitly enforce 's range check.

  5. "GET_CONTEXT", "DUP" and "SWAP" all read and push values that were already written in memory. The pushed values were therefore already range-checked.

Range-checks are performed on the range , to limit the trace length.

Lookup Argument

To enforce the range-checks, we leverage logUp, a lookup argument by Ulrich Häbock. Given a looking table and a looked table , the goal is to prove that

In our case, and is composed of all the columns in each STARK that must be range-checked.

The logUp paper explains that proving the previous assertion is actually equivalent to proving that there exists a sequence such that:

The values of can be stored in different columns of length each. In that case, the equality becomes:

The 'multiplicity' of value is defined as the number of times appears in . In other words:

Multiplicities provide a valid sequence of values in the previously stated equation. Thus, if we store the multiplicities, and are provided with a challenge , we can prove the lookup argument by ensuring:

However, the equation is too high degree. To circumvent this issue, Häbock suggests providing helper columns and such that at a given row :

The helper columns can be batched together to save columns. We can batch at most helper functions together. In our case, we batch them 2 by 2. At row , we now have:

If is odd, then we have one extra helper column:

For clarity, we will assume that is even in what follows.

Let be a generator of a subgroup of order . We extrapolate and to get polynomials such that, for : .

We can define the following polynomial:

Constraints

With these definitions and a challenge , we can finally check that the assertion holds with the following constraints:

These ensure that We also need to ensure that is well constructed for all :

Note: if is odd, we have one unbatched helper column for which we need a last constraint:

Finally, the verifier needs to ensure that the table was also correctly computed. In each STARK, is computed starting from 0 and adding at most 1 at each row. This construction is constrained as follows: