Technical Design: VM
Last updated
Last updated
The content of this section is largely copied from the Valida Working ISA Spec. This is a work in progress.
A Valida zkVM consists of a CPU and several coprocessors, which are connected with communication buses. A basic example of a machine layout, omitting some standard chips for simplicity, would be
Communication buses are implemented in permutation arguments (either grand product or multi-set checks), and may be multiplexed for efficiency when only one of a subset of buses will be used in a given cycle.
There are multiple VM configurations. The "Core" configuration is always present, and provides instructions for basic control flow and memory access. Additional configurations, such as "Field Arithmetic" or "Additional Jump" build upon the core configuration and offer additional instructions.
Memory is comprised of word-addressable cells. A given cell contains 4 field elements, each of which are typically used to store a single byte (arbitrary field elements can also be stored). All core and ALU-related instructions operate on cells (i.e. any operand address is word aligned -- a multiple of 4). In the VM compiler, the address of newly added local variables in the stack is word aligned.
For example, a U32 is represented in memory by its byte decomposition (4 elements). To initialize a U32 from an immediate value, we use the SETL16
instruction (see the complete instruction list below), which sets the first two bytes in memory. To initialize a U32 value greater than 16 bits, we can also call the SETH16
instruction to set the upper two bytes.
Our zkVM does not operate on general purpose registers. Instead, instructions refer to variables local to the call frame, i.e. relative to the current frame pointer fp
.
The following notation is used throughout this document:
Operand values: opa
, opb
, opc
denote the value encoded in the operand a, b, or c of the current instruction.
CPU registers: fp
, pc
denote the value of the current frame pointer and program counter, respectively.
Relative addressing: [a]
denote the cell value at address a
offset from fp
, i.e. fp + a
. Variables local to the call frame are denoted in this form. Note that we are omitting fp
in the expression here, but that the first dereference of an operand is always relative to the frame pointer.
Absolute addressing: [[a]]
denotes the cell value at absolute address [a]
. Heap-allocated values are denoted in this form.
Listed below are the instructions offered in each configuration.
Note that field arithmetic instructions only operate on the first element in a cell, which represents a field element instead of a single byte.
Note: These will not be supported in the initial version.
Notes:
Fixed configurable stack size (e.g. 8MB), growing in opposite direction of the heap.
Allocate-only malloc (no de-allocation using free
)
We follow the RISC-V convention and grows the stack downwards. For a function call, the arguments are pushed onto the stack in reverse order. We only allow statically sized allocation on the stack, unlike traditional architectures where alloca
can be used to allocate dynamically. All dynamic allocation will be compiled to heap allocations. We have a special-purpose register dedicated to holding the frame pointer, but not a special-purpose register dedicated to holding the stack pointer.
Note that:
Functions arguments are stored at fp+16, fp+20, ...
Return FP offset (the value to add to FP to get to the value of FP before the call) is stored at fp+12
Return value is stored at fp+8
Return address is stored at fp+4
Local variables are stored at fp, fp-4, fp-8, ...
Memcpy will require roughly 2 cycles per word. We can follow this memcpy implementation on RISC-V.
Multiply stored immediates and return
Multiply arguments and return
The stack at the time of executing mul
inside mul
(line 11) looks like:
TODO: Replace this trace table and associated constraints with more efficient nondeterministic methods
Each instruction is encoded as 6 field elements
Core
Field arithmetic
U32 Arithmetic
Trace cells are also allocated for each selector. In each cycle, the opcode is decoded into selector flags, including but not limited to the following, which are grouped by type (not configuration) below for convenience. All flags are binary values, except for the instruction code.
Instruction code
Operand modifiers
Field arithmetic
Memory
Call
Jumps
U32 arithmetic
Binary operations
We are writing a compiler from LLVM IR to our ISA.
This is a STARK-based zkVM. We are using Plonky3 to implement the polynomial IOP and PCS.
We plan to use the 32-bit field defined by p = 2^31 - 1
, which should give very good performance on GPUs or with most vector instruction sets.
Our VM has no general purpose registers, since memory is cheap.
We will use a conventional R/W memory.
The CPU can do up to three memory operations per cycle, to support binary operations involving two reads and one write.
If we used a single-trace model, we could support this by adding columns for 6 memory operations in each row of our trace: 3 for the chronological memory log and 3 for the (address, timestamp)
ordered memory log.
Instead, we make the memory a separate table (i.e. a separate STARK which gets connected with a permutation argument). We also use multi-table support to implement other coprocessors that are wasteful to include in the main CPU, as their operations may not be used during most cycles (e.g. Keccak).
TODO: Explain the permutation-based continuation implementation.
Initially, we will support lookups only against prover-supplied tables. The main use case is range checks. To perform a 16-bit range check, for example, we would have the prover send a table containing [0 .. 2^16 - 1]
in order. (If the trace was not already 2^16, we would pad it. If it was longer than 2^16, the prover would include some duplicates.) We would then use constraints to enforce that this table starts at 0, ends at 2^16 - 1, and increments by 0 or 1.
Preprocessed tables can also be useful, particularly for bitwise operations like xor. However, we will not support them initially because they require non-succinct preprocessing.
Fast floating point arithmetic doesn't seem important for our anticipated use cases, so we do not plan to support compiling source programs which use floating point primitives.
Instructions are encoded in groups of 6 field elements. The first element in the group contains the opcode, followed by three elements representing the operands and two immediate value flags: , , , , .
Our VM operates under the Harvard architecture, where progrom code is stored separately from main memory. Code is addressed by any field element, starting from . The program counter pc
stores the location (a field element) of the instruction that is being executed.
Our VM cannot represent operand values that are greater than the prime , and cannot distinguish between and . Therefore, any immediate values greater than or equal to need to be expanded into smaller values.
To refer to relative or absolute element values, we use the notation or respectively.
Each instruction contains 5 field element operands, . Often, and are binary flags indicating whther operands and are immediate values or relative offets.
Mnemonic | Operands(asm) | Description |
---|---|---|
Mnemonic | Operands(asm) | Description |
---|---|---|
Mnemonic | Operands(asm) | Description |
---|---|---|
Mnemonic | Operands(asm) | Description |
---|---|---|
Mnemonic | Operands(asm) | Description |
---|---|---|
Instruction | Operands(asm) | |
---|---|---|
We will closely follow RISC-V assembly, making modifications as necessary. The most important difference between our zkVM assembly and RV32IM is that instead of registers x0-31
, we only have two special-purpose registers fp
and pc
. However, we have (up to ) local variables, addressed relative to the current frame point fp
.
Stack (grows downwards, i.e. address decreases from top row to bottom row) |
---|
Pseudo Instruction | Instruction |
---|---|
Stack |
---|
Columns | Configuration | Description |
---|---|---|
Columns are specified by the program code (see the "Instruction Trace" section below).
Trace cells are also allocated to hold buffered read memory values for and , and buffered write values for . We read and write 4 elements from memory at a time to the main trace. These elements are only constrained when the immediate value flags are not set (see the "Instruction Decoding" section below):
Cell | Configuration | Description |
---|---|---|
Cell | Description |
---|---|
The memory table is sorted by ()
Cell | Description |
---|---|
There are also 5 helper value cells: through .
Mnemonic | Operands(asm) | Encoded |
---|---|---|
Mnemonic | Operands(asm) | Encoded |
---|---|---|
Mnemonic | Operands(asm) | Encoded |
---|---|---|
Mnemonic | Operands(asm) | Encoded |
---|---|---|
Selector | Configuration | Description |
---|---|---|
Selector | Configuration | Description |
---|---|---|
Selector | Configuration | Description |
---|---|---|
Selector | Configuration | Description |
---|---|---|
Selector | Configuration | Description |
---|---|---|
Selector | Configuration | Description |
---|---|---|
Selector | Configuration | Description |
---|---|---|
Selector | Configuration | Description |
---|---|---|
LW / LOAD32
a(fp), c(fp)
Follow the pointer stored at offset from the current frame pointer and write the next 4 byte values to those beginning at offset a. Operand is unused, but is constrained to in the trace. LOAD32 is used to load 4 bytes from the heap, and is aligned (i.e. the address at offset is assumed to be a multiple of ).
SW / STORE32
b(fp), c(fp)
Write the 4 byte values beginning at the address stroed at offset to those beginning at offset . Operand is unused, but is constrained to in the trace. STORE32 is used to write 4 bytes to the heap, and is aligned.
LOADFP
a(fp), b
Load the value of fp + b into local stack variable at offset a.
JAL
a(fp), b, c
Jump to address and link: Store the to local stack variable at offset , then set to field element . Set to .
JALV
a(fp), b(fp), c(fp)
Jump to variable and link: Store the to local stack variable at offset , then set to the field element . Set to fp+.
BEQ
a, b(fp), c(fp)
If , then set the program counter to
BEQI
a, b(fp), c
If , then set the program counter to
BNE
a, b(fp), c(fp)
If , then set the program counter to
BNEI
a, b(fp), c
If , then set the program counter to
IMM32
a(fp), b, c, d, e
Write the immediate values to the cell located at offset .
STOP
No operands
Halt the program.
READ_ADVICE
a(fp)
Read the next byte on the input tape into the cell located at offset a.
FEADD
a(fp), b(fp), c(fp)
and are a flags denoting whether and are interpreted as an immediate or offset. Let if and otherwise. Let if and otherwise. The instruction compute , and write the result to offset
FEMUL
a(fp), b(fp), c(fp)
and are a flags denoting whether and are interpreted as an immediate or offset. Let if and otherwise. Let if and otherwise. The instruction compute , and write the result to offset
TO_FE
a(fp), b(fp)
Convert an U32, represented by 4 field elements starting at offset b, to a field element stored to the first field element at offset . is assuemd to be a multiple of .
FROM_FE
a(fp), b(fp)
Convert a field element to an U32 stored at offset a, which is assumed to be a multiple of .
ADD
a(fp), b(fp), c(fp)
Compute the unchecked addition of the U32 values at cell offsets and and write the sum to cell offset . Note that because a full 32-bit value does not fit within one field element, we assume that values have been decomposed into 4 8-byte elements. The summed output is stored at cell offset . The same limb decomposition is used for the other U32 operations listed below.
ADDI
a(fp), b(fp), c
Compute the unchecked addition of the U32 variable at cell offsets and an immediate , and write the sum to cell offset .
SUB
a(fp), b(fp), c(fp)
Unchecked subtraction
SUBI
a(fp), b(fp), c
Unchecked subtraction
MUL
a(fp), b(fp), c(fp)
Unchecked multiplication
MULI
a(fp), b(fp), c
Unchecked multiplication
MULHU
a(fp), b(fp), c(fp)
High bits of unsigned multiplication
MULHUI
a(fp), b(fp), c
High bits of unsigned multiplication
MULHS
a(fp), b(fp), c(fp)
High bits of signed multiplication
MULHSI
a(fp), b(fp), c
High bits of signed multiplication
DIV
a(fp), b(fp), c(fp)
Unsigned division
DIVI
a(fp), b(fp), c
Unsigned division
SDIV
a(fp), b(fp), c(fp)
Signed division
SDIVI
a(fp), b(fp), c
Signed division
SH{L,R}
a(fp), b(fp), c(fp)
Bitwise shift [b] left/right by [c] and write to offset .
SH{L,R}I
a(fp), b(fp), c
Bitwise shift [b] left/right by c and write to offset .
SRA
a(fp), b(fp), c(fp)
Arithmetic shift b right by [c] and write to offset .
SRAI
a(fp), b(fp), c
Arithmetic shift b right by c and write to offset
LT
a(fp), b(fp), c(fp)
Set local variable to if and otherwise. Uses unsigned comparison.
LTI
a(fp), b(fp), c
Set local variable to if and otherwise. Uses unsigned comparison.
NE
a(fp), b(fp), c(fp)
Set local variable a to 0 if [b] = [c] and 1 otherwise.
NEI
a(fp), b(fp), c
Set local variable a to 0 if [b] = c and 1 otherwise.
EQ
a(fp), b(fp), c(fp)
Set local variable a to 1 if [b] = [c] and 0 otherwise.
EQI
a(fp), b(fp), c
Set local variable a to 1 if [b] = c and 0 otherwise.
AND
a(fp), b(fp), c(fp)
Set to bitwise-and
ANDI
a(fp), b(fp), c
Set to bitwise-and c
OR
a(fp), b(fp), c(fp)
Set to bitwise-or
ORI
a(fp), b(fp), c
Set to bitwise-or c
XOR
a(fp), b(fp), c(fp)
Set to bitwise-xor
XORI
a(fp), b(fp), c
Set to bitwise-xor c
WRITE
b(fp)
Write byte to the output
LOAD8
a(fp), b(fp)
Load a byte at the address specified by local variable at offset to local variable at offset .
STORE8
b(fp), c(fp)
Store a byte encoded at offset to the address encoded in offset
STORE8I
b(fp), c
Store a byte encoded in the field element to the address encoded in offset
Arg 2
Arg 1
Return FP offset
Return value
Return address
Local 1 (<- Current fp)
Local 2
...
Local N
call label
jal 0(fp), -b, label; addifp b
, where b is size of the current stack frame plus the call frame size for instantiate a call to lable
ret
jalv 4(fp), 0(fp), 12(fp)
, set pc to [fp] where the return address is stored
.. (<- fp of main)
7
938253
0 (before mul2) -> 6567771 (after mul2)
main:7 (<- fp of mul2)
Core
Clock cycle
Core
Program counter
Core
Frame pointer
Core
Instruction opcode
Core
Operand
Core
Operand
Core
Operand
Core
Operand , flag for if is immediate or offset.
Core
Operand , flag for if is immediate or offset.
Core
(if is not set)
Core
(if is not set)
Core
Core
Core
Core
Core
Core
Core
Core
Core
Core
Value written to
Core
Value written to
Core
Value written to
Core
Value written to
Address
Clock cycle
Value 0
Value 1
Value 2
Value 3
Lower 16 bits of
Upper 16 bits of
Nondeterministic inverse of
LW / LOAD32
a(fp), c(fp)
, , _, , ,
SW / STORE32
b(fp), c(fp)
, _, , , ,
JAL
a(fp), b, c
, , , , ,
JALV
a(fp), b(fp), c
, , , , ,
BEQ
a, b(fp), c(fp)
, , , , ,
BEQI
a, b(fp), c
, , , , ,
BNE
a, b(fp), c(fp)
, , , , ,
BNEI
a, b(fp), c
, , , , ,
IMM32
a, b, c, d, e
, , , , ,
READ_ADVICE
a(fp)
OP_{READ_ADVICE}, a, 0, 0, 0, 0
STOP
No operands
OP_STOP, 0, 0, 0, 0, 0
LOADFP
a(fp), b
OP_LOADFP, a, b, 0, 0, 0
FEADD
a(fp), b(fp), c(fp)
, , , , 0, 0
FEADDI
a(fp), b(fp), c
, , , , 0, 1
FEMUL
a(fp), b(fp), c(fp)
, , , , 0, 0
FEMULI
a(fp), b(fp), c
OP_FEMULI, a, b, c, 0, 1
TO_FE
a(fp), b(fp)
OP_{TO_FE}, a, b, 0, 0, 0
FROM_FE
a(fp), b(fp)
OP_{FROM_FE}, a, b, 0, 0, 0
ADD
a(fp), b(fp), c(fp)
, , , , ,
ADDI
a(fp), b(fp), c
, , , , ,
SUB
a(fp), b(fp), c(fp)
, , , , ,
SUBI
a(fp), b(fp), c
, , , , ,
MUL
a(fp), b(fp), c(fp)
, , , , ,
MULI
a(fp), b(fp), c
, , , , ,
DIV
a(fp), b(fp), c(fp)
, , , , ,
SH{L,R}
a(fp), b(fp), c(fp)
, , , , ,
SH{L,R}I
a(fp), b(fp), c
, , , , ,
ISH{L,R}
a(fp), b, c(fp)
, , , , ,
SLT
a(fp), b(fp), c(fp)
, , , , ,
SLT
a(fp), b(fp), c
, , , , ,
AND
a(fp), b(fp), c(fp)
, , , , , _
OR
a(fp), b(fp), c(fp)
, , , , , _
XOR
a(fp), b(fp), c(fp)
, , , , , _
Core
Instruction code
Core
Immediate value flag for operand ( denotes offset, denotes immediate value)
Core
Immediate value flag for operand ( denotes offset, denotes immediate value)
Field Arithmetic
Addition
Field Arithmetic
Multiplication
Core
Store 4 bytes at at a memory address
Core
Load 4 bytes from a memory address
Core
Store a byte at a memory address
Core
Load a byte from a memory address
Core
Update the frame pointer
Clock cycle
Requested instruction (constrained by communication bus)
Selector flag for addition
Selector flag for subtraction
Selector flag for multiplication
Selector flag for division
s_\text{checked_add}
Selector flag for checked addition (requires to be set as well)
s_\text{checked_mul}
Selector flag for checked multiplication (requires to be set as well)
Address of input 1
Address of input 2
Address of output
Input 1 (12-bit limb)
Input 1 (12-bit limb)
Input 1 (12-bit limb)
Input 2 (12-bit limb)
Input 2 (12-bit limb)
Input 2 (12-bit limb)
Output (12-bit limb)
Output (12-bit limb)
Output (12-bit limb)
Output (12-bit limb)
Output (12-bit limb)
s_\text{jump_eq}
Core
Jump equal flag
s_\text{jump_neq}
Additional Jumps
Jump not equal flag
s_\text{jump_ge}
Additional Jumps
Jump greater than or equal to flag
s_\text{jump_lt}
Additional Jumps
Jump less than flag
s_\text{u32_add}
U32 Arithmetic
s_\text{u32_sub}
U32 Arithmetic
s_\text{u32_mul}
U32 Arithmetic
s_\text{u32_div}
U32 Arithmetic
s_\text{u32_checked_add}
U32 Arithmetic
s_\text{u32_checked_mul}
U32 Arithmetic
s_\text{bitwise_and}
Bitwise
s_\text{bitwise_or}
Bitwise
s_\text{bitwise_xor}
Bitwise