M extension

Jolt supports the RISC-V "M" extension for integer multiplication and division. The instructions included in this extension are described here. For RV32, the M extension includes 8 instructions: MUL, MULH, MULHSU, MULU, DIV, DIVU, REM, and REMU.

The Jolt paper describes how to handle the M extension instructions in Section 6, but our implementation deviates from the paper in a couple ways (described below).

Virtual sequences

Section 6.1 of the Jolt paper introduces virtual instructions and registers –– some of the M extension instructions cannot be implemented as a single subtable decomposition, but rather must be split into a sequence of instructions which together compute the output and places it in the destination register. In our implementation, these sequences are captured by the VirtualInstructionSequence trait.

The instructions that comprise such a sequence can be a combination of "real" RISC-V instructions and "virtual" instructions which only appear in the context of virtual sequences. We also introduce 32 virtual registers as "scratch space" where instructions in a virtual sequence can write intermediate values.

Deviations from the Jolt paper

There are three inconsistencies between the virtual sequences provided in Section 6.3 of the Jolt paper, and the RISC-V specification. Namely:

  1. The Jolt prover (as described in the paper) would fail to produce a valid proof if it encountered a division by zero; since the divisor y is 0, the ASSERT_LTU/ASSERT_LT_ABS would always fail (for DIVU and DIV, respectively).
  2. The MLE provided for ASSERT_LT_ABS in Section 6.1.1 doesn't account for two's complement.
  3. The ASSERT_EQ_SIGNS instruction should always return true if the remainder is 0.

To address these issues, our implementation of DIVU, DIV, REMU, and REM deviate from the Jolt paper in the following ways.

DIVU virtual sequence

  1. ADVICE --, --, --, // store non-deterministic advice into
  2. ADVICE --, --, --, // store non-deterministic advice into
  3. MUL , , --, // compute q * y
  4. ASSERT_VALID_UNSIGNED_REMAINDER , , --, -- // assert that y == 0 || r < y
  5. ASSERT_LTE , , --, -- // assert q * y <= x
  6. ASSERT_VALID_DIV0 , , --, -- // assert that y != 0 || q == 2 ** WORD_SIZE - 1
  7. ADD , , --, // compute q * y + r
  8. ASSERT_EQ , , --, --
  9. MOVE , --, --, rd

REMU virtual sequence

  1. ADVICE --, --, --, // store non-deterministic advice into
  2. ADVICE --, --, --, // store non-deterministic advice into
  3. MUL , , --, // compute q * y
  4. ASSERT_VALID_UNSIGNED_REMAINDER , , --, -- // assert that y == 0 || r < y
  5. ASSERT_LTE , , --, -- // assert q * y <= x
  6. ADD , , --, // compute q * y + r
  7. ASSERT_EQ , , --, --
  8. MOVE , --, --, rd

DIV virtual sequence

  1. ADVICE --, --, --, // store non-deterministic advice into
  2. ADVICE --, --, --, // store non-deterministic advice into
  3. ASSERT_VALID_SIGNED_REMAINDER , , --, -- // assert that r == 0 || y == 0 || (|r| < |y| && sign(r) == sign(y))
  4. ASSERT_VALID_DIV0 , , --, -- // assert that y != 0 || q == 2 ** WORD_SIZE - 1
  5. MUL , , --, // compute q * y
  6. ADD , , --, // compute q * y + r
  7. ASSERT_EQ , , --, --
  8. MOVE , --, --, rd

REM virtual sequence

  1. ADVICE --, --, --, // store non-deterministic advice into
  2. ADVICE --, --, --, // store non-deterministic advice into
  3. ASSERT_VALID_SIGNED_REMAINDER , , --, -- // assert that r == 0 || y == 0 || (|r| < |y| && sign(r) == sign(y))
  4. MUL , , --, // compute q * y
  5. ADD , , --, // compute q * y + r
  6. ASSERT_EQ , , --, --
  7. MOVE , --, --, rd

R1CS constraints

Ciruict flags

With the M extension we introduce the following circuit flags:

  1. is_virtual: Is this instruction part of a virtual sequence?
  2. is_assert: Is this instruction an ASSERT_* instruction?
  3. do_not_update_pc: If this instruction is virtual and not the last one in its sequence, then we should not update the PC. This is because all instructions in virtual sequences are mapped to the same ELF address.

Uniform constraints

The following constraints are enforced for every step of the execution trace:

  1. If the instruction is a MUL, MULU, or MULHU, the lookup query is the product of the two operands x * y (field multiplication of two 32-bit values).
  2. If the instruction is a MOV or MOVSIGN, the lookup query is a single operand x (read from the first source register rs1).
  3. If the instruction is an assert, the lookup output must be true.

Program counter constraints

Each instruction in the preprocessed bytecode contains its (compressed) memory address as given by the ELF file. This is used to compute the expected program counter for each step in the program trace.

If the do_not_update_pc flag is set, we constrain the next PC value to be equal to the current one. This handles the fact that all instructions in virtual sequences are mapped to the same ELF address.

This also means we need some other mechanism to ensure that virtual sequences are executed in order and in full. If the current instruction is virtual, we can constrain the next instruction in the trace to be the next instruction in the bytecode. We observe that the virtual sequences used in the M extension don't involve jumps or branches, so this should always hold, except if we encounter a virtual instruction followed by a padding instruction. But that should never happend because an execution trace should always end with some return handling, which shouldn't involve a virtual sequence.