Skip to content

RISC-V ELF Transpilation to OpenVM Executable

The OpenVM framework supports transpilation of a RISC-V ELF consisting of the RV32IM instruction set as well as custom RISC-V instructions specified by VM extensions into an OpenVM executable.

Transpiler Framework

The transpiler is a function that converts a RISC-V ELF into an OpenVM executable, where an OpenVM executable is defined as the following pieces of data:

  • Program ROM
  • Starting program counter pc_0
  • Initial data memory

The OpenVM executable forms a part of the initial VM state.

We define a RISC-V machine code block to be a 32-bit aligned contiguous sequence of bits in the RISC-V program memory, where the bit length is variable and a multiple of 32. The code block may contain instructions from standard or non-standard RISC-V ISA extensions, but it may also contain arbitrary bits.

The transpiler is configured upon construction with the set of VM extensions to support. In order to be supported by the transpiler, a VM extension must specify a set of RISC-V machine code blocks and rules for mapping each code block to a sequence of potentially multiple OpenVM instructions. Extensions may also modify the executable's initial memory image.

The transpilation rules must satisfy:

  • A read or write to the RISC-V program counter corresponds to a read or write to the program counter of the same value in OpenVM. This includes the implicit read of the program counter to fetch the instruction from program code, as well as any implicit pc += 4 advancement in some RISC-V instructions.
  • A RISC-V 32-bit register x{i} read or write access corresponds to an OpenVM memory access at [4 * i: 4]_1 except for writes to x0, see below. The 32-bits of x{i} are represented as 4 little-endian bytes in OpenVM memory.
    • A RISC-V code block must never map to any OpenVM instruction that changes the value of [0:4]_1 in OpenVM memory.
  • A RISC-V 32-bit user memory access of the jth byte in word i corresponds to an OpenVM memory access at [4 * i + j]_2.
  • If the RISC-V code block is a standard instruction from the RISC-V Instruction Set Manual Volume I: Unprivileged ISA (pdf), then the transpilation rule must map the RISC-V instruction to an OpenVM instruction that follows the RISC-V specification after applying the above correspondences to register and memory accesses.

The above requirements, together with the invariants of the OpenVM ISA, imply that transpilation will only be valid for programs where:

  • The program code does not have program address greater than or equal to 2^PC_BITS.
  • The program does not access memory outside the range [0, 2^pointer_max_bits): programs that attempt such accesses will fail to execute.

A transpiler configuration is only considered valid if there are no two transpilation rules that may map the same RISC-V code block to different OpenVM instructions.

  • When defining a new VM extension with transpiler support, the associated RISC-V code blocks should be chosen to avoid conflicts with RISC-V code blocks from other pre-existing VM extensions that the new VM extension expects to be compatible with.

Register x0 Handling

As specified in Section 2.1 of RISC-V Instruction Set Manual Volume I: Unprivileged ISA (pdf), register x0 is hardwired to zero and must never be written to.

The OpenVM ISA treats [0:4]_1 as normal read/write memory and makes no guarantees on memory accesses to this location. The transpiler must never transpile a RISC-V code block to any OpenVM instruction that changes the value of [0:4]_1 in OpenVM memory. For compatibility with the RISC-V ISA, the transpiler must always transpile a RISC-V instruction to an OpenVM instruction that matches the RISC-V specification. In particular, any RISC-V instruction that has rd=x0 must be transpiled to either the NOP OpenVM instruction if it has no side effects or to an OpenVM instruction that executes the expected side effect and does not change the value of [0:4]_1.

Transpiler Specification for Default VM Extensions

This section specifies the behavior of the transpiler for the default VM extensions with the custom RISC-V instructions specified here. We use the following notation:

  • Let ind(rd) denote 4 * (register index), which is in 0..128. In particular, it fits in one field element.
  • We use itof for the function that sends an immediate to its signed integer representation, as specified in Section 2.3 of RISC-V Instruction Set Manual Volume I: Unprivileged ISA (signed 12-bit, 13-bit, 21-bits for I-type, B-type, J-type, respectively), and then maps it to the corresponding field element. So 0b11…11 goes to -1 in F.
  • We use sign_extend_24 to convert a 12-bit integer into a 24-bit integer via sign extension. We use this in conjunction with utof, which converts 24 bits into an unsigned integer and then maps it to the corresponding field element. Note that each 24-bit unsigned integer fits in one field element.
  • We use sign_extend_16 for the analogous conversion into a 16-bit integer via sign extension.
  • We use zero_extend_24 to convert an unsigned integer with at most 24 bits into a 24-bit unsigned integer by zero extension. This is used in conjunction with utof to convert unsigned integers to field elements.
  • We use sign_of(imm) to get the sign bit of the immediate imm.
  • The notation imm[0:4] means the lowest 5 bits of the immediate.
  • For a phantom instruction ins, disc(ins) is the discriminant specified in the ISA specification.
  • For a phantom instruction ins and a 16-bit c_upper, phantom_c(c_upper, ins) = c_upper << 16 | disc(ins) is the corresponding 32-bit operand c for PHANTOM.

We now specify the transpilation for system instructions and the default set of VM extensions.

System Instructions

RISC-V InstOpenVM Instruction
terminateTERMINATE _, _, utof(imm) where imm must fit in u8

RV32IM Extension

Transpilation from RV32IM to OpenVM assembly follows the mapping below, which is generally a 1-1 translation between RV32IM instructions and OpenVM instructions. The main exception relates to handling of the x0 register, which discards writes and has value 0 in all reads. We handle writes to x0 in transpilation as follows:

  • Instructions that write to x0 with no side effects are transpiled to the PHANTOM instruction with c = 0x00 (Nop).
  • Instructions that write to a register which might be x0 with side effects (JAL, JALR) are transpiled to the corresponding custom instruction whose write behavior is controlled by a flag specifying whether the target register is x0.

Because [0:4]_1 is initialized to 0 and never written to, this guarantees that reads from x0 yield 0 and enforces that any OpenVM program transpiled from RV32IM conforms to the RV32IM specification for x0.

System Level Extensions to RV32IM

RISC-V InstOpenVM Instruction
hintstorewHINT_STOREW_RV32 0, ind(rd), _, 1, 2
hintbufferHINT_BUFFER_RV32 ind(rs1), ind(rd), _, 1, 2
revealSTOREW_RV32 ind(rs1), ind(rd), utof(sign_extend_16(imm)), 1, 3, 1, sign_of(imm)
hintinputPHANTOM _, _, disc(Rv32HintInput)
printstrPHANTOM ind(rd), ind(rs1), disc(Rv32PrintStr)
hintrandomPHANTOM ind(rd), _, disc(Rv32HintRandom)

Standard RV32IM Instructions

RISC-V InstOpenVM Instruction
addADD_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
subSUB_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
xorXOR_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
orOR_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
andAND_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
sllSLL_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
srlSRL_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
sraSRA_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
sltSLT_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
sltuSLTU_RV32 ind(rd), ind(rs1), ind(rs2), 1, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
addiADD_RV32 ind(rd), ind(rs1), utof(sign_extend_24(imm)), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
xoriXOR_RV32 ind(rd), ind(rs1), utof(sign_extend_24(imm)), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
oriOR_RV32 ind(rd), ind(rs1), utof(sign_extend_24(imm)), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
andiAND_RV32 ind(rd), ind(rs1), utof(sign_extend_24(imm)), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
slliSLL_RV32 ind(rd), ind(rs1), utof(zero_extend_24(imm[0:4])), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
srliSRL_RV32 ind(rd), ind(rs1), utof(zero_extend_24(imm[0:4])), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
sraiSRA_RV32 ind(rd), ind(rs1), utof(zero_extend_24(imm[0:4])), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
sltiSLT_RV32 ind(rd), ind(rs1), utof(sign_extend_24(imm)), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
sltiuSLTU_RV32 ind(rd), ind(rs1), utof(sign_extend_24(imm)), 1, 0 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
lbLOADB_RV32 ind(rd), ind(rs1), utof(sign_extend_16(imm)), 1, 2, (rd != x0), sign_of(imm)
lhLOADH_RV32 ind(rd), ind(rs1), utof(sign_extend_16(imm)), 1, 2, (rd != x0), sign_of(imm)
lwLOADW_RV32 ind(rd), ind(rs1), utof(sign_extend_16(imm)), 1, 2, (rd != x0), sign_of(imm)
lbuLOADBU_RV32 ind(rd), ind(rs1), utof(sign_extend_16(imm)), 1, 2, (rd != x0), sign_of(imm)
lhuLOADHU_RV32 ind(rd), ind(rs1), utof(sign_extend_16(imm)), 1, 2, (rd != x0), sign_of(imm)
sbSTOREB_RV32 ind(rs2), ind(rs1), utof(sign_extend_16(imm)), 1, 2, 1, sign_of(imm)
shSTOREH_RV32 ind(rs2), ind(rs1), utof(sign_extend_16(imm)), 1, 2, 1, sign_of(imm)
swSTOREW_RV32 ind(rs2), ind(rs1), utof(sign_extend_16(imm)), 1, 2, 1, sign_of(imm)
beqBEQ_RV32 ind(rs1), ind(rs2), itof(imm), 1, 1
bneBNE_RV32 ind(rs1), ind(rs2), itof(imm), 1, 1
bltBLT_RV32 ind(rs1), ind(rs2), itof(imm), 1, 1
bgeBGE_RV32 ind(rs1), ind(rs2), itof(imm), 1, 1
bltuBLTU_RV32 ind(rs1), ind(rs2), itof(imm), 1, 1
bgeuBGEU_RV32 ind(rs1), ind(rs2), itof(imm), 1, 1
jalJAL_RV32 ind(rd), 0, itof(imm), 1, 0, (rd != x0)
jalrJALR_RV32 ind(rd), ind(rs1), utof(sign_extend_16(imm)), 1, 0, (rd != x0), sign_of(imm)
luiLUI_RV32 ind(rd), 0, utof(zero_extend_24(imm[12:31])), 1, 0, 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
auipcAUIPC_RV32 ind(rd), 0, utof(zero_extend_24(imm[12:31]) << 4), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
mulMUL_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
mulhMULH_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
mulhsuMULHSU_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
mulhuMULHU_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
divDIV_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
divuDIVU_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
remREM_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
remuREMU_RV32 ind(rd), ind(rs1), ind(rs2), 1 if rd != x0, otherwise PHANTOM _, _, disc(Nop)

OpenVM Intrinsic VM Extensions

The following sections specify the transpilation of the default set of intrinsic extensions to OpenVM. In order to preserve correctness of handling of x0, the transpilation must respect the constraint that any instruction that writes to a register must:

  • Transpile to Nop if the register is x0 and there are no side effects.
  • Transpile to an OpenVM assembly instruction that does not write to [0:4]_1 and processes side effects if the register is x0 and there are side effects.

Each VM extension's behavior is specified below.

Keccak Extension

RISC-V InstOpenVM Instruction
xorinXORIN_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
keccakfKECCAKF_RV32 ind(rd), _, _, 1, 2

SHA-2 Extension

RISC-V InstOpenVM Instruction
sha256_updateSHA256_UPDATE_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
sha512_updateSHA512_UPDATE_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2

Deferral Extension

The 12-bit immediate encodes both the sub-opcode and deferral index: imm[1:0] selects CALL_RV32 (0x0) vs OUTPUT_RV32 (0x1), and def_idx = imm[11:2] identifies which deferral function to invoke. In addition to transpiling instructions, the deferral transpiler extension writes the initial input accumulator values (the def_circuit_commit for each configured deferral circuit) into the deferral address space (address space 4) via modify_initial_memory.

RISC-V InstOpenVM Instruction
def_callCALL_RV32 ind(rd), ind(rs1), def_idx, 1, 2
def_outputOUTPUT_RV32 ind(rd), ind(rs1), def_idx, 1, 2

BigInt Extension

RISC-V InstOpenVM Instruction
add256ADD256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
sub256SUB256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
xor256XOR256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
or256OR256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
and256AND256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
sll256SLL256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
srl256SRL256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
sra256SRA256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
slt256SLT256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
sltu256SLTU256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
mul256MUL256_RV32 ind(rd), ind(rs1), ind(rs2), 1, 2
beq256BEQ256_RV32 ind(rs1), ind(rs2), itof(imm), 1, 2

Algebra Extension

Modular Arithmetic

RISC-V InstOpenVM Instruction
addmod<N>ADDMOD_RV32<N> ind(rd), ind(rs1), ind(rs2), 1, 2
submod<N>SUBMOD_RV32<N> ind(rd), ind(rs1), ind(rs2), 1, 2
mulmod<N>MULMOD_RV32<N> ind(rd), ind(rs1), ind(rs2), 1, 2
divmod<N>DIVMOD_RV32<N> ind(rd), ind(rs1), ind(rs2), 1, 2
iseqmod<N>ISEQMOD_RV32<N> ind(rd), ind(rs1), ind(rs2), 1, 2 if rd != x0, otherwise PHANTOM _, _, disc(Nop)
setup<N>SETUP_ADDSUBMOD_RV32<N> ind(rd), ind(rs1), 0, 1, 2 if rs2 = x0, SETUP_MULDIVMOD_RV32<N> ind(rd), ind(rs1), 0, 1, 2 if rs2 = x1, SETUP_ISEQMOD_RV32<N> ind(rd), ind(rs1), 0, 1, 2 if rs2 = x2
hint_non_qrPHANTOM 0, 0, phantom_c(mod_idx, HintNonQr)
hint_sqrtPHANTOM ind(rs1), 0, phantom_c(mod_idx, HintSqrt)

Complex Extension Field Arithmetic

RISC-V InstOpenVM Instruction
addcomplexADD_RV32<Fp2> ind(rd), ind(rs1), ind(rs2), 1, 2
subcomplexSUB_RV32<Fp2> ind(rd), ind(rs1), ind(rs2), 1, 2
mulcomplexMUL_RV32<Fp2> ind(rd), ind(rs1), ind(rs2), 1, 2
divcomplexDIV_RV32<Fp2> ind(rd), ind(rs1), ind(rs2), 1, 2
setupcomplexSETUP_ADDSUB_RV32<Fp2> ind(rd), ind(rs1), 0, 1, 2 if rs2 = x0, SETUP_MULDIV_RV32<Fp2> ind(rd), ind(rs1), 0, 1, 2 if rs2 = x1

Elliptic Curve Extension

RISC-V InstOpenVM Instruction
sw_add_ne<C>EC_ADD_NE_RV32<C> ind(rd), ind(rs1), ind(rs2), 1, 2
sw_double<C>EC_DOUBLE_RV32<C> ind(rd), ind(rs1), 0, 1, 2
setup<C>SETUP_EC_ADD_NE_RV32<C> ind(rd), ind(rs1), ind(rs2), 1, 2 if ind(rs2) != 0, SETUP_EC_DOUBLE_RV32<C> ind(rd), ind(rs1), ind(rs2), 1, 2 if ind(rs2) = 0

Pairing Extension

RISC-V InstOpenVM Instruction
hint_final_expPHANTOM ind(rs1), ind(rs2), phantom_c(pairing_idx, HintFinalExp)