Technical Design

We want to make it practical to compile conventional code to a zkVM with reasonable performance. No zkVM today is optimized for this goal. We desire a VM with neither registers nor a dedicated stack, and one that has a small field size with low-degree constraints. We also want efficient compilation from LLVM IR to the set of VM instructions.

zkVM Design

Our vision

We want to make it practical to compile conventional code to a zkVM with reasonable performance. No zkVM today is optimized for this goal. We desire a VM with neither registers nor a dedicated stack, and one that has a small field size with low-degree constraints. We also want efficient compilation from LLVM IR to the set of VM instructions.

  • Architecture

    The zkVM consists of a CPU and several coprocessors, which are connected with communication buses:

    graph TD;
       CPU* --- Memory*;
       CPU* --- Memory*;
       CPU* --- Memory*;
       CPU* --- Tip5*;
       CPU* --- Logic;
       CPU* --- u32_mul;
       CPU* --- u32_add_sub;
       CPU* --- Bls12;
       CPU* --- Keccak-f;
       Memory* --- Pager*;

    * Part of the core (non-optional) configuration

    Communication buses are implemented using 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.

  • Instruction format

    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:
    opcode, opa, opb, opc, immb, immc.

  • Program ROM

    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 0. The program counter pc stores the location (a field element) of the instruction that is being executed.

  • Memory

    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.

  • Immediate Values

    Our VM cannot represent operand values that are greater than the prime p, and cannot distinguish between 0 and p. Therefore, any immediate values greater than or equal to p need to be expanded into smaller values.

  • Registers

    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.

  • Notation

    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.

    To refer to relative or absolute element values, we use the notation [a]elem or [[a]]elem respectively.

  • Instruction list

    Each instruction contains 5 field element operands, a, b, c, d, e. Often, d and e are binary flags indicating whther operands a and b are immediate values or relative offets.

    Listed below are the instructions offered in each configuration.

    Core
    Mnemonic
    Operands(asm)
    Description
    LW / LOAD32
    a(fp), c(fp)
    Follow the pointer stored at offset c from the current frame pointer and write the next 4 byte values to those beginning at offset a. Operand b is unused, but is constrained to [c] in the trace. LOAD32 is used to load 4 bytes from the heap, and is aligned (i.e. the address at offset c is assumed to be a multiple of 4).
    SW / STORE32
    b(fp), c(fp)
    Write the 4 byte values beginning at the address stroed at offset c to those beginning at offset b. Operand a is unused, but is constrained to [c] in the trace. STORE32 is used to write 4 bytes to the heap, and is aligned.
    JAL
    a(fp), b, c
    Jump to address and link: Store the pc+1 to local stack variable at offset a, then set pc to field element b. Set fp to fp+c.
    JALV
    a(fp), b(fp), c(fp)
    Jump to variable and link: Store the pc+1 to local stack variable at offset a, then set pc to the field element [b]elem. Set fp to [c].
    BEQ
    a, b(fp), c(fp)
    If [b]=[c], then set the program counter pc to a.
    BEQI
    a, b(fp), c
    If [b]=c, then set the program counter pc to a.
    BNE
    a, b(fp), c(fp)
    If [b]≠[c], then set the program counter pc to a.
    BNEI
    a, b(fp), c
    If [b]≠c, then set the program counter pc to a.
    BNE
    a, b(fp), c(fp)
    If [b]≠[c], then set the program counter pc to a.
    IMM32
    a, b, c, d, e
    Write the immediate values b, c, d, e to the cell located at offset a.
    Field arithmetic
    Mnemonic
    Operands(asm)
    Description
    FEADD
    a(fp), b(fp), c(fp)
    d and a are a flags denoting whether a and b are interpreted as an immediate or offset. Let A=a if d=1and [a]elem otherwise. Let B=b if e=1 and [b]elem otherwise. The instruction compute A+B, and write the result to offset c
    FEMUL
    a(fp), b(fp), c(fp)
    d and e are a flags denoting whether a and b are interpreted as an immediate or offset. Let A=a if d=1and [a]elem otherwise. Let B=b if e=1 and [b]elem otherwise. The instruction compute A⋅B, and write the result to offset c
    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 a. a is assumed to be a multiple of 4.
    FROM_FE
    a(fp), b(fp)
    Convert a field element [b]elem to an U32 stored at offset a, which is assumed to be a multiple of 4.

    Note that field arithmetic instructions only operate on the first element in a cell, which represents a field element instead of a single byte.

    U32 Arithmetic
    Mnemonic
    Operands(asm)
    Description
    ADD
    a(fp), b(fp), c(fp)
    Compute the unchecked addition of the U32 values at cell offsets b and c and write the sum to cell offseta. 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 a. 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 b and an immediate c, and write the sum to cell offset a.
    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 subtraction
    MULI
    a(fp), b(fp), c
    Unchecked subtraction
    DIV
    a(fp), b(fp), c(fp)
    Division
    SH{L,R}
    a(fp), b(fp), c(fp)
    Shift [b] left/right by [c] and write to offset a.
    SH{L,R}I
    a(fp), b(fp), c
    Shift [b] left/right by c and write to offset a.
    ISH{L,R}
    a(fp), b, c(fp)
    Shift b left/right by [c] and write to offset a.
    SLT
    a(fp), b(fp), c(fp)
    Set local variable a to 1 if [b]<[c] and 0 otherwise.
    SLT
    a(fp), b(fp), c
    Set local variable a to 1 if [b]<c and 0 otherwise.
    Bitwise
    Mnemonic
    Operands(asm)
    Description
    AND
    a(fp), b(fp), c(fp)
    Set [a] to [b] bitwise-and [c]
    OR
    a(fp), b(fp), c(fp)
    Set [a] to [b] bitwise-or [c]
    XOR
    a(fp), b(fp), c(fp)
    Set [a] to [b] bitwise-xor [c]
    Byte Manipulation

    Note: These will not be supported in the initial version.

    Instruction
    Operands(asm)
    Description
    LOAD8
    a(fp), b(fp)
    Load a byte at the address specified by local variable at offset b to local variable at offset a.
    STORE8
    b(fp), c(fp)
    Store a byte encoded at offset c to the address encoded in offset b.
    STORE8I
    b(fp), c
    Store a byte encoded in the field element c to the address encoded in offset b.
  • Heap allocation

    Note:

    • Fixed configurable stack size (e.g. 8MB), growing in opposite direction of the heap.

    • Allocate-only malloc (no de-allocation using free)

  • Assembly

    Byte Manipulation

    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 231−1) local variables, addressed relative to the current frame point fp.

    Calling convention / stack frame
    Stack (grows downwards, i.e. address decreases from top row to bottom row)
    Arg 2
    Arg 1
    Return FP
    Return value
    Return address (<- Current fp)
    Local 1
    Local 2
    ...
    Local N

    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. Instead of using a frame pointer that points at the begining of the frame, we use a stack pointer which points at the first free stack cell.

    Note that:

    • Functions arguments are stored at fp + 12, fp + 16, …

    • Return FP (the value of FP before the call) is stored at fp+8

    • Return value is stored at fp + 4

    • Return address is stored at fp

    • Local variables are stored at fp - 4, fp - 8, …

    Pseudo instructions
    Pseudo Instruction
    Instruction
    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
    jal 4(fp), 0(fp), 0, set pc to [fp] where the return address is stored
    Implementing MEMCPY/SET/MOVE

    Memcpy will require rougly 2 cycles per word. We can follow this memcpy implementation on RISC-V.

    Heap allocations
  • Example programs

    Heap allocations

    define i32 @main() {
     %1 = alloca i32, align 4
     %2 = alloca i32, align 4
     %3 = alloca i32, align 4
     store i32 24, i32* %1, align 4
     store i32 7, i32* %2, align 4
     %4 = load i32, i32* %1, align 4
     %5 = load i32, i32* %2, align 4
     %6 = mul nsw i32 %4, %5
     store i32 %6, i32* %3, align 4
     %7 = load i32, i32* %1, align 4
     ret i32 %7
    }

    main:
     sub      -4(fp), 0(fp), 0(fp)      # Setup the 0 local variable at fp - 4
     add     -8(fp), -4(fp), 24,    # Set [fp - 8] to 24
     add    -12(fp), -4(fp), 7,     # Set [fp - 12] to 7

     mul     -16(fp), 8(fp), 12(fp)    # Set [fp - 16] to 24 * 7

     add    4(fp), -8(fp), 0     # Set return value at [fp + 4] to [fp - 8]
     ret

    Multiply arguments and return

    define i32 @main() {
     %1 = alloca i32, align 4
     store i32 0, i32* %1, align 4
     %2 = call i32 @mul(i32 938253, i32 7)
     ret i32 %2
    }

    define i32 @mul2(i32 %0, i32 %1) {
     %3 = alloca i32, align 4
     %4 = alloca i32, align 4
     store i32 %0, i32* %3, align 4
     store i32 %1, i32* %4, align 4
     %5 = load i32, i32* %3, align 4
     %6 = load i32, i32* %4, align 4
     %7 = mul nsw i32 %5, %6
     ret i32 %7
    }

    main:
       imm32     -4(fp), 938253
       imm32     -8(fp), 7
       call         mul2
       # call translates to
       # jal 0(fp), -16, mul2  " store pc + 1 to [fp], add -16 to fp, set pc to mul2
       # addifp         16

       ret

    mul2:
       mul    4(fp), 8(fp), 12(fp)
       ret

    The stack at the time of executing mul inside mul (line 11) looks like:

    Stack
    .. (<- fp of main)
    Arg 1
    7
    938253
    0 (before mul2) -> 6567771 (after mul2)
    main:7 (<- fp of mul2))
  • Trace

    Main CPU
    Columns
    Configuration
    Description
    clk
    Core
    Clock cycle
    pc
    Core
    Program counter
    fp
    Core
    Frame pointer
    opcode
    Core
    Instruction opcode
    opa
    Core
    Operand a
    opb
    Core
    Operand b
    opc
    Core
    Operand c
    opd
    Core
    Operand d, flag for if opa is immediate or offset.
    ope
    Core
    Operand e, flag for if opb is immediate or offset.
    addra
    Core
    fp + opa (if opd is not set)
    addrb
    Core
    fp + opb (if ope is not set)
    addrc
    Core
    fp + opc

    Columns opcode, opa, opb, opc, opd, ope are specified by the program code (see the “Instruction Trace” section below).

    Trace cells are also allocated to hold buffered read memory values for addra and addrb, and buffered write values for addrc . 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
    va,0
    Core
    [addea]elem
    va,1
    Core
    [addea + 1]elem
    va,2
    Core
    [addea + 2]elem
    va,3
    Core
    [addea + 3]elem
    vb,0
    Core
    [addeb]elem
    vb,1
    Core
    [addea + 1]elem
    vb,2
    Core
    [addea + 2]elem
    vb,3
    Core
    [addea + 3]elem
    vc,0
    Core
    Value written to addec
    vc,1
    Core
    Value written to addec + 1
    vc,2
    Core
    Value written to addec + 2
    vc,3
    Core
    Value written to addec + 3
    Memory
    Cell
    Description
    addr
    Address
    clk
    Clock cycle
    val0
    Value 0
    val1
    Value 1
    val2
    Value 2
    val3
    Value 3
    d1
    Lower 16 bits of clk’ - clk - 1
    d2
    Upper 16 bits of clk’ - clk - 1
    t
    Nondeterministic inverse of addr’ - addr

    The memory table is sorted by (addr, clk)

    U32 Arithmetic

    TODO: Replace this trace table and associated constraints with more efficient nondeterministic methods

    Cell
    Description
    clk
    Clock cycle
    sinstr
    Requested instruction (constrained by communication bus)
    sadd
    Selector flag for addition
    ssub
    Selector flag for subtraction
    smul
    Selector flag for multiplication
    sdiv
    Selector flag for division
    schecked_add
    Selector flag for checked addition (requires sadd to be set as well)
    schecked_mul
    Selector flag for checked multiplication (requires smul to be set as well)
    a
    Address of input 1
    b
    Address of input 2
    c
    Address of output
    a0
    Input 1 (12-bit limb)
    a1
    Input 1 (12-bit limb)
    a2
    Input 1 (12-bit limb)
    b0
    Input 2 (12-bit limb)
    b1
    Input 2 (12-bit limb)
    b2
    Input 2 (12-bit limb)
    c0
    Output 1 (12-bit limb)
    c1
    Output 1 (12-bit limb)
    c2
    Output 1 (12-bit limb)
    c3
    Output 1 (12-bit limb)
    c4
    Output 1 (12-bit limb)

    There are also 5 helper value cells: ℎ0 through ℎ4.

  • Instruction Trace

    Core
    Mnemonic
    Operands(asm)
    Encoded
    LW / LOAD32
    a(fp), c(fp)
    OPLW, a, _, c, 0, 0
    Follow the pointer stored at offset c from the current frame pointer and write the next 4 byte values to those beginning at offset a. Operand c is unused, but is constrained to [c] in the trace. LOAD32 is used to load 4 bytes from the heap, and is aligned (i.e. the address at offset c
    is assumed to be a multiple of 4).
    SW / STORE32
    b(fp), c(fp)
    OPSW, _, b, c, 0, 0
    Write the 4 byte values beginning at the address stored at offset c to those beginning at offset b. Operand a is unused, but is constrained to [c] in the trace. STORE32 is used to write 4 bytes to the heap, and is aligned.
    JAL
    a(fp), b, c
    OPjump, a, b, c, 1, 1
    Jump to address and link: Store the pc+1 to local stack variable at offset a, then set pc to field element b. Set fp to fp+c.
    JALV
    a(fp), b (fp), c
    OPjump, a, b, c, 0, 1
    Jump to address and link: Store the pc+1 to local stack variable at offset a, then set pc to field element [b]elem. Set fp to fp+c.
    BEQ
    a, b (fp), c (fp)
    OPbranch, a, b, c, 0, 0
    If [b]=[c], then set the program counter pc to a.
    BEQ
    a, b (fp), c
    OPbranch, a, b, c, 0, 1
    If [b]=c, then set the program counter pc to a.
    BNE
    a, b (fp), c (fp)
    OPbranch, a, b, c, 1, 0
    If [b]≠[c], then set the program counter pc to a.
    BNEI
    a, b (fp), c
    OPbranch, a, b, c, 1, 1
    If [b]≠c, then set the program counter pc to a.
    IMM32
    a, b, c, d, e
    OPIMM, a, b, c, d, e
    Write the immediate values b, c, d, e to the cell located at offset a.
    Field arithmetic
    Mnemonic
    Operands(asm)
    Encoded
    FEADD
    a(fp), b(fp), c(fp)
    OPFEADD, a, b, c, d, e
    d and e are a flags denoting whether a and b are interpreted as an immediate or offset. Let A=a if d=1 and [a]elem otherwise. Let B=b if e=1and [b]elem otherwise. The instruction compute A+B, and write the result to offset c.
    FEMUL
    a(fp), b(fp), c(fp)
    OPFEMUL, a, b, c, d, e
    d and e are a flags denoting whether a and b are interpreted as an immediate or offset. Let A=a if d=1 and [a]elem otherwise. Let B=b if e=1and [b]elem otherwise. The instruction compute A⋅B, and write the result to offset c.
    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 b.ais assuemd to be a multiple of 4.
    FROM_FE
    a(fp), b(fp)
    Convert a field element [b]elem to an U32 stored at offset a, which is assumed to be a multiple of 4.
    U32 Arithmetic
    Mnemonic
    Operands(asm)
    Encoded
    ADD
    a(fp), b(fp), c(fp)
    OPADD, a, b, c, 0, 0
    Compute the unchecked addition of the U32 values at cell offsets b and c and write the sum to cell offset a . 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 a. The same limb decomposition is used for the other U32 operations listed below.
    ADDI
    a(fp), b(fp), c
    OPADD, a, b, c0, 1, c1
    Compute the unchecked addition of the U32 variable at cell offsets b and an immediate c, and write the sum to cell offset a.
    SUB
    a(fp), b(fp), c(fp)
    OPSUB, a, b, c, 0, 0
    Unchecked subtraction
    SUBI
    a(fp), b(fp), c
    OPADD, a, b, c1, 1, c2
    Unchecked subtraction
    MUL
    a(fp), b(fp), c(fp)
    OPMUL, a, b, c, 0, 0
    Unchecked subtraction
    MULI
    a(fp), b(fp), c
    OPMUL, a, b, c1, 1, c2
    Unchecked subtraction
    DIV
    a(fp), b(fp), c(fp)
    OPMUL, a, b, c, 0, 0
    Division
    SH{L,R}
    a(fp), b(fp), c(fp)
    OPSHIFT, a, b, c, 0, 0
    Shift [b] left/right by [c] and write to offset a.
    SH{L,R}I
    a(fp), b(fp), c
    OPSHIFT, a, b, c, 0, 1
    Shift [b] left/right by c and write to offset a.
    ISH{L,R}
    a(fp), b. c(fp)
    OPSHIFT, a, b, c, 0, 1
    Shift b left/right by [c] and write to offset a.
    SLT
    a(fp), b(fp), c(fp)
    OPSLT, a, b, c, 0, 0
    Set local variable a to 1 if [b]<[c] and 0 otherwise.
    SLT
    a(fp), b(fp), c
    OPSLT, a, b, c, 0, 1
    Set local variable a to 1 if [b]<c and 0 otherwise.
    Bitwise
    Mnemonic
    Operands(asm)
    Encoded
    ADD
    a(fp), b(fp), c(fp)
    OPADD, a, b, c, 0, _
    Set [a] to [b] bitwise-and [c]
    OR
    a(fp), b(fp), c(fp)
    OPOR, a, b, c, 0, _
    Set [a] to [b] bitwise-or [c]
    XOR
    a(fp), b(fp), c(fp)
    OPXOR, a, b, c, 0, _
    Set [a] to [b] bitwise-or [c]
  • Instruction decoding

    Trace cells are also allocated for each selector. In each cycle, the opcode is decoded into the following selector flags, which are grouped by type (not configuration) below for convenience. All flags are binary values, except for the instruction code.

    Instruction code
    Selector
    Configuration
    Description
    sinsrt
    Core
    Instruction code
    Operand modifiers
    Selector
    Configuration
    Description
    simm a
    Core
    Immediate value flag for operand a (0 denotes offset, 1 denotes immediate value)
    simm b
    Core
    Immediate value flag for operand b (0 denotes offset, 1 denotes immediate value)
    Field arithmetic
    Selector
    Configuration
    Description
    sadd
    Field Arithmetic
    Addition
    smul
    Field Arithmetic
    Multiplication
    Memory
    Selector
    Configuration
    Description
    sstore32
    Core
    Store 4 bytes at at a memory address
    sload32
    Core
    Load 4 bytes from a memory address
    sstore8
    Core
    Store a byte at a memory address
    sload8
    Core
    Load a byte from a memory address
    Call
    Selector
    Configuration
    Description
    scall
    Core
    Update the frame pointer
    Jumps
    Selector
    Configuration
    Description
    sjump_eq
    Core
    Jump equal flag
    sjump_neq
    Additional Jumps
    Jump not equal flag
    sjump_ge
    Additional Jumps
    Jump greater than or equal to flag
    sjump_lt
    Additional Jumps
    Jump less than flag
    U32 arithmetic
    Selector
    Configuration
    s32_add
    U32 Arithmetic
    s32_sub
    U32 Arithmetic
    s32_mul
    U32 Arithmetic
    s32_div
    U32 Arithmetic
    s32_checked_add
    U32 Arithmetic
    s32_checked_mull
    U32 Arithmetic
    Binary operations
    Selector
    Configuration
    sbitwise_and
    Bitwise
    sbitwise_or
    Bitwise
    sbitwise_xor
    Bitwise
  • Design notes

    Frontend target

    We are writing a compiler from LLVM IR to our ISA.

    ZK stack

    This is a STARK-based zkVM. We are using Plonky3 to implement the polynomial IOP and PCS.

    Field choice

    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.

    Registers

    Our VM has no general purpose registers, since memory is cheap.

    Memory

    We will use a conventional R/W memory.

    Tables

    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).

    Continuations

    TODO: Explain the permutation-based continuation implementation.

    Lookups

    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.

    Floating point arithmetic

    Fast floating point arithmetic doesn’t seem important for our anticipated use cases, so we will convert float operations to integer ones during compilation.

  • Call for collaboration

    This open-source project aims to construct a robust, versatile system optimized for wide-ranging use cases, performance, and development productivity. We seek varied perspectives, innovative ideas, and unwavering dedication to quality.

    Your contribution can take many forms, each equally valuable. Here are a few ways you can get involved:

    • Code Contribution: You can directly contribute to the source code. This could range from fixing bugs and improving documentation, to developing new features.

    • Adding Coprocessors: Valida zkVM is designed to be flexible and extensible. If you have an idea for a new coprocessor that could enhance zkVM’s functionality, we encourage you to design and implement it.

    • Contributing to Plonky3: As part of our ongoing efforts, we are working on the Plonky3 backend. Whether you have experience in this area or are interested in learning more, your contribution can significantly help us expedite our progress.

    • Code Review and Bug Reporting: Reviewing our codebase and reporting any issues you find is a great way to contribute to the project.

    • Documentation: Comprehensive and clear documentation is the backbone of any successful open-source project. If you have a knack for writing or explaining complex concepts in a simple way, your skills would be greatly valued.

    We are looking forward to your contributions and are ready to provide guidance and support to anyone who wants to get involved. Please visit our GitHub page to get started and feel free to reach out with any questions or ideas.