Scope. This is an examination of the RISC-V base integer and standard-extension instructions as objects of proof: what each opcode is defined to do, what it costs to constrain that behavior inside a succinct argument, and how four production-lineage zkVMs — Jolt, Ceno, OpenVM, and ZKsync's Airbender — actually realize each family. Section 6 is the reference deliverable: an opcode-by-opcode encyclopedia of the I, M, A, C, Zicsr, and Zifencei instructions with their spec-mandated edge cases and zkVM proving considerations.
Sourcing. Architectural semantics follow the ratified RISC-V Instruction Set Manual, Volume I: Unprivileged ISA; per-project claims are pinned to specific public commits with full SHAs and line numbers in the references. Codebases move: statements describe the pinned revisions as of July 27, 2026, not any project's future. This piece is a companion to our survey of guest-program compilation and ISA targeting, which covers how programs reach these instruction sets; here we treat what happens to the instructions once a prover must answer for them.
Every zkVM in production today asks to be judged as a processor: it names an ISA, promises architectural compliance, and invites developers to compile ordinary programs against it. That framing invites a particular reviewer — the person who has actually built pipelines, written ISA compliance suites, or argued about a bit in an encoding — and most writing about zkVMs is not addressed to them. This piece is. The claim we defend over the next several thousand words is that a zkVM is best understood as a machine implementation under a radically inverted cost model: the ISA semantics are unchanged to the bit, but nearly every intuition about what is cheap and what is expensive — carry chains, barrel shifters, comparators, dividers, register ports — is either neutralized or reversed. The instruction set survives; the microarchitectural folklore does not.
1 · The object being proven: a three-piece machine and its transition relation
Strip the machine to what the architecture actually specifies. A RISC-V hart, for the purposes of the unprivileged integer ISA, is three pieces of state:
hart state
+-------------------------------------------+ memory (one byte-addressed array)
| x0..x31 : 32 registers, XLEN bits each | +--------------------------+
| x0 = 0 (hardwired: writes discard, | | addr 0x0000: 0x00 |
| reads return zero) | load | ... |
| x1..x31 = general purpose | <----- | addr 0x2008: 0xCD |
| | store | addr 0x2009: 0xAB |
| pc : XLEN bits, address of the next | -----> | ... |
| instruction to execute | | addr 0x8000_0000: text |
+-------------------------------------------+ +--------------------------+
Everything else — pipelines, caches, register renaming, branch predictors — is implementation. The architecture is the transition relation: given a state s = (regs, pc, mem), fetch the instruction at mem[pc], decode it, and produce the unique successor state s' = δ(s). Execution is the orbit of δ. This is the standard operational-semantics view of an ISA, and it is worth stating because a zkVM takes it literally in a way silicon never has to: the zkVM's statement is precisely "here is a commitment to a trace s₀, s₁, …, s_T, and for every step, s_{i+1} = δ(s_i)," with the initial state bound to a committed program and its inputs and the final state bound to the claimed outputs. A hardware implementation must produce the orbit; a prover must certify it after the fact, to a verifier who will not re-execute.
A scoping note for the formally minded: δ-as-function is a deliberate simplification — adequate for this article, too strong for the machine as deployed. The moment the guest consumes a host-supplied word (an advice tape, the ECALL doorways of §4.10, Airbender's oracle CSR), the step is no longer a function of machine state alone. A more complete formalism replaces the function with a relation R(si, ei, si+1), where ei ranges over the environment's contribution at step i — nondeterministic input, host behavior crossed into by an environment call — and the proof statement then quantifies over an e that is committed, constrained, or deliberately left free. Everything below survives that refinement; we keep δ for the families where the environment stays silent.
That single sentence decomposes into the five obligations every zkVM discharges, whatever its proof system:
| Obligation | Informal statement | Where it typically lives |
|---|---|---|
| Fetch | The instruction claimed at step i is the one the committed program stores at pc_i | A read-argument against a committed program/bytecode table |
| Decode | The operand fields, immediate, and control signals claimed for that instruction are what its encoding actually says | Preprocessing (decode once per static instruction) or per-row constraints |
| Execute | The claimed result is the ISA-defined function of the claimed operands | Per-opcode constraints, lookup tables, or delegated circuits — the subject of §4 |
| State consistency | Every register/memory read returns the most recent write to that location | A memory-consistency argument (multiset / offline memory checking) |
| Control flow | pc_{i+1} is the ISA-defined successor: pc_i + 4 (or +2), or the taken branch/jump target | Next-pc constraints tied to the execute obligation |
Two architectural points that recur throughout, stated here once. First, x0 is not a register with a zero in it; it is the absence of a register — a destination that discards and a source that manufactures zero. Silicon implements this with a read-port bypass; provers, as we will see, implement it with a constraint or by aliasing address 0 of a register-memory to a cell whose writes are discarded. Second, the base ISA has exactly one addressing mode — base register plus sign-extended 12-bit displacement, used by every load and store (the A extension's atomics are sparer still: register-direct through rs1, with no displacement at all) — and no instruction computes on memory directly. An address is an ordinary number that happens to sit in a register. This load-store discipline, a deliberate RISC inheritance, turns out to be a substantial gift to the prover: memory interaction is quarantined into two instruction families, so the expensive machinery (the consistency argument) attaches to a small, syntactically obvious subset of the trace.
One more preliminary, because it governs everything in §4: the width of the machine. RV32 gives XLEN=32, RV64 gives XLEN=64. All register arithmetic is modular — ADD is addition mod 2XLEN, full stop. There are no flags: no carry, no overflow, no condition-code register anywhere in the integer architecture. Software that wants a carry reconstructs it from comparisons (sltu after an add), and the absence of flag state is one of the tidiest things about proving this ISA — a step's effect is entirely "one register changed, pc changed," never "and three sticky bits elsewhere also changed."
2 · Instruction anatomy: six formats and where the bits live
All standard RV32I/RV64I instructions are 32 bits, with the lowest two bits 11; the compressed extension occupies the other three values of those two bits with 16-bit encodings (§4.11). Six formats cover the base ISA, distinguished only by how they use the constant field:
31 25 24 20 19 15 14 12 11 7 6 0
+--------------+--------+--------+------+-------------+--------+
| funct7 | rs2 | rs1 |funct3| rd | opcode | R : register-register ALU
+--------------+--------+--------+------+-------------+--------+
| imm[11:0] | rs1 |funct3| rd | opcode | I : ALU-immediate, loads, JALR
+--------------+--------+--------+------+-------------+--------+
| imm[11:5] | rs2 | rs1 |funct3| imm[4:0] | opcode | S : stores
+--------------+--------+--------+------+-------------+--------+
|imm[12|10:5] | rs2 | rs1 |funct3|imm[4:1|11] | opcode | B : branches (offset ×2)
+--------------+--------+--------+------+-------------+--------+
| imm[31:12] | rd | opcode | U : LUI, AUIPC
+--------------+--------+--------+------+-------------+--------+
| imm[20|10:1|11|19:12] | rd | opcode | J : JAL (offset ×2)
+--------------+--------+--------+------+-------------+--------+
Three regularities matter more than the formats themselves. The source registers rs1/rs2 and destination rd never move: whichever format an instruction uses, the same five-bit fields are in the same place, which is why a hardware decoder can start its register-file read before it knows what the instruction is. The immediate is always sign-extended (the sole exceptions in the whole unprivileged integer ISA: the shift amount — five bits on RV32, six on RV64 — and the five-bit CSR immediate, both unsigned by nature), and the sign bit is always instruction bit 31, so sign extension needs no decoding at all. And the B and J formats scatter their immediate bits into an order no human would choose precisely to preserve those two invariants — the scrambling is free in wires and saves a mux on the critical path.
For a prover, this hardware-shaped regularity is pleasant but the deeper observation is different: a zkVM can often refuse to pay for decode at execution time at all. The program is static and committed before proving; every distinct instruction can be decoded once, in preprocessing, into whatever internal tuple the proof system prefers — opcode kind, operand indices, expanded immediate, control flags — and the fetch obligation of §1 then reads from this predecoded table. The per-cycle question "what do these 32 bits mean" becomes the one-time question "is this table an honest decoding of the committed program," which can even be delegated to the verifier's preprocessing or a one-off argument. Chip-per-opcode designs make the same move differently, dispatching each fetched instruction to the circuit for its kind and letting a bus argument glue the pieces. Either way, one of silicon's per-cycle taxes — decode — largely vanishes from the marginal cost of a proven cycle. We will see in §4 which taxes replace it.
A reading note for the code fragments that follow: assembly syntax puts the destination first — add x5, x6, x7 is x5 ← x6 + x7; a trailing i marks an immediate variant; u marks unsigned interpretation; a trailing w on RV64 marks a 32-bit "word" operation. In sll x5, x6, x7 all three operands are registers; nothing in it is an address.
3 · The cost-model inversion
An ISA is a contract, and every contract is negotiated against an implementation technology. RISC-V's integer ISA was shaped by what CMOS charges for: it has no divide-step instruction because iterative dividers are microarchitecture, no flags because renaming flag state complicates wide issue, one addressing mode because address-generation adders are on the critical path. When the implementation technology becomes "polynomial commitments and a constraint system over a prime field," the price list is rewritten wholesale. The contract's text does not change; its economics do.
The inversion has a single root cause worth naming precisely. A circuit verifies; it does not compute. Silicon must produce the result of an operation from its inputs, so the cost of an instruction is the cost of the producing structure — the carry-lookahead tree, the barrel shifter, the iterative divider. A constraint system only needs the result to appear, from anywhere (the prover simply writes it into the witness), and then be checked. Where checking is cheaper than producing, the prover pockets the difference; this is nondeterminism as an engineering resource, and it is not available to hardware at any price. Division is the canonical beneficiary — §4.9 — but the resource is spent on nearly every family. The converse also bites: some things silicon gets for free by physically being made of bits (bitwise logic, truncation, wiring a bus in a permuted order) have no native meaning over a prime field, where the atoms are field elements, and must be bought back with decomposition and range proofs.
The recurring price list, before we meet the families that pay it:
| Operation | What silicon charges | What a prover charges |
|---|---|---|
| Bitwise AND/OR/XOR | Effectively nothing — one gate per bit, no carries, minimal delay | No field-native meaning. Either decompose both operands to bits (XLEN boolean constraints each) or split into small limbs and look each limb pair up in a table — the option every VM here takes in some form |
| Wide add/sub | Carry propagation — the classic timing wall; lookahead/prefix trees exist to beat it | One field addition plus range checks: the sum is witnessed, and the claim "result wraps mod 2XLEN" is enforced by checking limbs are in range and a carry bit is boolean. Carry time is free; carry truth costs range proofs |
| Multiply | Partial-product arrays / DSP blocks; area-hungry, often multi-cycle | Nearly free if the product fits the field (a 254-bit field multiplies 64-bit words natively); schoolbook limb products plus carries if it does not (31-bit fields vs 32-bit words). Field choice, not gate count, sets this price |
| Divide / remainder | Iterative — tens of cycles, or a large SRT unit; the classic "why is DIV slow" answer | Cheaper than multiply-adjacent: nondeterministically supply quotient and remainder, then check one multiply-add and two inequalities. No divider is represented in the constraint system — the prover's witness generator still computes q and r by ordinary division; only the circuit is spared. The soundness burden moves into the inequality checks (§4.9) |
| Variable shift | A barrel shifter — log-depth mux tree, cheap and single-cycle | Genuinely awkward: a data-dependent bit rotation has no algebraic form. Standard answers: materialize 2shamt (by lookup or advice) and multiply, then split the double-width result; or decompose to bits and mux. Right shifts add a masking/sign-fill obligation |
| Compare (<, signed/unsigned) | A subtractor and one flag bit | A borrow/difference witness plus a range proof that the difference fits XLEN bits, or a direct lookup; signedness is an interpretation applied via the sign bits or a +2XLEN−1 bias, never a different datapath |
| Register file | Ports — the superscalar bottleneck; more read ports means quadratic area | Registers are just a 32-cell memory inside the same consistency argument as RAM (§5). "Ports" do not exist; three register accesses per step cost three tuples in a multiset. Every VM in this survey makes this move |
| Random-access memory | SRAM/DRAM hierarchy; latency hidden by caches | An offline memory checking argument: all accesses, gathered across the whole trace, must form a consistent read-write history — permutation/multiset machinery whose cost scales with access count, not address range. Locality is worthless; consistency is everything |
| Control flow / branches | Pipeline bubbles, predictors, misprediction flushes | No speculation to lose: the trace already knows the outcome. The cost is constraining that the outcome is right — a comparison plus a two-way next-pc constraint. A taken branch costs the same as a fall-through |
| Decode | Per-cycle logic; trivial for RISC-V by design | Amortizable to zero at trace time against a committed, predecoded program (§2) |
Two systemic effects sit on top of the per-operation prices. First, the field is a second word size. Every zkVM carries two widths: the architectural XLEN and the field's capacity. When the field is large (Jolt's ~254-bit scalar field), a 64-bit word is a single field element and double-width products are native, but every bit-level claim needs decomposition arguments sized for 64 bits. When the field is small (the 31-bit fields of OpenVM and Airbender — Baby Bear and Mersenne31, respectively), a 32-bit word does not even fit one field element, so words live as limbs permanently — two 16-bit or four 8-bit pieces — and every operation is defined over limbs with explicit carries; in exchange, field arithmetic itself is fast and range checks are small. Neither regime dominates; they relocate the same costs. Second, uniformity has a price and so does its absence. A single universal row that can be any opcode (Jolt's stance) pays for generality with lookups into large structured tables; a dedicated circuit per opcode (Ceno's stance) pays nothing for generality but must glue heterogeneous chips into one trace with bus/permutation arguments and prove a chip even when it ran twice. This tension — one machine that does everything versus a federation of specialists — is the closest thing zkVM design has to the CISC/RISC argument, and §5 returns to it with the four concrete answers.
With the price list in hand, the families. For each: the architected behavior with its edge cases exactly as the manual fixes them, then the arithmetization obligations those semantics create, then what the four VMs at our pinned commits actually do.
4 · Family by family: semantics, then constraints
A note on vocabulary for readers arriving from the hardware side. A constraint is a polynomial equation over the field that every row of the committed trace must satisfy — the prover's analog of an assertion that is physically impossible to violate. A witness is a value the prover writes into the trace at its own discretion, trusted only insofar as constraints pin it down; "advice" is a witness with no defining equation, only checks. A lookup argument proves that each of millions of trace values appears in some table — where the table can be small and materialized (a 216 byte-pair table) or astronomically large and never materialized, defined only by the low-degree polynomial that describes its entries (Jolt's 2128-entry instruction tables, evaluated via sumcheck rather than stored). A range check is the workhorse lookup: "this value fits in N bits." With that, the families.
4.1 · Addition and subtraction: buying back the wrap
Architected behavior. ADD rd,rs1,rs2 / ADDI rd,rs1,imm / SUB rd,rs1,rs2 compute mod 2XLEN; overflow is silently discarded, no exception exists, and there is no SUBI (add a negative immediate). On RV64 the *W variants compute on the low 32 bits and sign-extend the 32-bit result — the RV64 convention that all 32-bit values, even unsigned ones, live sign-extended in 64-bit registers [1].
The obligation. Field addition is exact — 264−1 plus 1 is 264 in the field, not 0 — so the entire proving content of ADD is the truncation. Every zkVM proves ADD as "add somewhere it cannot wrap, then prove the claimed result is the low XLEN bits," and the differences are only in where the carry lives:
- Jolt adds the two 64-bit operands natively in its ~254-bit field inside R1CS, then looks the 65-bit sum up in a
RangeChecktable whose entries are "identity on the low 64 bits" — truncation as a lookup [9]. SUB is the same machinery with a two's-complement bias: the lookup operand is formed as left − right + 264, and the same table drops the borrow [9]. - OpenVM, whose 31-bit Baby Bear field cannot hold even one 32-bit word, keeps words as four 8-bit limbs and witnesses a boolean carry per limb: carryi = (bi + ci + carryi−1 − ai)/28, constrained to {0,1}, with result limbs byte-range-checked through the bitwise lookup bus [42].
- Ceno does the same over two 16-bit limbs, and proves SUB by re-arranging the addition — constrain rs1 = rd + rs2, so the subtraction circuit is the addition circuit with the roles rotated and only rd freshly range-checked [25].
- Airbender shares one
AddSubRelationacross both opcodes: two degree-2 constraints over 16-bit limbs with a boolean middle carry and carry-out [55]. Its relation slots are pooled: because at most one instruction family executes per row, the same physical add-sub relation is reused by whichever family is live, selected by mutually exclusive execution flags [55].
The hardware translation: the carry chain's timing problem is gone, but its truth must now be purchased, either as one lookup against a truncation table (large field) or as N boolean-carry witnesses plus N range checks (small field). Note what this makes of ADDI: identical datapath, and the immediate costs nothing at all, because it is a constant of the committed program — decoded once, materialized in the fetched instruction tuple, never witnessed per cycle (§2).
4.2 · Bitwise logic: the field's blind spot
Architected behavior. AND/OR/XOR and immediate forms, bit-parallel, no edge cases beyond the immediate being sign-extended (so andi rd,rs,-1 is identity and xori rd,rs,-1 is bitwise NOT) [1].
The obligation. This is the purest case of the inversion: the operation silicon performs with one gate per bit has no algebraic expression over a prime field of any size. All four VMs answer with lookups, differing in granularity:
- Jolt gives each of AND/OR/XOR (and ANDN) a dedicated virtual table over the bit-interleaved pair of 64-bit operands — 2128 conceptual entries, never materialized, evaluated through the structure of the table's multilinear extension [8][10].
- Ceno splits words into four bytes and constrains each output byte against materialized 216-row And/Or/Xor tables keyed by the input byte pair [26].
- OpenVM ships only a XOR table and derives the rest by identity: for OR it checks that x⊕y equals 2a−b−c, for AND that it equals b+c−2a (both consequences of a = op(b,c) bytewise); the same table's second multiplicity column doubles as the byte range check for arithmetic outputs — one preprocessed 28×28 table serving three opcodes and the range-check bus [42].
- Airbender makes one byte-table query per operand byte and indexes the table family directly by the instruction's funct3 — its AND/OR/XOR table ids are literally 7, 6, 4, the RISC-V funct3 encodings, so decode and dispatch collapse into the lookup key [56]. The byte tables also make separate range checks unnecessary: a value that answers a byte-table query is a byte [56].
Cost intuition for the hardware reader: bitwise ops are no longer free, but they are not the disaster naive bit-decomposition (XLEN boolean constraints per operand per op) suggests — the lookup amortizes them to a handful of bus interactions per instruction. The residual asymmetry is real, though, and it is why every zkVM pushes hash functions — thousands of XOR/rotate steps — out of the ISA into precompiles, delegation circuits, or constraint-native inlines rather than proving them opcode by opcode.
4.3 · Shifts: the barrel shifter has no algebra
Architected behavior. SLL/SRL/SRA shift by the low 5 bits (RV32) or low 6 bits (RV64) of rs2 — upper bits ignored, so there is no "shift by ≥ width" undefined behavior at the ISA level, ever. Immediate forms carry the amount in a shamt field, with reserved code points policing the width: on RV32, SLLI/SRLI/SRAI with inst[25]=1 is reserved encoding space, and on RV64 the *W immediate shifts with imm[5]≠0 are reserved [1]. SRL zero-fills; SRA replicates the sign bit; the W variants shift within the low 32 bits (amount from rs2[4:0] only) and sign-extend the word result.
The obligation. A data-dependent shift is a permutation of bits selected by a runtime value — a barrel shifter is a log-depth mux tree, and mux trees are exactly what constraint systems do badly. Three distinct strategies appear across our four VMs, which is the widest divergence of any family:
- Materialize 2shamt and multiply (Jolt).
SLLexpands at trace time intoVirtualPow2— a lookup whose table is 2(index mod XLEN), the mod performing the architectural masking — followed by an ordinary MUL [11]. Right shifts go through a bitmask table (((1<<(XLEN−s))−1)<<s) and dedicated VirtualSRL/VirtualSRA lookups, SRA's table adding a sign-extension term gated by the operand's top bit — arithmetic shift as table structure, not as a mux [11]. Immediate shifts collapse further: SLLI becomes a single multiply-by-constant row, the constant 1<<(imm&63) baked in at expansion [11]. The word shifts are small puzzles in 64-bit space — SRLW first multiplies by 232 to move the word into the high half, ORs 32 into the shift amount, then reuses the 64-bit right-shift path and sign-extends [11]. - One-hot select over limbs (OpenVM, and Ceno's default circuit, which its comments attribute to OpenVM's design). Boolean marker vectors — one over the bit-shift within a limb, one over the limb displacement — each constrained to sum to 1; a bit_multiplier witness is forced to equal 2bit_shift under the marked position; limb equations then relate input and output limbs with per-limb carry witnesses, sign-filling the vacated limbs with sign·0xFF(FF) for SRA [43][27]. The architectural masking becomes a range check that (c0 − limb_shift·L − bit_shift) is divisible by 32 — i.e. only c mod 32 matters [43].
- Precomputed shift table (Airbender). A 222-entry table keyed by (16-bit limb, 5-bit amount, direction) returns each limb's two partial contributions, recombined by direction muxes; left shift is modeled as right shift with adjusted amount; SRA adds one more lookup — a sign-filler table returning the high-ones mask for the (sign, amount) pair [57]. The masking is a literal AND-table query of the amount against 0b11111 [57].
Two observations a VM designer should not miss. First, all three strategies get the ISA's masking rule for free from structure — a table indexed mod XLEN, a divisibility range check, an AND lookup — rather than by trusting the toolchain never to emit an oversized amount; this matters because the claim being proven is about arbitrary register values, not about well-behaved compilers. Second, the immediate forms are dramatically cheaper than the register forms in Jolt's strategy (one row versus a two-row expansion), and identically priced in the limb strategies — a cost asymmetry with no silicon analog, where the barrel shifter neither knows nor cares where the amount came from.
4.4 · Comparisons: signedness is an interpretation
Architected behavior. SLT/SLTU/SLTI/SLTIU write 1 or 0. The bit patterns are compared under two interpretations — and the ISA's one genuinely counterintuitive corner here is SLTIU: its immediate is sign-extended to XLEN first, then compared unsigned, which is why sltiu rd,rs1,1 (compare against 1) is the canonical seqz and an immediate of −1 compares against 2XLEN−1 [1].
The obligation. "a < b" over field elements is meaningless — a prime field has no order compatible with its arithmetic — so comparison must be reduced to range facts about a difference, or to a table. The four answers:
- Jolt: one lookup per comparison into SignedLessThan / UnsignedLessThan virtual tables; the 0/1 output is written to rd. Signedness is a property of the table's defining polynomial, not of any circuit branch [12].
- OpenVM and Ceno (shared design): find the most significant differing limb with a one-hot marker vector, prove the marker is correctly placed by prefix constraints, then range-check (diff_val − 1) to force the difference nonzero and correctly signed. Signed comparison re-reads the top limb through a witnessed "MSB field value" allowed to lie in [−2L−1, 2L−1) instead of [0, 2L) — the two's-complement bias applied at the witness level, one datapath for both signednesses [44][28].
- Airbender: comparison is a by-product of subtraction — the shared sub relation's borrow bit is unsigned-less-than; an is-zero relation on the difference gives equality; and a 128-entry resolution table keyed by (funct3, borrow, eq, sign₁, sign₂) decides both "should branch" and "should store," folding the entire SLT-and-branch family's control logic into one lookup [58].
The bias trick deserves one sentence of respect from the hardware audience: it is exactly the observation that signed comparison is unsigned comparison with the top bit flipped, executed here not as an XOR gate on two wires but as a relaxation of a range constraint on a witness. Same identity, different substrate.
4.5 · Branches: both futures are constrained
Architected behavior. Six conditional branches (BEQ/BNE/BLT/BGE/BLTU/BGEU), B-format, no destination register, 13-bit signed byte offset with an implicit zero low bit, target = branch's own PC + offset. The precise trap semantics matter to compliance reviewers: an instruction-address-misaligned exception is raised only if the branch is taken and the target violates IALIGN, is reported on the branch rather than the target, and cannot occur at all when the C extension (IALIGN=16) is present [1].
The study note this article grew from carries a worked trace that is worth keeping — a three-iteration loop, printed as the machine steps:
// x5 = running sum, x6 = counter, x7 = limit (=3)
// loop: add x5,x5,x6 ; addi x6,x6,1 ; bge x7,x6,loop
step PC instr after
1 0x20 add x5,x5,x6 x5=1 x6=1
2 0x24 addi x6,x6,1 x6=2
3 0x28 bge x7,x6,0x20 3>=2 -> PC=0x20 (taken)
4 0x20 add x5,x5,x6 x5=3 x6=2
5 0x24 addi x6,x6,1 x6=3
6 0x28 bge x7,x6,0x20 3>=3 -> PC=0x20 (taken)
7 0x20 add x5,x5,x6 x5=6 x6=3
8 0x24 addi x6,x6,1 x6=4
9 0x28 bge x7,x6,0x20 3>=4 -> PC=0x2C (fall through) // x5 = 6
Nine rows of a witness. The prover does not predict step 9's outcome; it already happened. What must be proven is that the recorded outcome is the only one the ISA permits — which is two constraints, not one: that the condition bit is the correct function of the operands (§4.4's machinery), and that next-PC took the branch iff the bit says so. Every VM constrains both arms:
- OpenVM writes it in one line: to_pc = from_pc + cond·imm + (1−cond)·4 [44]. Ceno's is algebraically identical (next_pc = pc + taken·imm − taken·4 + 4), with the immediate coming out of the committed program record rather than a witness [28].
- Jolt computes the condition as a lookup output, multiplies it by the Branch flag into a ShouldBranch product wire, and lets R1CS rows force NextUnexpandedPC to PC+imm under ShouldBranch and to the sequential successor otherwise [9][12]. Equality itself is the textbook multilinear product ∏(xiyi+(1−xi)(1−yi)) — notably not the inverse-witness trick — because in a lookup-central design equality is just one more table [12].
- Ceno and OpenVM prove BEQ/BNE with the multiplicative-inverse marker: cmp_eq + Σ(ai−bi)·invi = 1 with cmp_eq·(ai−bi) = 0 per limb — the classic "a nonzero difference has an inverse, zero does not" certificate [28][44]. Airbender resolves the whole family through its conditional table (§4.4) and then enforces alignment negatively: the target's bit 1 is extracted by lookup, and taken·bit1 = 0 is a constraint — a taken misaligned branch is not a trap but an unsatisfiable proof [58], a compliance divergence §5 returns to.
The economics, once more against silicon: no predictor, no flush, no fetch bubble — a taken branch costs identically to a fall-through, and a data-dependent unpredictable branch costs identically to a perfectly biased one. The entire branch-handling apparatus of a modern front end exists to hide a latency that the proving model does not have. What the proving model has instead is the obligation to constrain the road not taken: the (1−cond) arm, which hardware simply never executes, must appear in the constraint system so that the prover cannot claim a fall-through where the ISA demands a jump.
4.6 · Jumps: the link, the low bit, and the compressed adjustment
Architected behavior. JAL rd,off: rd ← PC+4, PC ← PC + sext(21-bit offset). JALR rd,rs1,imm: target = (rs1 + sext(imm)) with bit 0 cleared, rd ← PC+4, PC ← target; the byte-granular immediate (unlike B/J offsets, not premultiplied by 2) and the bit-clearing are both deliberate, the latter making function-pointer targets robust to odd addresses [1]. Because the target is computed from the old rs1, jalr rd,rs1 with rd = rs1 is well-defined. The link value is the address of the next sequential instruction — PC+4, or PC+2 when the jump was a compressed encoding, the one place where the C extension's existence leaks into the semantics of another family [4].
The obligation and the four answers. Three claims per jump: the target arithmetic (an add, §4.1's machinery), the bit-0 clearing (JALR only), and the link write.
- Jolt makes the target a lookup output — JAL through the plain truncation table on PC+imm, JALR through a
RangeCheckAlignedtable whose entries are (index & mask) & ~1, so the bit-clearing is a property of the table rather than a constraint [13]. The link is an R1CS row: rd = PC + 4 − 2·IsCompressed under the Jump flag — the compressed adjustment carried as a per-row decode flag [13][21]. - Ceno witnesses rs1+imm limb-wise, holds the target in an address gadget that separately witnesses its low bits as booleans, and forms next_pc by subtracting the witnessed bit 0 — alignment and bit-clearing expressed as controlled decomposition [29]. JAL is cheaper by construction: the committed program stores the J-immediate as (next_pc − pc), so the fetch record itself pins the target and the circuit adds nothing [29]. Both link writes decompose PC+4 into range-checked pieces bounded to the 30-bit PC space [29].
- OpenVM computes rs1+imm as two 16-bit limbs plus an explicitly witnessed boolean least-significant bit that is then dropped: to_pc = 2·limb₀ + 216·limb₁, with limbs range-checked to 15 and PC_BITS−16 bits — its comment cites the spec's bit-clearing sentence directly [45]. The JAL/LUI shared chip range-caps the link's top limb via a XOR-table trick to keep it inside the 30-bit PC space [45].
- Airbender merges JAL and JALR into one family, muxing src1 between PC and rs1, then passes the target's low word through a cleanup lookup returning (bit 1, word & ~3): bit 0 is forced off — the JALR clear — and bit 1 set under execution makes the circuit unsatisfiable, which is also how it enforces 4-byte targets with no C extension [59].
Note the compliance shading on the link value: Jolt proves PC+4−2·IsCompressed because it accepts RVC; Ceno, OpenVM and Airbender prove PC+4 unconditionally because they reject RVC at build time (§4.11) — each is exactly correct for the ISA profile it actually claims.
4.7 · LUI and AUIPC: constants and position
Architected behavior. A 12-bit immediate cannot carry a large constant, so the U-format pair sets the upper bits: LUI rd,imm is imm<<12 (low 12 bits zero; on RV64 the 32-bit value is sign-extended), and AUIPC rd,imm is this instruction's own PC + sext(imm<<12) — the primitive from which all PC-relative addressing, and therefore all position-independent code, is built [1].
Under the prover, these are the cheapest instructions in the ISA, and instructive for that reason. Jolt proves LUI as a lookup of the immediate itself and AUIPC as the PC+imm truncation lookup [9]. Airbender writes the decoder-assembled immediate straight to rd with no arithmetic at all, and its AUIPC add needs no alignment check since a U-immediate cannot disturb the low bits [60]. Ceno's default build gives each a small dedicated circuit (with the program table storing the immediate in a limb-friendly pre-shifted form) [30] — but its Goldilocks build does something more interesting: it transpiles them away, LUI into an internal ADDI from x0 with the full shifted constant (its internal immediate field is not limited to 12 bits), and AUIPC into an ADDI whose immediate has the link-time PC already folded in — legal precisely because a static program's PC-relative constants are static [30]. OpenVM pre-shifts the U-immediate at transpile time and shares one chip between LUI and JAL [45].
The lesson generalizes: an instruction whose result is a static function of the instruction word and its address is not really a runtime instruction to a zkVM — it is a constant the committed program carries, and the VM's only job is to route it. Hardware cannot make this move because hardware does not get to preprocess the program it will run.
4.8 · Loads and stores: where the machine touches the world
Architected behavior. The only addressing mode: EA = rs1 + sext(imm12). Loads sign-extend (LB/LH/LW) or zero-extend (LBU/LHU, and RV64's LWU) into rd; stores truncate rs2 to the access width. RV64 adds LD/SD. The fine print a compliance reviewer will probe: a load to x0 must still raise its exceptions and perform its side effects even though the value is discarded; naturally aligned accesses never raise address-misaligned and are guaranteed atomic; misaligned accesses are EEI-defined — they may complete (possibly slowly, via invisible trap), raise address-misaligned, or raise access-fault [1]. Memory is byte-addressed; endianness is EEI-chosen with little-endian the standard convention.
The obligation splits in two. The consistency half — every read returns the latest write — is global machinery (§5), priced per access. The per-instruction half is address arithmetic, alignment, and sub-word extraction, and here the four VMs diverge on a genuine design axis: what is the unit the memory argument sees?
- Jolt: the doubleword. Its RAM argument (Twist) addresses 8-byte-aligned doublewords only;
LD/SDare the only memory instructions that survive into proven bytecode. Every narrower access is expanded at trace time into an aligned-doubleword sequence: LB becomes align-the-address (ANDI −8), LD the containing doubleword, XOR/shift the selected byte into the top, then SRAI 56 or SRLI 56 — sign extension performed by the arithmetic-shift lookup, with the byte-lane selection done by XORing the low address bits [15]. Sub-word stores become masked read-modify-writes: LD, build the lane mask, merge rs2 by XOR/AND/XOR, SD the whole doubleword back [15]. Halfword and word forms additionally assert alignment through dedicated lookup tables (1 iff addr mod 2/4 = 0) [15]. The R1CS glue is five rows: address = rs1+imm when a memory flag is set, load ⇒ read = write = rd, store ⇒ rs2 = written value [9]. - OpenVM: the aligned 4-byte block, with the shift as an opcode. One adapter always accesses the aligned word containing the EA, and the core chip treats each (operation, byte-offset) pair as a distinct internal opcode — LoadBu0 through LoadBu3, StoreH0/2, and so on — muxing limbs between read data and prior contents [48]. Alignment is a divisibility range check on (EA − shift)/4; sign-extending loads are routed to a separate chip that extracts the top bit by range-checking (top_limb − bit·128) to 7 bits and fills upper limbs with bit·0xFF [48]. Misaligned access is simply outside the execution environment's contract — guest-invalid, host-fatal [48].
- Ceno: the word, one circuit per width. Five load circuits and three store circuits; EA witnessed limb-wise with boolean carries; an address gadget witnesses the low bits as booleans and range-checks the middle, enforcing per-width alignment (align4 for LW, align2 for LH/LHU, none for bytes) with the actual memory read always at the aligned word [33]. Byte extraction muxes on the witnessed address bits; SB/SH stores decompose the previous word into range-checked bytes and splice [33]. Misalignment is a trap in the emulator, which simply never produces a provable trace for it [33].
- Airbender: the word, with ROM carved out of the address space. EA = rs1+imm through the shared add relation; the low two bits come from a lookup, and alignment is enforced negatively — (bit0+bit1)·exec_word = 0 makes a misaligned word access unsatisfiable [60]. A high-bits lookup classifies the address as ROM or RAM: ROM loads are served by the committed-program table (with the cycle's RAM query then pinned to a harmless x0 access), stores into ROM are unsatisfiable, and one lookup keyed (halfword, bit0, funct3) performs all four sub-word extensions at once [60]. Sub-word stores are read-modify-write splices on the cycle's single load/store slot [60].
The unifying observation: no VM proves a misaligned access, and none needs the EEI's permissive language — each picks the strictest reading (align or be unprovable/trapped) because supporting misalignment would poison the sub-word extraction machinery for a case compilers essentially never emit. This is an EEI choice the spec explicitly authorizes, but a reviewer should know it is being made, and that it is load-bearing: an attacker-supplied address is checked by constraints, not by trust in the compiler. And the x0 fine print — exceptions still fire on a discarded load — is quietly honored in OpenVM's needs_write mechanism, which suppresses only the register write, never the access itself [40].
4.9 · Multiply and divide: nondeterminism's showcase
Architected behavior (M extension). MUL returns the low XLEN bits of the 2·XLEN-bit product — identical under either signedness, which is why one instruction suffices. The high half needs its own instruction per signedness combination: MULH (s×s), MULHU (u×u), MULHSU (s×u, roles fixed). Division truncates toward zero, and the M extension is deliberately trap-free — the spec's own rationale — with fully defined degenerate results: divide by zero gives quotient all-ones (−1 signed, 2XLEN−1 unsigned) and remainder = dividend; the unique signed overflow (−2XLEN−1 ÷ −1) gives quotient = dividend, remainder = 0 [2]. RV64 adds W variants, all sign-extending their 32-bit results — including DIVUW/REMUW, which sign-extend an unsigned 32-bit result, the corner most likely to catch an emulator author [2].
Multiplication is where field size speaks loudest. Jolt multiplies two 64-bit operands natively in its 254-bit field — the 128-bit product is one field multiplication — then extracts low or high half by lookup (truncation table for MUL, an upper-word table for MULHU), and expands MULH/MULHSU into the standard sign-correction identity (sign masks via a lookup, two corrective MULs, a MULHU, two ADDs) rather than proving them directly [17]. The small-field VMs run schoolbook limb convolution with carry chains: OpenVM checks each (limb, carry) pair against a two-dimensional range table sized [28, 8·28] [46], Ceno's carries are range-checked to 18 bits over 16-bit limbs [31], and both witness the MULH sign correction as ext/sign fields entering the upper convolution [46][31]. Airbender's single MulDivRelation — op1·op2 + additive = lo + 232·hi with per-operand sign fields, enforced as four 16-bit-chunk constraints with byte-plus-bit carry witnesses — serves the whole family [61].
Division is the family where every VM plays the same card, because it is the best card in the deck: the proving relation verifies division without representing a divider inside the arithmetic circuit. The prover's witness generator still performs the division the ordinary way — quotient and remainder are computed, once, outside the constraint system — and then enters them as advice for the circuit to check against the division contract. Stated fully — and this is the checklist a soundness reviewer should carry, because each clause has been a real bug class somewhere in this industry's history:
| Clause | Why it cannot be dropped |
|---|---|
| 1. q·d + r = dividend, computed where it cannot wrap | The defining identity — but alone it admits infinitely many (q, r) pairs |
| 2. 0 ≤ |r| < |d| (sign of nonzero r = sign of dividend, for signed) | The clause that makes (q, r) unique; omit it and q = 0, r = dividend satisfies clause 1 for every input |
| 3. The product q·d must not overflow the width in which clause 1 is checked | Otherwise a wrapped product forges the identity |
| 4. d = 0 ⇒ q = all-ones, r = dividend, exactly | Clause 2 is vacuous or ill-defined at d = 0; the ISA pins the answer and the circuit must pin it too |
| 5. Signed overflow ⇒ q = dividend, r = 0 | The one input where clause 1's naive form fails signed; must be carved out explicitly or absorbed by construction |
How each VM discharges the checklist: Jolt takes q and r as VirtualAdvice rows (range-checked to 64 bits by their own lookup [18]), then runs an assert-sequence: a ValidDiv0 table forces q = 264−1 whenever d = 0 (clause 4); a ChangeDivisor table substitutes d→1 exactly on the overflow input pair so the same recomposition covers clause 5; MULH/MUL of q against the adjusted divisor with an asserted sign-extension high half discharges clause 3; recomposition + AssertEQ gives clause 1; and absolute values via SRAI/XOR/SUB feed a ValidUnsignedRemainder table — entry true iff r < d or d = 0 — for clause 2 [18]. The DIVU sequence is the same skeleton with an explicit no-overflow table and an unsigned q·d ≤ dividend assert [18]. OpenVM folds all five clauses into one core chip: carry-checked convolution for clause 1/3, an r′ = |r| negation path and one-hot unsigned comparison against d for clause 2, a zero_divisor flag whose constraints force every q limb to 255 (clause 4), and an r_zero flag that absorbs the overflow input as "the r = 0 special case," with execution returning q = dividend there (clause 5) [47]. Ceno's circuit is the same architecture over 16-bit limbs (its comments credit the OpenVM core), with inverse witnesses proving divisor ≠ 0 on the main path and both special-case flags mutually exclusive [32]. Airbender reuses its MulDivRelation as the recomposition, pins q's limbs to 0xffff under a divisor-is-zero flag, enforces r < d through the sub relation's borrow with (1−div0)·(1−borrow)·exec = 0, and — a detail we found nowhere else — ships a Z3 script in-tree that machine-checks its signed-remainder sign logic [61]. Its shipped prover configuration, note, simply drops signed division: the NoSignedMulDiv machine variant decodes MULH/MULHSU/DIV/REM as invalid, and the toolchain must not emit them [64].
Step back and appreciate what happened to the cost model. In silicon, DIV is the slowest integer instruction ever shipped — tens of cycles of iterative subtract-shift, or acres of SRT lookup logic. Under a prover it is two advice values and a handful of checks, cheaper than the shifts. The saving is precisely the witness-generation-versus-constraint-verification distinction: the division still happens — once, in the prover's unconstrained witness generator, at ordinary software cost — and what disappears is any need to represent the dividing inside the constraint system, which only re-checks the result. Constraint count is the scarce resource; native computation is not. The flip side is that the entire soundness burden concentrates into the checklist above — silicon's divider can be slow but cannot be argued with, whereas a missing clause 2 is not a performance bug but a forgery vector.
4.10 · ECALL, EBREAK, FENCE, CSRs: the system boundary
Architected behavior. ECALL and EBREAK raise precise exceptions — environment call and breakpoint — and the base ISA assigns them no register side effects; everything else (cause codes, xepc pointing at the instruction itself, parameter passing in a0–a7) belongs to the privileged architecture and the EEI [1]. FENCE orders memory/IO accesses between predecessor and successor sets encoded in pred/succ fields (with fm=1000, pred=succ=RW as FENCE.TSO, and reserved configurations required to execute as normal fm=0000 fences honoring the encoded pred/succ sets); on one hart with one memory, it orders nothing [1]. The Zicsr instructions atomically read-modify-write one of 4096 CSRs, with precisely specified read/write suppression: CSRRW with rd=x0 skips the read (and its side effects); CSRRS/C with rs1=x0 skip the write entirely — the asymmetry that makes csrr and csrs safe idioms [5].
Under the prover, this family is where ISA compliance genuinely forks, because a zkVM has no privileged software stack to trap into — the "environment" is the host, outside the proof. Four different answers, from most to least architectural:
- Jolt models the trap. ECALL expands into a genuine M-mode entry sequence: materialize the faulting PC into a shadow mepc, write cause 11 and mtval 0, compose mstatus (MPP=M, interrupts off), and jump through the shadow mtvec — all against CSR shadow copies living in virtual registers; MRET returns through mepc symmetrically [19]. CSRRW/CSRRS expand to register moves against those shadows, with read-before-write order preserved and unmapped CSRs rejected at expansion [19]. FENCE survives to final bytecode as a decoded instruction with no lookup and no flags — proven, as a no-op [19]. EBREAK becomes
JAL +0, a self-jump the tracer reads as termination [19]. - Ceno keeps exactly one door. ECALL is the only surviving SYSTEM instruction — dispatched per syscall number (read from x5, SP1-compatible codes) to dedicated circuits: halt (exit code to public values, next_pc forced to the exit sentinel), I/O commit, and the precompile fleet (Keccak, SHA extend, secp256k1/r1, BN254, uint256) [36]. EBREAK, every CSR instruction, FENCE, MRET and WFI all transpile to an unimplemented kind whose execution traps — no CSR file exists at all [22].
- OpenVM replaces the door. ECALL, EBREAK and all CSR encodings transpile to
unimp— a TERMINATE with panic exit code 2 — except the one CSR idiom its comment calls out (CSRRW x0,x0: a counter reset with no CSR file behind it) which becomes a NOP; termination and hints instead use custom instructions on the custom-0 major opcode — encoding space the spec designates for exactly this use, and FENCE transpiles to NOP [49]. The system boundary is thus not RISC-V at all — it is OpenVM's own ISA, entered from RISC-V by transpilation. - Airbender inverts the CSR's meaning. ECALL/EBREAK have no decoder match — SYSTEM funct3=000 simply does not decode, so their appearance makes the trace unprovable [62]. What remains of Zicsr is CSRRW alone, aimed at a whitelist table over the 4096-CSR space: CSR 0x7c0 is the non-determinism oracle (reads return unconstrained host words, range-checked only), and designated CSRs above it are the precompile interface — a CSRRW to 0x7c7 emits a delegation request that a Blake2 circuit must answer through a set-equality argument [62]. FENCE's opcode arm is commented out in the simulator ("nothing to do in fence, our memory is linear") making it illegal [64]; the ROM is padded with
0xc0001073—csrrw x0, cycle, x0— which is unprovable precisely becausecycleis not on the whitelist: the padding word is a semantic booby trap [53].
For the hardware reviewer, the honest summary is: the unprivileged compute core is proven bit-exactly everywhere; the privileged and environment layer is nowhere proven as the spec writes it — it is modeled (Jolt), narrowed to one syscall (Ceno), transpiled away (OpenVM), or repurposed as a bus to co-processors (Airbender). All four choices are defensible under the EEI escape hatch the spec itself provides; none of them is what a compliance suite for silicon would accept without a profile document saying so. §7 returns to why this is the correct engineering outcome and not a corner cut.
4.11 · Atomics and compressed encodings: extensions as lowering problems
A extension. Architecturally: LR registers a reservation; SC writes only if the reservation survived, returning 0 on success or a nonzero failure code (canonically 1), and always invalidates the reservation; nine AMOs atomically load-op-store returning the old value; all require natural alignment (misalignment is an exception — unconditionally for LR/SC, and for AMOs absent the optional misaligned-atomicity-granule PMA); aq/rl bits order; a constrained 16-instruction LR/SC loop carries an eventual-success guarantee [3]. On a single-hart machine with no interrupts, every one of these guarantees is satisfied by doing nothing special — atomicity against nobody is free. Jolt is the only VM of the four that accepts the A extension, and its treatment is the correct minimal one: AMOs expand to plain LD, op, SD sequences (its comment noting that sequential traces make the read-modify-write order the whole of atomicity), with min/max computed branch-free via a comparison-then-blend [20]. LR/SC is subtler and Jolt actually models the reservation: LR records the address into a dedicated virtual register (asserting it points into RAM); SC takes its success bit as Boolean advice, constrained so that success ⇒ address matches the reservation, stores old + success·(rs2−old), and returns the architectural 0/1 [20]. Note the fine grain: the ISA permits spurious SC failure, so "success is advice, constrained only when claimed" is not a shortcut — it is exactly the specification. Ceno, OpenVM and Airbender reject the extension outright (build-time transpile error or undecodable), pushing the problem into the toolchain — the lower-atomic story our companion article documents at length.
C extension. Architecturally: ~30 sixteen-bit encodings, each expanding to exactly one base instruction, with register fields restricted (3-bit specifiers address x8–x15), immediates zero-extended and scaled for loads/stores, a lattice of reserved and HINT code points, and the mapping's only non-identities at its linking jumps — C.JALR, and RV32's C.JAL, link PC+2 where their 32-bit expansions would link PC+4 [4]. For a prover, RVC is a fetch-and-decode problem, not an execute problem: Jolt statically expands every 16-bit encoding to its 32-bit form before decode, and carries a single per-row IsCompressed flag whose only constraint effects are the −2 in the link value and the +2 in the sequential-PC update [21] — the entire extension reduced to one bit of decode metadata. The other three reject RVC at build time (OpenVM's transpiler consumes the text segment as strict 32-bit words [37]; Ceno walks the ELF four bytes at a time [22]; Airbender fetches from a 4-aligned ROM and hard-wires PC+4 [64]), which is why their jump circuits may verify only bit 1 of targets: in a world without 2-byte instructions, bit 1 is the whole alignment question. The C extension is also why a VM that does accept it must never raise the misaligned-target exception (IALIGN=16 makes it architecturally impossible) — a spec interaction Jolt's expanded-PC bookkeeping absorbs silently.
5 · System-level interactions: where the opcodes meet the argument
5.1 · Registers are memory; ports are free; consistency is the tax
All four VMs make the same move, from four directions: the register file is not a separate structure but a small address range inside the same memory-consistency argument as RAM. Jolt runs a Twist instance over 128 register cells — 32 architectural plus 96 virtual scratch registers its expansions and inlines use (reservation slots, CSR shadows, temporaries) [16]. Ceno tags register accesses as a RAM type with per-access timestamps, four sub-cycle ticks per instruction (rs1, rs2, rd, and an optional memory slot), plus a 33rd phantom register that absorbs writes from rd-less instructions [34]. OpenVM gives registers address space 1 — 32 registers × 4 byte-limbs at pointers [0,128) — inside its offline-checking bus [39][41]. Airbender's only persistent machine state between cycles is the PC; the 32 GPRs live in the shuffle-RAM argument, three fixed-timestamp queries per cycle (rs1, rs2-or-load, rd-or-store) [63].
The consequences invert several decades of register-file lore. Read ports are free — a fourth register read would cost one more multiset tuple, not quadratic area. Register pressure still matters (spills are memory traffic and memory traffic is tuples) but renaming, banking, and port scheduling simply have no referent. The one hardware intuition that transfers intact is x0: every VM must actively enforce its zero-ness, and the mechanisms are a nice cross-section of each design's character — OpenVM refuses at transpile time to ever write it (rd=x0 side-effect-free instructions become NOPs; JAL/loads carry a needs_write flag) [40], Airbender masks the writeback value to zero through an is-zero gadget on the rd index [63], and Jolt/Ceno pin cell 0 / route rd-less writes to a sink cell in the memory argument itself [16][34].
Timestamps are the other quiet load-bearing element. Ceno enforces prev_ts < ts per access with a limb-decomposed less-than gadget under a 29-bit budget [34]; OpenVM does the same with timestamps in [1, 229] [41]; Airbender packs two 19-bit limbs with the low two bits encoding the three per-cycle accesses, budgeting 236 cycles [63]. These bounds are real ISA-visible capacity limits — a trace longer than the timestamp budget is not slow, it is unprovable in one segment — and they are why every production VM shards long executions into continuation segments glued by carrying the machine state (or a commitment to it) across segment boundaries as public values [36].
5.2 · Fetch and decode: the program as a table
§2 promised decode could amortize to zero; here is the mechanism in each VM. Jolt commits the fully expanded bytecode — virtual sequences included — as a lookup table; each cycle's fetch is a lookup, with a dual PC bookkeeping (the ELF address for architectural semantics, the expanded index for sequencing) and constraints forcing virtual sequences to execute from their first row without interleaving [14][9]. Ceno commits a decoded program table — (pc, kind, rd, rs1, rs2, imm, imm_sign) — as fixed columns with a witnessed multiplicity per row; each instruction circuit's fetch is one lookup of its full decoded tuple, which is why its immediates can live in circuit-friendly encodings (pre-shifted U-immediates, pow2-encoded shift amounts in one build) chosen per opcode at preprocessing [35]. OpenVM commits the transpiled program as a cached trace and fetches over a program bus keyed (pc, opcode, seven operands), with an execution bus chaining (pc, timestamp) between chips and a connector AIR pinning initial and final PC to public values [50]. Airbender is the outlier that decodes per cycle — but even its in-circuit decoder is mostly preprocessing in disguise: one 217-entry fixed table keyed opcode‖funct3‖funct7 returns a bitmask of format and family flags, two fixed decomposition tables validate the operand chunks, and the ROM itself is a preprocessed lookup table padded with an unprovable sentinel [52][64].
The structural fork worth a hardware analogy: Jolt/Ceno/OpenVM decode at preprocessing — the analog of a trace cache or predecoded I-cache, paid once per static instruction — while Airbender decodes per cycle in constraints, the analog of hardwired decode, paid per dynamic instruction but requiring no per-program preprocessing of the proving key beyond the ROM table. Both positions are coherent; they trade setup cost against cycle cost, and Airbender's choice fits a system whose one guest (an L2 block executor) is fixed and whose cycles are astronomically many.
5.3 · The field is an architectural parameter
No hardware ISA document has a chapter on "the field," but for a zkVM it determines more than XLEN does. The four data points:
| Field | Size | Word repr. | What it buys | What it costs | |
|---|---|---|---|---|---|
| Jolt | BN254 scalar field [8] | ~2254 | one 64-bit word per element | native 64×64→128 products; one-lookup truncation; pairing-curve commitments | large field arithmetic per constraint; bit-level claims need 64-bit-scale decompositions |
| Ceno | Baby Bear (default; Goldilocks build exists) [24] | ~231 | 2 × 16-bit limbs | fast small-field arithmetic; 16-bit range tables | every word op is a limb protocol; carries everywhere |
| OpenVM | Baby Bear (15·227+1) [39] | ~231 | 4 × 8-bit limbs | byte-granular lookups unify logic ops and range checks | 4-limb carry chains; more columns per word |
| Airbender | Mersenne31 (231−1) [54] | ~231 | 2 × 16-bit limbs | cheapest reduction of all; degree-2-only AIR discipline [54] | 32-bit word never fits one element; all the limb costs |
One correction to our own study notes, worth making publicly since we made the error publicly available to ourselves: the note this article grew from tabulated Airbender's field as Baby Bear. The pinned source is unambiguous — Mersenne31Field with order 231−1 [54]. Two 31-bit fields, easy to conflate, materially different in reduction cost and FFT structure; the kind of cell-level error a table invites and a source pin catches.
The deeper point for an ISA audience: field size versus word size is the zkVM's word-size decision, and it does not track the guest's XLEN. Jolt proves a 64-bit ISA in a 254-bit field (word ⊂ field, comfortably); the other three prove a 32-bit ISA in 31-bit fields (word ⊄ field, permanently limbed). The apparent coincidence that everyone's ratio lands near "word ≈ field or word = 2 × 16-bit limbs" is not a coincidence at all — it is the same engineering pressure that gave hardware its power-of-two register widths: range-check tables want power-of-two sizes, and limb counts want to be small.
5.4 · Four answers to opcode heterogeneity
Finally, the architecture-level taxonomy, which §3 promised. The ISA hands every implementer the same problem: ~50 operations of wildly different character must somehow share one execution substrate. Microarchitects answer with functional units and issue queues; these four proof systems answer with:
| Stance | Mechanism | Nearest hardware analog | |
|---|---|---|---|
| Jolt | One universal row; semantics live in tables | Every cycle is the same ~22-constraint R1CS row [9] + one lookup into the executing instruction's virtual table (40 tables total [10]); complex opcodes lowered to a ~29-opcode proven core + assert/advice primitives [7] | Microcode: a tiny fixed datapath, complexity pushed into a control store (here, into table structure) |
| Ceno | One circuit per opcode | ~45 chips, each proving all dynamic instances of its kind, glued by the shared fetch/state/memory arguments [23] | Fully hardwired control: dedicated functional unit per operation, no shared ALU at all |
| OpenVM | One chip per opcode class, ISA open at the edges | 13 RV32IM executors (adapter + core AIRs) [38] on shared program/execution/memory buses; new capability = new chip + new custom opcode, not a new VM | A chiplet fabric with a standard bus protocol; the ISA is the interconnect spec |
| Airbender | Thirteen op families in one AIR, heavy ops delegated | Thirteen family circuits [51] sharing relation slots multiplexed by orthogonal family flags [55]; degree ≤ 2 everywhere; crypto exits through CSR-addressed delegation circuits [62] | A minimal in-order core with tightly-coupled accelerators on a co-processor interface |
The trade is the CISC/RISC argument reincarnated with different physics. The universal-row design pays a fixed per-cycle overhead so that rare opcodes cost nothing extra to support; the chip-per-opcode design pays nothing for uniformity but must commit, prove, and verify a chip even for opcodes the program barely uses, and its proving-key surface grows with the ISA. Delegation splits the difference by class: keep the base integer core small and uniform, and let anything with internal structure (hash rounds, big-integer ops) be a separate argument connected by a bus — which is, of course, exactly what the ISA itself did when it pushed those same operations out to extensions.
6 · The opcode encyclopedia
The reference deliverable. Every instruction of RV32I, RV64I, M, A, C, Zicsr, and Zifencei, with its encoding, exact semantics, the spec-mandated edge cases an implementation answers for, and the proving considerations attached to it in a zkVM. Conventions: sext/zext = sign-/zero-extension; edge cases states normative ISA behavior per the ratified manual [1][2][3][4][5]; proving considerations distills the obligations of §4 and names the four VMs' concrete treatments where they differ, with citations carried by the corresponding §4 discussion unless a claim appears here only. The encoding line gives format · major opcode · funct fields. Where an entry says "as row X," the obligation is identical and only the noted delta applies — the table is normalized the way an ISA manual is, not the way a tutorial is.
6.1 · Integer register–immediate (OP-IMM = 0010011)
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
ADDI rd,rs1,immI · OP-IMM · f3=000 | rd ← rs1 + sext(imm12), wrap mod 2XLEN | No overflow exception ever. addi x0,x0,0 is the canonical NOP; addi rd,rs,0 is mv. rd=x0 otherwise: HINT code point. No SUBI exists — negative immediates serve. | The cheapest real instruction: the immediate is a committed-program constant (no witness, no runtime range check), leaving only §4.1's truncation obligation. Its NOP encoding is why the ISA needs no dedicated NOP opcode — though the VMs pad with internal no-ops of their own (Jolt a Noop bytecode kind, OpenVM a PHANTOM, Airbender its booby-trapped UNIMP). Ceno's Goldilocks build even absorbs LUI/AUIPC into a widened internal ADDI (§4.7). |
SLTI rd,rs1,immI · OP-IMM · f3=010 | rd ← (rs1 <s sext(imm)) ? 1 : 0 | Signed compare vs sign-extended immediate. rd=x0: HINT. | §4.4 comparison machinery with a constant right operand. The result is a single boolean — trivially range-checked — but the compare itself still needs the full limb/bias or table apparatus; constant operands buy nothing structural. |
SLTIU rd,rs1,immI · OP-IMM · f3=011 | rd ← (rs1 <u sext(imm)) ? 1 : 0 | Immediate is sign-extended first, then compared unsigned: imm=−1 compares against 2XLEN−1. sltiu rd,rs,1 = seqz. rd=x0: HINT. | The sext-then-unsigned subtlety is a decode-time fact: the circuit receives an XLEN-wide constant and never sees the 12-bit form, so the trap is entirely in the transpiler/preprocessor (OpenVM materializes i12 as a sign-extended 24-bit operand for exactly this reason [49]). |
XORI/ORI/ANDII · OP-IMM · f3=100/110/111 | rd ← rs1 op sext(imm) | Immediate sign-extended: xori rd,rs,−1 = NOT; andi with −1 = identity. rd=x0: HINT. | §4.2 lookup machinery, one operand constant. In byte-table VMs the constant's limbs are just fixed lookup inputs; in Jolt the interleaved index has half its bits fixed. No cost difference vs the register forms — unlike silicon, where an immediate saves a register read port. |
SLLI rd,rs1,shamtI · OP-IMM · f3=001 · imm[11:6]=000000 (RV32: imm[11:5]=0000000) | rd ← rs1 << shamt | RV32: 5-bit shamt, inst[25]=1 reserved. RV64: 6-bit shamt. Shamt is unsigned (never sign-extended). rd=x0: HINT (slli x0,x0,31 = semihosting marker). | Static shift amount collapses §4.3's hardest case: Jolt lowers to a single multiply-by-1<<shamt row [11]; limb VMs pay the same as the register form. The reserved shamt[5] code point is policed by decode (Airbender's bitmask table marks it invalid → unsatisfiable; OpenVM rejects at transpile; Ceno lowers it to an unimplemented kind that traps at runtime). |
SRLI rd,rs1,shamtI · OP-IMM · f3=101 · imm[11:6]=000000 (RV32: imm[11:5]=0000000) | rd ← rs1 >>u shamt | Zero-fills. Same shamt width/reserved rules as SLLI. rd=x0: HINT. | Static-amount right shift: Jolt encodes the amount as a bitmask immediate consumed by one VirtualSRLI lookup row [11]; elsewhere identical to SRL. |
SRAI rd,rs1,shamtI · OP-IMM · f3=101 · imm[11:6]=010000 (RV32: imm[11:5]=0100000) | rd ← rs1 >>s shamt | Sign bit replicates into vacated bits. inst[30] distinguishes from SRLI. Same reserved rules. rd=x0: HINT (srai x0,x0,7 = semihosting marker). | Sign-fill is the extra obligation over SRLI: a sign-mask term (Jolt's table adds it in the MLE; Airbender adds a filler-mask lookup; limb VMs force vacated limbs to sign·0xFF..) — §4.3. |
6.2 · Integer register–register (OP = 0110011, funct7 = 0000000 / 0100000)
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
ADD rd,rs1,rs2R · OP · f3=000 · f7=0000000 | rd ← rs1 + rs2, wrap mod 2XLEN | No overflow exception. rd=x0: HINT (specifically add x0,x0,x2..x5 encode the NTL.* locality hints). | §4.1 in full: in-field add + truncation lookup (Jolt) or limb carries + range checks (the rest). Two register reads + one write = three memory-argument tuples (§5.1); the ALU is the cheap part. |
SUB rd,rs1,rs2R · OP · f3=000 · f7=0100000 | rd ← rs1 − rs2, wrap mod 2XLEN | inst[30] selects SUB. No overflow exception; rd=x0 HINT. | Never a second datapath: bias-and-truncate (Jolt, +264), re-arranged addition (Ceno proves rs1 = rd+rs2), or the same shared relation with roles swapped (Airbender) — §4.1. The borrow is the carry in disguise everywhere. |
SLL rd,rs1,rs2R · OP · f3=001 · f7=0000000 | rd ← rs1 << rs2[4:0] (RV32) / rs2[5:0] (RV64) | Upper bits of rs2 ignored — no shift-overflow UB exists at ISA level. rd=x0: HINT. | The masking must come from structure, not trust: table indexed mod XLEN (Jolt), divisibility range check (OpenVM/Ceno), AND-table query (Airbender) — §4.3. Dynamic amount makes this the priciest ALU family per row. |
SLT / SLTUR · OP · f3=010/011 · f7=0000000 | rd ← (rs1 < rs2) ? 1 : 0, signed / unsigned | Same bit patterns, two answers — signedness is the instruction's interpretation, not the data's. sltu rd,x0,rs2 = snez. rd=x0: HINT. | §4.4: one lookup (Jolt), one-hot most-significant-differing-limb with biased MSB witnesses (Ceno/OpenVM), or the sub relation's borrow + resolution table (Airbender). The 0/1 output is the same wire branches consume — Airbender literally proves SLT and the branches in one circuit family, while Ceno and OpenVM share the comparison gadget across separate chips. |
XOR / OR / ANDR · OP · f3=100/110/111 · f7=0000000 | rd ← rs1 op rs2, bit-parallel | No edge cases; the ISA's only truly total, exception-free, interpretation-free operations. | §4.2: free in silicon, bought back by lookup here — per-byte-pair tables (Ceno/Airbender), one XOR table plus algebraic identities for OR/AND (OpenVM), giant virtual tables (Jolt). Airbender's table ids are the funct3 values [56]. |
SRL / SRAR · OP · f3=101 · f7=0000000/0100000 | rd ← rs1 >>u/s rs2[4:0 / 5:0] | Zero-fill vs sign-replicate; amount masking as SLL; rd=x0 HINT. | §4.3's three strategies in full (pow2/bitmask lookups, one-hot limb select, precomputed shift table). SRA ≠ SRL only by the sign-fill term; no VM implements a per-bit mux tree — the barrel shifter has no survivors here. |
6.3 · Upper-immediate (LUI = 0110111, AUIPC = 0010111)
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
LUI rd,immU · LUI | rd ← imm[31:12] << 12; RV64: sext of the 32-bit value | Low 12 bits zero by construction. rd=x0: HINT. Pairs with ADDI to build any 32-bit constant in two instructions. | A routed constant, not a computation (§4.7): direct write (Airbender), lookup of the immediate (Jolt), small recomposition circuit (Ceno/OpenVM, which pre-shift the immediate at preprocessing), or transpiled away entirely (Ceno's Goldilocks build). RV64's sign extension is one more decode-time constant fact. |
AUIPC rd,immU · AUIPC | rd ← PCthis + sext(imm << 12), wrap | Uses the AUIPC's own address, not PC+4. imm=0 reads the PC. rd=x0: HINT. The root of all PC-relative addressing. | The one instruction that makes the PC a data operand, so the circuit's PC wire must flow into the ALU path (a flag in Jolt's R1CS; PC limbs entering the add in Ceno/OpenVM/Airbender). For a fixed program it is almost a constant — Ceno's Goldilocks build folds the PC in at transpile time and erases the instruction (§4.7). |
6.4 · Control transfer (JAL = 1101111, JALR = 1100111, BRANCH = 1100011)
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
JAL rd,offJ · JAL | rd ← PC+4; PC ← PC + sext(off21) | 21-bit offset, implicit zero bit, ±1 MiB, scrambled encoding (imm[20|10:1|11|19:12]). Link = next sequential address (PC+2 if compressed form). rd=x0 = plain j; rd=x1/x5 are return-address-stack hints only. Misaligned-target exception reported on the JAL; impossible under IALIGN=16. | Target is static per call site — Ceno pins it through the committed program record (imm stored as next_pc − pc) at zero circuit cost [29]; others run the PC+imm add. Link write needs a range-capped recomposition to the PC space (XOR-trick cap in OpenVM/Ceno). The bit-scrambled immediate never reaches any circuit — preprocessing pays it once. |
JALR rd,rs1,immI · JALR · f3=000 | t ← (rs1+sext(imm)) & ~1; rd ← PC+4; PC ← t | Byte-granular immediate (not ×2). Bit 0 of the target is cleared. Old rs1 used, so rd=rs1 is defined. ret = jalr x0,0(x1). RAS push/pop hint matrix on rd/rs1 ∈ {x1,x5}. Only bit 1 can then offend alignment; exception on the JALR itself. | The dynamic jump: the one place §4.6's bit-clearing obligation is live. Realized as an aligned-truncation table (Jolt), a witnessed-and-dropped LSB (OpenVM, Ceno), or a cleanup lookup that also polices bit 1 by unsatisfiability (Airbender). Every VM must range-bound the computed target into its PC space — an obligation with no JAL analog. |
BEQ / BNEB · BRANCH · f3=000/001 | if rs1 =/≠ rs2: PC ← PC+sext(off13); else PC+4 | 13-bit offset, implicit zero bit, ±4 KiB. No rd. Misaligned-target exception only if taken; never for fall-through; impossible under IALIGN=16. | §4.5: equality by inverse-witness certificate (Ceno/OpenVM), product-form table (Jolt), or is-zero relation on the difference (Airbender); then the two-arm next-PC constraint — the untaken path is constrained, not skipped. Taken and untaken cost identically; branch predictors have no analog. |
BLT / BGEB · BRANCH · f3=100/101 | signed < / ≥ branch | Signed comparison; bgt/ble are operand swaps. Same alignment rules. | Comparison machinery of §4.4 feeding the same two-arm PC constraint; GE is 1−LT everywhere (OpenVM literally constrains cmp_lt = result·lt + (1−result)·ge). |
BLTU / BGEUB · BRANCH · f3=110/111 | unsigned < / ≥ branch | Unsigned compare; offset still sign-extended. The single-BLTU bounds-check idiom relies on negative indices comparing high. | Same circuits as BLT with the sign-bias disabled (a witness-range choice, not a datapath). The bounds-check idiom means provers see BLTU guarding nearly every memory access a compiler emits — its cost is a system-level constant. |
6.5 · Loads and stores (LOAD = 0000011, STORE = 0100011)
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
LB / LBUI · LOAD · f3=000/100 | rd ← sext/zext(mem8[rs1+sext(imm)]) | Bytes are always naturally aligned. rd=x0: value discarded but exceptions/side effects must still occur. Data extension is per-instruction; the address offset is always sext. | Sub-word extraction on top of a wider consistency-argument cell (§4.8): lane-select by low address bits (witnessed booleans, XOR tricks, or dedicated lookups), then sign/zero extension (shift pair, fill-with-sign·0xFF, or one combined lookup). One byte load costs a word/doubleword read plus extraction — the inverse of silicon, where narrow loads are the cheap ones. |
LH / LHUI · LOAD · f3=001/101 | rd ← sext/zext(mem16[EA]) | Misaligned EA: EEI-defined (complete / address-misaligned / access-fault); aligned is atomic and never faults on alignment. | As LB plus an alignment obligation, resolved strictly by every VM: assert-lookup (Jolt), witnessed-low-bit gadget (Ceno), divisibility check (OpenVM), unsatisfiability (Airbender). No VM exercises the EEI's permissive misalignment options (§4.8). |
LWI · LOAD · f3=010 | rd ← sext(mem32[EA]); full width on RV32 | On RV64, LW sign-extends (LWU zero-extends). 4-byte alignment rules as LH. | The native width for the RV32 VMs (their consistency-argument cell); for Jolt a lowered case of LD with lane-select and word sign-extension (§4.8). |
SB / SHS · STORE · f3=000/001 | mem8/16[EA] ← rs2 truncated | Upper rs2 bits ignored. S-format splits the immediate (inst[31:25|11:7]). No rd. Alignment rules per width. | A sub-word store is also a read: every VM implements read-containing-word, splice, write-back (masked RMW in Jolt/Airbender/Ceno; per-(op,offset) internal opcodes in OpenVM). A "write port" is a read tuple plus a write tuple in the memory argument. |
SWS · STORE · f3=010 | mem32[EA] ← rs2[31:0] | 4-byte alignment; truncation of a 64-bit rs2 on RV64. | Native-width store for RV32 VMs (direct write of rs2's limbs); lowered lane-splice into a doubleword for Jolt. Stores into committed-program space are separately policed where ROM and RAM share an address space (Airbender: unsatisfiable [60]). |
6.6 · RV64I additions (OP-IMM-32 = 0011011, OP-32 = 0111011, plus widths)
These twelve exist to keep 32-bit arithmetic exact on a 64-bit machine, and they all share one invariant a prover must internalize: every *W result is the sign-extension of a 32-bit value, so bits 63:31 are equal — an invariant the RV64 ABI extends even to unsigned 32-bit values [1]. Of our four VMs only Jolt (RV64) proves them; its uniform recipe is: compute in 64-bit space, then apply a word-sign-extension lookup — with per-family wrinkles noted below [11][17].
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
LWU / LD / SDI/I/S · LOAD f3=110/011 · STORE f3=011 | zext 32-bit load / 64-bit load / 64-bit store | LWU is the zero-extending word load (LW sign-extends on RV64). LD/SD: 8-byte natural alignment. | LD/SD are Jolt's only proven memory instructions — the fixed point of its lowering (§4.8); LWU is LD + lane-select + zero-extension. In RV32 VMs these encodings simply don't decode. |
ADDIWI · OP-IMM-32 · f3=000 | rd ← sext32((rs1+sext(imm))[31:0]) | Ignores upper 32 bits of rs1. addiw rd,rs,0 = sext.w. rd=x0: HINT. | 64-bit add then word-sign-extend lookup; the truncation-to-32 and the extension are one table (§4.1). |
SLLIW / SRLIW / SRAIWI · OP-IMM-32 · f3=001/101/101 · f7=00.../010... | 32-bit shifts of rs1[31:0], result sext32 | 5-bit shamt only; imm[5]≠0 is reserved (was illegal, now reserved — a compatibility-relevant reclassification). SRLIW zero-fills from bit 31, so it differs from SRLI even at equal shamt. | The "fills from bit 31" subtlety is why Jolt's word shifts first relocate the low word to the top half of the 64-bit space (multiply by 232) before reusing the 64-bit shift path (§4.3) — shifting in place would fill from the wrong bit. |
ADDW / SUBWR · OP-32 · f3=000 · f7=0000000/0100000 | 32-bit add/sub, result sext32 | Ignore upper halves of both inputs; rd=x0 HINT. | Full-width add/sub then word-sign-extension lookup (Jolt lowers ADDW to exactly that two-row sequence [11]). |
SLLW / SRLW / SRAWR · OP-32 · f3=001/101/101 · f7=00.../010... | 32-bit dynamic shifts, result sext32 | Amount = rs2[4:0] — five bits, where SLL on RV64 reads six; rs2[5] is ignored here, not reserved. | The masking width changes with the opcode, so the masking structure must too: Jolt swaps in a Pow2W table (mod 32) for SLLW and ANDs the amount with 0x1f for SRAW (§4.3). A shared 6-bit masking path would be a soundness bug precisely here. |
6.7 · M extension (OP funct7 = 0000001; W variants on OP-32)
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
MULR · OP · f3=000 · f7=0000001 | rd ← (rs1·rs2)[XLEN−1:0] | Low half is signedness-independent — one instruction serves both. Never traps. MULH-then-MUL is a fusion recommendation only. | Field-size showcase (§4.9): one native field multiply + truncation lookup when the double-width product fits (Jolt); schoolbook limb convolution with range-checked carries when it doesn't (the 31-bit-field VMs). The wrap that silicon gets by dropping wires is a proven truncation here. |
MULH / MULHU / MULHSUR · OP · f3=001/011/010 · f7=0000001 | rd ← upper XLEN bits of s×s / u×u / s×u product | MULHSU's roles are fixed (rs1 signed, rs2 unsigned) — it exists for multi-word signed arithmetic. Never traps. | Signedness lives in correction terms, not datapaths: sign-mask expansion into MULHU + fixups (Jolt lowers MULH entirely [17]), or witnessed sign bits injecting ext·0xFF limbs into the upper convolution (OpenVM/Ceno/Airbender). MULHU is everyone's primitive; the signed forms orbit it. |
DIV / DIVUR · OP · f3=100/101 · f7=0000001 | rd ← quotient, truncated toward zero | ÷0: quotient all-ones (−1 / 2XLEN−1), no trap — M is deliberately trap-free. Signed overflow (−2XLEN−1÷−1): quotient = dividend. Fusion note as MUL. | Advice + checklist (§4.9): q,r witnessed; q·d+r=dividend where it can't wrap; |r|<|d| forced; both degenerate cases pinned by explicit flags/tables. The slowest silicon instruction becomes one of the cheapest proven ones — and the checklist's clause 2 is the classic forgery vector if dropped. Airbender's shipped config drops signed DIV entirely (undecodable) [64]. |
REM / REMUR · OP · f3=110/111 · f7=0000001 | rd ← remainder; sign of nonzero REM = sign of dividend | ÷0: remainder = dividend (no trap). Overflow: remainder = 0. Truncated (C-style) semantics, not floored. | Same advice circuit as DIV — the (q, r) pair is witnessed once and the opcode only selects which half is written (explicit in OpenVM/Ceno's shared core; Jolt's REM sequences copy r instead of q). The dividend-sign rule for r is a constrained sign relation, not a convention. |
MULWR · OP-32 · f3=000 · f7=0000001 | rd ← sext32((rs1[31:0]·rs2[31:0])[31:0]) | Signedness-independent like MUL; upper input bits ignored; never traps. | 64-bit MUL + word-sign-extension lookup (Jolt lowers it so [17]); nonexistent on the RV32 VMs. |
DIVW / DIVUW / REMW / REMUWR · OP-32 · f3=100/101/110/111 · f7=0000001 | 32-bit division family, results sext32 | ÷0 and overflow rules as the 64-bit forms, applied at 32 bits then sign-extended — including DIVUW/REMUW sign-extending unsigned results, the corner that catches emulator authors. | Jolt's word-division sequences add two obligations beyond §4.9's checklist: prove the advice quotient is already word-sign-extended (extend-and-assert-equal) and the remainder fits the low word — otherwise a 64-bit advice value could satisfy the 32-bit contract while poisoning rd's upper half [18]. |
6.8 · System: MISC-MEM (0001111), SYSTEM (1110011), Zicsr, Zifencei
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
FENCEI · MISC-MEM · f3=000 | Order pred-set accesses before succ-set accesses | Fields fm|pred|succ; fm=1000, pred=succ=RW is FENCE.TSO (always correct to implement as FENCE RW,RW); reserved configs must execute as normal fence; rs1/rd ignored-and-zeroed; fence w,0 encodes the PAUSE hint. CSR reads/writes classify as device I/O for ordering. | On one hart ordering is vacuous, and the four answers to "prove it or refuse it" span the whole design space: proven decoded no-op (Jolt), transpiled NOP (OpenVM), unimplemented-trap (Ceno), illegal-by-omission (Airbender) — §4.10. All are sound; only Jolt's would pass a trace containing the instruction. |
FENCE.II · MISC-MEM · f3=001 (Zifencei) | Order prior stores before subsequent instruction fetches (same hart) | Without it, stores to instruction memory are not guaranteed visible to fetch. Local-hart only — multiprocessor code patching needs more. Moved out of base I into Zifencei. | zkVM programs are committed before proving: self-modifying code is not a semantics question but a commitment question, and every VM here answers it by construction — the fetched program is a fixed table (ROM padded with unprovable sentinels in Airbender; Airbender additionally makes stores into ROM unsatisfiable [60]). FENCE.I therefore has nothing to order; none of the four decodes it. |
ECALLI · SYSTEM · f3=000 · imm=0x000 | Raise environment-call exception | Base ISA defines no register side effects; xepc gets the ECALL's own address (handler must advance); not counted as retired; parameter passing is pure ABI/EEI convention. | The proof's edge of the world (§4.10): modeled M-mode trap into shadow-CSR virtual registers (Jolt), the single syscall door with per-syscall circuits (Ceno), transpiled to terminate — replaced by a custom instruction (OpenVM), or undecodable (Airbender, whose oracle/delegation CSRs take over the role). The "environment" is the host; no VM proves the environment. |
EBREAKI · SYSTEM · f3=000 · imm=0x001 | Raise breakpoint exception | No register side effects; xepc points at the EBREAK; the semihosting idiom brackets it with uncompressed slli/srai HINT markers. | No debugger exists inside a proof: self-jump-as-halt (Jolt), unimplemented-trap (Ceno), terminate-with-panic-code (OpenVM), undecodable (Airbender). A guest hitting EBREAK is a liveness event for the host, never a provable trap frame. |
CSRRW / CSRRS / CSRRCI · SYSTEM · f3=001/010/011 | Atomic CSR read-modify-write / set-bits / clear-bits | The gating matrix is normative: CSRRW rd=x0 ⇒ no read (or read side effects); CSRRS/C rs1=x0 ⇒ no write at all (so csrr/csrs on read-only CSRs are legal); illegal-instruction on absent CSR, bad privilege, or write to read-only. 12-bit csr field is an address, not an immediate. | No VM has 4096 CSRs. Jolt maps a 6-CSR machine-mode subset to shadow virtual registers and expands CSRRW/CSRRS to register moves honoring read-before-write; unmapped CSRs are rejected at expansion [19]. Airbender whitelists per-index via a 212 properties table — supported means "oracle or delegation doorway," and an unlisted CSR makes the trace unsatisfiable (its ROM padding word exploits exactly this) [62][53]. Ceno and OpenVM refuse the whole family (trap / unimp, one OpenVM NOP carve-out for csrrw x0,cycle,x0-style counter resets [49]). |
CSRRWI / CSRRSI / CSRRCII · SYSTEM · f3=101/110/111 | Immediate forms; uimm[4:0] in the rs1 field | The 5-bit uimm is zero-extended — one of the ISA's only unsigned immediates. Gating: CSRRWI rd=x0 skips the read; CSRRSI/CI uimm=0 skip the write. | Same story as the register forms; none of the four VMs decodes the immediate variants at the pinned commits (Jolt's decoder accepts CSRRW/CSRRS only [6]; Airbender's immediate-form gates are compiled false [62]). Compilers emit them rarely outside handler prologues — which zkVM guests don't have. |
6.9 · A extension (AMO = 0101111; f3 = 010 (.W) / 011 (.D); Zalrsc + Zaamo)
Context for every row: of the four VMs only Jolt decodes this opcode space [6] — elsewhere atomics are a build-time rejection and a toolchain problem (the lower-atomic story of our companion survey). Alignment here is mandatory: unlike ordinary loads/stores, a misaligned LR/SC always raises an exception, and a misaligned AMO does too absent the optional misaligned-atomicity-granule PMA [3]. On a single-hart, non-preemptive machine, every atomicity and ordering guarantee below is discharged by sequential execution itself; what remains provable is the data flow and the reservation logic.
| Instruction | Semantics | Spec-mandated edge cases | Proving considerations |
|---|---|---|---|
LR.W / LR.DR · AMO · funct5=00010 · rs2=0 | rd ← sext(mem[rs1]); register reservation over the accessed bytes | rs2 field must be 0. One reservation per hart; any LR or SC replaces/invalidates it. aq/rl ordering bits; rl-without-aq discouraged. | Jolt materializes the reservation as architectural state: a virtual register records the address (asserted to point into RAM), the sibling-width reservation is cleared, then an ordinary load runs [20]. The reservation becomes just more state under the same memory argument — no special machinery. |
SC.W / SC.DR · AMO · funct5=00011 | If valid reservation covers the bytes: store rs2, rd ← 0; else no store, rd ← nonzero (canonically 1). Reservation invalidated either way | Spurious failure is architecturally permitted. Must fail if the address lies outside the most recent LR's reservation set (the set may exceed the bytes accessed, so a different address inside it may legally succeed), if another hart's store to the set intervenes, or if any SC intervenes. Constrained ≤16-instruction LR/SC loops carry an eventual-success (livelock-freedom) guarantee. | The permitted spurious failure makes advice the exact encoding of the spec: Jolt witnesses the success bit as Boolean advice, constrains success ⇒ reservation address matches, stores old + success·(rs2−old), and returns the 0/1 [20]. Failure needs no justification — precisely as architected. The forward-progress guarantee is a liveness property; proofs of a finished trace never owe liveness. |
AMOSWAP / AMOADD / AMOXOR / AMOAND / AMOORR · AMO · funct5=00001/00000/00100/01100/01000 | Atomically: rd ← sext(old); mem ← old op rs2 (swap = replace) | rd receives the old value; .W on RV64 sign-extends it and ignores rs2's upper half. Natural alignment mandatory. aq+rl = sequentially consistent. | Lowered to load, op, store, copy-old sequences (Jolt), inheriting each op's own family considerations (§4.1/§4.2); "atomicity reduces to preserving the read-modify-write order" in a sequential trace [20]. Word AMOs on the doubleword-granular memory argument splice one 32-bit lane — the §4.8 sub-word machinery again. |
AMOMIN / AMOMAX / AMOMINU / AMOMAXUR · AMO · funct5=10000/10100/11000/11100 | Atomically: rd ← sext(old); mem ← min/max(old, rs2), signed/unsigned at width | Comparison at operand width (32-bit for .W even on RV64); old value sign-extended into rd even for the unsigned comparisons. | Branch-free blend: compare (§4.4 machinery), then old + take·(rs2−old) — a mux as arithmetic, since a proof cannot "branch" inside a row. The width-of-comparison subtlety (.W compares 32-bit values) rides on the word-lane extraction [20]. |
6.10 · C extension (Zca; quadrants 00/01/10)
Every RVC instruction expands to exactly one base instruction; the table gives the expansion, the encoding constraints (which are the extension's entire semantic content), and what — if anything — is left for a prover. The global answer to the last column: for a VM that statically expands (Jolt, the only acceptor among our four [21]), each row's proving obligations are identical to its expansion's row above, plus exactly two deltas visible in constraints: sequential next-PC is +2, and the linking jumps write PC+2. Reserved code points and the ×4/×8-scaled zero-extended offsets are decode-time facts, checked where decode is checked.
| Instruction | Expands to | Encoding constraints and reserved/HINT points |
|---|---|---|
C.ADDI4SPN rd′,nzuimmCIW · Q0 · f3=000 | ADDI rd′,x2,nzuimm | rd′ ∈ x8–x15; nzuimm = zero-extended multiple of 4 in [4,1020]; nzuimm=0 reserved — and the all-zero 16-bit word, which falls here, is the architecture's designated permanently-illegal instruction. |
C.LW / C.LDCL · Q0 · f3=010/011 | LW/LD rd′,uimm(rs1′) | 3-bit registers (x8–x15); zero-extended offsets scaled ×4 / ×8. C.LD is RV64-only (its code point is C.FLW on RV32+Zcf). |
C.SW / C.SDCS · Q0 · f3=110/111 | SW/SD rs2′,uimm(rs1′) | As C.LW/C.LD; C.SD RV64-only. |
C.ADDI rd,nzimmCI · Q1 · f3=000 | ADDI rd,rd,nzimm | Sign-extended 6-bit nonzero immediate. rd≠x0 with imm=0: HINT. rd=x0 encodes C.NOP (imm≠0 there: HINT). |
C.JAL off (RV32)CJ · Q1 · f3=001 | JAL x1,off | RV32-only — the code point is C.ADDIW on RV64. Links PC+2, consistent with JAL's "next sequential instruction" rule. ±2 KiB range. |
C.ADDIW rd,imm (RV64)CI · Q1 · f3=001 | ADDIW rd,rd,imm | rd≠x0 (rd=x0 reserved); imm may be zero — c.addiw rd,0 is sext.w, unlike C.ADDI's nonzero rule. |
C.LI rd,immCI · Q1 · f3=010 | ADDI rd,x0,imm | Sign-extended 6-bit immediate; rd=x0: HINT. |
C.LUI rd,nzimmCI · Q1 · f3=011 | LUI rd,nzimm (bits 17:12) | rd∉{x0,x2}; imm≠0 (imm=0 reserved); rd=x2 encodes C.ADDI16SP; rd=x0 with imm≠0: HINT. Sign-extends from bit 17. |
C.ADDI16SP nzimmCI · Q1 · f3=011, rd=x2 | ADDI x2,x2,nzimm | Shares C.LUI's code point, selected by rd=x2; immediate = sign-extended multiple of 16 in [−512,496]; nzimm=0 reserved. Exists purely for stack-frame adjustment. |
C.SRLI / C.SRAI rd′,shamtCB · Q1 · f3=100 | SRLI/SRAI rd′,rd′,shamt | rd′ ∈ x8–x15; shamt=0: HINT; RV32 shamt[5]=1: designated custom space. |
C.ANDI rd′,immCB · Q1 · f3=100 | ANDI rd′,rd′,imm | rd′ ∈ x8–x15; sign-extended 6-bit immediate, zero permitted. |
C.SUB / C.XOR / C.OR / C.ANDCA · Q1 · f3=100 | SUB/XOR/OR/AND rd′,rd′,rs2′ | Both registers in x8–x15; no other constraints. |
C.SUBW / C.ADDWCA · Q1 · f3=100 (RV64) | SUBW/ADDW rd′,rd′,rs2′ | RV64-only; reserved on RV32; registers in x8–x15. |
C.J offCJ · Q1 · f3=101 | JAL x0,off | ±2 KiB; no register operands; no link, so no PC+2 subtlety. |
C.BEQZ / C.BNEZ rs1′,offCB · Q1 · f3=110/111 | BEQ/BNE rs1′,x0,off | rs1′ ∈ x8–x15; ±256 B; compares the full XLEN register against zero. The compiler's dominant branch shape — comparisons against zero are most branches. |
C.SLLI rd,shamtCI · Q2 · f3=000 | SLLI rd,rd,shamt | shamt=0 or rd=x0: HINTs; RV32 shamt[5]: custom space. |
C.LWSP / C.LDSP rd,uimmCI · Q2 · f3=010/011 | LW/LD rd,uimm(x2) | rd=x0 reserved (unlike the base LW, where rd=x0 is legal-with-side-effects); zero-extended offsets ×4 (0–252) / ×8 (0–504); C.LDSP RV64-only. |
C.JR rs1CR · Q2 · f4=1000, rs2=0 | JALR x0,0(rs1) | rs1≠x0 (rs1=x0 reserved). The dominant ret encoding. |
C.MV rd,rs2CR · Q2 · f4=1000, rs2≠0 | ADD rd,x0,rs2 | rd=x0: HINT. Spec notes the ADD (not ADDI) expansion choice is deliberate and implementations may treat it as the canonical MV. |
C.JALR rs1CR · Q2 · f4=1001, rs2=0 | JALR x1,0(rs1) — except the link | rs1≠x0 (that code point is C.EBREAK). Links PC+2 where JALR links PC+4 — with RV32's C.JAL, one of the extension's two non-identity expansions — the single fact forcing link-value logic to know instruction length (Jolt's −2·IsCompressed term exists for this row [21]). |
C.ADD rd,rs2CR · Q2 · f4=1001, rs2≠0 | ADD rd,rd,rs2 | rd=x0: HINT (rs2=x2..x5 encode the compressed NTL.* hints). |
C.EBREAKCR · Q2 · f4=1001, rd=rs2=0 | EBREAK | Shares the C.JALR/C.ADD code point at the doubly-degenerate corner. Note the semihosting idiom requires the uncompressed EBREAK, so C.EBREAK is never part of it. |
C.SWSP / C.SDSP rs2,uimmCSS · Q2 · f3=110/111 | SW/SD rs2,uimm(x2) | Full 5-bit rs2, x0 permitted; offsets ×4 / ×8; C.SDSP RV64-only. |
C.NOPCI · Q1 · f3=000, rd=0, imm=0 | ADDI x0,x0,0 | The compressed NOP; imm≠0 variants are HINTs. |
7 · What survives contact with a hardware review
Summing up for the reviewer this piece is addressed to — the seven claims we would defend at a whiteboard:
1. The unprivileged compute semantics are preserved to the bit, and that claim is checkable. Wraparound, shift masking, SLTIU's sext-then-unsigned, division's trap-free degenerate results, DIVUW's sign-extended unsigned quotient, JALR's cleared bit 0, the C.JALR link exception — every one is a constraint or table property in at least one pinned codebase cited here, not a testing outcome. The compliance methodology differs from silicon's in kind: not "ran the suite," but "the constraint system admits no other answer." When we found our own study notes wrong (Airbender's field), the pin settled it in one file read.
2. Compliance ends at the system boundary, uniformly, and honesty requires saying so. No VM here implements the trap model, the CSR file, FENCE's ordering sets, or misaligned access as the manual writes them. The unprivileged spec's own EEI parameterization makes each choice legal — but "we prove RISC-V" always means "we prove an EEI," and the EEIs differ enough that identical guests are portable across these four VMs only at the level our companion article covers (toolchain and runtime), not at the level of raw traces.
3. Illegal instructions mark the deepest divergence: trap versus unsatisfiability. Silicon must define behavior for every bit pattern; a prover may instead make bad patterns unprovable (Airbender constrains the decoder's invalid flag to zero and pads its ROM with a booby-trapped CSRRW; Ceno traps in the emulator so no trace exists; OpenVM and Jolt reject at build). This is sound for the "an honest run of this program" statement a zkVM actually makes — but a hardware reviewer should recognize that the ISA's totality (δ defined on all inputs) has been traded for a partial function plus the meta-claim that the program stays inside it.
4. Nondeterminism is the resource that redraws the price list. Division cheaper than shifting; SC's spurious failure encoded exactly by an advice bit; sub-word extraction steered by witnessed address bits. Every "compute the answer" obligation that silicon carries becomes "check an answer someone already wrote down" — and the residual engineering discipline is that every advice value must be pinned by range checks and defining constraints, because unconstrained advice is not an optimization but a forgery. The division checklist of §4.9 is the pattern in miniature.
5. The microarchitectural vocabulary does not transfer; the invariants do. Ports, predictors, barrel shifters, carry-lookahead, store buffers — none has a referent here. What transfers untouched: x0's hardwiredness (actively enforced in four different ways), the sign-extension invariant of RV64 words, alignment discipline, and the load-store architecture's quarantine of memory traffic — which proves to be the ISA design decision the proving model rewards most.
6. The four architectures are one design space, and it is the control-unit argument again. Universal row + tables (microcode), chip per opcode (hardwired control), chip per class on open buses (chiplet fabric), family multiplexing + delegation (minimal core + co-processors). The forty-year-old trade between uniformity and specialization reappears intact, with constraint-degree and commitment count as the new area and timing.
7. The encyclopedia's per-row lesson compresses to one sentence: an instruction's proving cost is set not by its computational depth but by how much non-algebraic structure — bit selection, ordering, interpretation — its semantics smuggle in. ADD is nearly free; ADDI freer; DIV is advice; but a byte load is a small protocol and a variable shift is a genuine design problem. That inversion, applied instruction by instruction down the encyclopedia, is what it means to implement an ISA under a prover.
References
All GitHub citations are permalinks pinned to full commit SHAs, verified to exist publicly; line numbers refer to the files at those exact revisions and were extracted by reading each repository at its pinned commit during July 27, 2026. Specification citations pin the riscv-isa-manual sources at a main-branch commit fetched the same day; the semantics cited are those of the ratified Unprivileged ISA. The four zkVM pins are the same revisions used by our companion survey of guest compilation, so the two articles cite one consistent snapshot of each codebase.
- RISC-V Instruction Set Manual, Vol. I: Unprivileged ISA — base integer chapters at riscv-isa-manual@4757308 · src/unpriv/rv32.adoc, rv64.adoc, intro.adoc, rv-32-64g.adoc (encoding tables). Immediate layouts, x0, HINTs, alignment and misalignment norms, taken-branch exception rules, W-op sign extension.
- M extension — src/unpriv/m-st-ext.adoc: division-by-zero and overflow semantics table, trap-free rationale, MULH/fusion notes, W-variant sign extension.
- A extension — src/unpriv/zalrsc.adoc, zaamo.adoc, a-st-ext.adoc: reservation rules, SC failure codes, forward-progress constraint, mandatory alignment, aq/rl.
- C extension — src/unpriv/zca.adoc, c-st-ext.adoc: per-instruction expansions, register/immediate restrictions, reserved and HINT code points, the C.JALR pc+2 link note, IALIGN=16 consequences.
- Zicsr — src/unpriv/zicsr.adoc: read/write gating and side-effect table, uimm zero-extension, CSR-vs-FENCE I/O classification.
- Jolt decoder — jolt@e0d5d7e · crates/jolt-program/src/image/decode.rs L15–L74 (major-opcode dispatch), L143–L169 (22 AMO forms), L171–L180 (SYSTEM: exact ECALL/EBREAK/MRET words, CSRRW/CSRRS only), L70–L73 and L524–L526 (unknown encodings fail at image load).
- Jolt instruction inventory — crates/jolt-riscv/src/lib.rs L22–L273: 135 decoded source kinds; L187–L215: the 29 real RV64 opcodes surviving into proven bytecode; instructions/mod.rs L331–L336: kinds deliberately proven only by expansion.
- Jolt field and lookup architecture — crates/jolt-field/src/arkworks/bn254.rs L15–L22 (BN254 scalar field); book · instruction_execution.md L8–L44 (2128-entry tables via prefix-suffix Shout); jolt-lookup-tables/src/traits.rs L93–L110 (interleaved vs combined operands).
- Jolt R1CS — crates/jolt-r1cs/src/constraints/rv64.rs: memory-link rows L272–L318, SUB bias L360–L371, PC/branch/jump rows L414–L505, product constraints L518–L531, 22-constraint/38-variable summary L1–L69, L551–L558; tables/range_check.rs L13–L31 (truncation table); instructions/i/add.rs L3–L8.
- Jolt lookup-table inventory — crates/jolt-lookup-tables/src/tables/mod.rs L115–L156: the 40 tables enumerated.
- Jolt shift expansions — expand/shifts/sll.rs (Pow2+MUL), slli.rs (single MULI row), srl.rs, shared.rs (bitmask immediates), srlw.rs, sllw.rs; tables pow2.rs L10–L17 (mod-XLEN masking), virtual_sra.rs L15–L54 (sign term in the MLE); expand/arithmetic/addw.rs L9–L30 (ADDW lowering).
- Jolt comparison/branch lookups — instructions/riscv/slt.rs L6–L23, beq.rs L6–L21 (per-branch table mapping), tables/equal.rs L14–L33 (product-form equality MLE).
- Jolt jumps — instructions/riscv/jal.rs L6–L34; tables/range_check_aligned.rs L13–L16 (JALR bit-0 clearing in the table); link/next-PC rows in [9].
- Jolt bytecode fetch — book · bytecode.md L3–L54: expanded bytecode as a committed Shout table, dual PC bookkeeping.
- Jolt RAM — book · ram.md L3–L59 (doubleword Twist cells, address remap); sub-word lowerings expand/memory/shared.rs L29–L139 (LB/LH), L478–L550 (SB/SH RMW), lw.rs L7–L53; instructions/i/ld.rs L3–L8 (LD/SD the only memory bytecode).
- Jolt registers — common/src/constants.rs L2–L5 (128 = 32 + 96 virtual); book · registers.md L8–L29 (reservation slots, CSR shadows, scratch layout).
- Jolt multiplication — product constraint and UpperWord extraction in rv64.rs L373–L379 and tables/upper_word.rs L13–L16; MULH sign-correction expansion expand/arithmetic/mulh.rs L3–L63.
- Jolt division — expand/division/shared.rs L3–L45 (advice entry), L113–L263 (64-bit signed sequence), L286–L402 (word variants' extra asserts); divu.rs L3–L69; tables valid_div0.rs, virtual_change_divisor.rs, valid_unsigned_remainder.rs; instructions/virt/advice.rs L3–L8 (advice is range-checked and rd-written).
- Jolt system ops — expand/control_flow/ecall.rs L3–L66 (M-mode trap entry), ebreak.rs (JAL+0 self-jump), mret.rs, csrrs.rs L3–L77 (shadow-register moves, UnsupportedCsr); instructions/i/fence.rs L3–L6 (FENCE proven as flagless no-op).
- Jolt atomics — AMO lowering and branch-free min/max expand/memory/shared.rs L176–L319; lrw.rs L5–L47 (reservation registers); scw.rs L3–L90 (Boolean-advice success, conditional store).
- Jolt compressed — crates/jolt-riscv/src/uncompress.rs L1–L12 (static expansion before decode); flags.rs L47–L48 (IsCompressed flag), flag derivation lib.rs L335–L356.
- Ceno instruction set and rejections — ceno@cb03807 · ceno_emul/src/rv32im.rs L168–L219 (InsnKind enum), L298–L310 (Invalid-category trap); disassemble/mod.rs L348–L430 (EBREAK/CSR/FENCE/MRET/WFI → unimp; 4-byte ELF walk, no RVC).
- Ceno chip-per-opcode registration — ceno_zkvm/src/instructions/riscv/rv32im.rs L282–L338 (one circuit per InsnKind), L426–L435 (table circuits).
- Ceno field and limbs — e2e.rs L102–L128 (BabyBear default, Goldilocks alternative); constants.rs L24–L37 (2×16-bit limbs, PC_BITS=30); Cargo.toml L77–L88 (u16limb default); uint.rs L102–L156 (limb range checks on creation).
- Ceno ADD/SUB — instructions/riscv/arith.rs L55–L99: carry-witnessed add; SUB as re-arranged addition.
- Ceno logic — logic/logic_circuit.rs L46–L133 (byte-wise table constraint); gkr_iop/src/tables/ops.rs L3–L91 (216-row And/Or/Xor/Ltu tables).
- Ceno shifts — shift/shift_circuit_v2.rs L26–L198 (one-hot markers, mod-32 masking, XOR-extracted sign; comments crediting the OpenVM core), L431–L480 (immediate wrapper); shift_circuit.rs L55–L141 (v1 pow2 design).
- Ceno comparisons and branches — slt/slt_circuit_v2.rs L47–L84; gadgets/signed_limbs.rs L45–L149 (one-hot diff marker, biased MSB witnesses); branch/branch_circuit_v2.rs L56–L130 (inverse-marker equality); b_insn.rs L29–L90 (two-arm next-PC).
- Ceno jumps — jump/jal_v2.rs L59–L97; jalr_v2.rs L59–L135 (align2 target, range-checked link); j_insn.rs L19–L56 (target pinned via program record); emulator bit-0 clear rv32im.rs L320–L357.
- Ceno LUI/AUIPC — lui.rs L51–L97, auipc.rs L29–L110 (default circuits); disassemble/mod.rs L256–L335 (Goldilocks build transpiles both into widened ADDI, PC folded in).
- Ceno MUL family — mulh/mulh_circuit_v2.rs L55–L211: schoolbook limb product, 18-bit carry range checks, sign-extension witnesses per signedness variant.
- Ceno division — div/div_circuit_v2.rs L31–L363 (advice q/r, recomposition, divisor-zero forcing q limbs to 0xFFFF, |r|<|d| one-hot comparison, sign constraints), L623–L699 (special-case witness enumeration incl. signed overflow); emulator semantics rv32im.rs L392–L419.
- Ceno memory — memory/load_v2.rs L61–L171 (per-width circuits, limb/byte muxing, sign extension), store_v2.rs L59–L136 (RMW splice); insn_base.rs L684–L800 (MemAddr alignment gadget); emulator misalignment traps rv32im.rs L465–L547.
- Ceno registers/timestamps — insn_base.rs L26–L59 (state in/out records); tracer.rs L127–L131 (four subcycle ticks); gkr_iop/src/circuit_builder/ram.rs L12–L119 (paired records, prev_ts < ts under 29 bits); gadgets/is_lt.rs L181–L212; tables/ram.rs L26–L252 (init/final tables); vm_state.rs L19 (33 registers, RD_NULL sink).
- Ceno program table — tables/program.rs L85–L228 (fixed decoded columns, per-kind immediate encodings, multiplicities); fetch lookups r_insn.rs L31–L65.
- Ceno ECALL/precompiles — ecall_insn.rs L18–L70; ecall/halt.rs L44–L75 (exit code public, next_pc = 0); circuit fleet and dispatch rv32im.rs L340–L424, L924–L1044; syscalls.rs L15–L62 (SP1-compatible codes).
- OpenVM opcodes/transpilation — openvm@425913b · transpiler/src/instructions.rs L30–L266 (38 VM opcodes with class offsets); rrs.rs L20–L363 (1-to-1 transpile); transpiler.rs L45–L75 (32-bit-word consumption; unrecognized → ParseError at build).
- OpenVM executors — circuit/src/extension/mod.rs L94–L173: 10 Rv32I + 3 Rv32M + HintStore chips; LOADB/LOADH routing.
- OpenVM field/limbs/address spaces — docs · specs/openvm/isa.mdx L30–L44 (Baby Bear 15·227+1), L113–L131 (registers = address space 1, pointers [0,128)); instructions/src/riscv.rs L1–L9 (4×8-bit limbs); program.rs L13–L17 (PC_BITS=30).
- OpenVM x0 handling — transpiler/src/util.rs L24–L47 (rd=x0 → NOP for side-effect-free ops); adapters/rdwrite.rs L37–L101 (needs_write); adapters/loadstore.rs L145–L158 (suppression only for loads to x0 — the access still happens).
- OpenVM memory argument — docs · architecture/memory.mdx L18–L106 (BEGKN92 offline checking, timestamps in [1,229], split/merge access adapters); offline_checker/bridge.rs L297–L307 (per-access timestamp ordering); bus.rs L77–L103; volatile/mod.rs L45–L58 (boundary rows).
- OpenVM ALU and bitwise lookup — base_alu/core.rs L93–L139 (boolean limb carries; OR/AND via XOR identities; XOR lookup as range check); bitwise_op_lookup/mod.rs L30–L80 (28×28 preprocessed table, dual multiplicities).
- OpenVM shifts — shift/core.rs L108–L213: one-hot bit/limb markers, forced bit_multiplier, mod-32 masking range check, XOR-verified sign bit.
- OpenVM comparisons/branches — less_than/core.rs L95–L147 (one-hot diff marker, biased MSB witnesses); branch_eq/core.rs L79–L112 (inverse-marker equality; two-arm to_pc); branch_lt/core.rs L100–L164; adapters/branch.rs L67–L99.
- OpenVM jumps and upper-immediates — jal_lui/core.rs L88–L118 (shared chip; XOR-trick PC cap); jalr/core.rs L90–L149 (witnessed-and-dropped LSB, range-bounded target and link); auipc/core.rs L80–L152; transpile-time immediate pre-shifts rrs.rs L248–L285.
- OpenVM multiplication — mul/core.rs L73–L92 (schoolbook convolution, RangeTupleChecker pairs); mulh/core.rs L92–L156 (dual carry chains, sign-extension limbs); tuple-checker sizing extension/mod.rs L78–L80.
- OpenVM division — divrem/core.rs L122–L314 (advice q/r, carry-checked recomposition, zero_divisor and r_zero flags, |r|<|c| one-hot comparison, sign constraints), L586–L610 (signed-overflow execution path).
- OpenVM loads/stores — adapters/loadstore.rs L170–L250 (aligned-block adapter, divisibility alignment check, pointer bounds); loadstore/core.rs L25–L48 ((opcode, shift) as internal opcodes); load_sign_extend/core.rs L109–L150; misalignment contract riscv-custom-code.mdx L45–L50.
- OpenVM system boundary — transpiler/src/lib.rs L48–L62 (ECALL/EBREAK/CSR → unimp; the CSRRW-counter-reset NOP carve-out); util.rs L13–L58 (i12→u24 sign-extension), L151–L166 (unimp = TERMINATE code 2); custom system opcodes riscv-custom-code.mdx L31–L43 and guest/src/lib.rs L12–L26; FENCE → NOP rrs.rs L359–L362; hint writes byte-checked hintstore/mod.rs L169–L219.
- OpenVM fetch and buses — arch/execution.rs L394–L455 (execution bus, default PC step); program/bus.rs L26–L48 (fetch lookup keyed pc+opcode+operands); program/trace.rs L38–L72 (cached program commitment); connector/mod.rs L171–L178 (initial/final PC as public values).
- Airbender operations and machines — zksync-airbender@7e05fe2 · cs/src/devices/risc_v_types.rs L18–L59 (36 executor operations); full_isa_with_delegation_no_exceptions/mod.rs L23–L39 (13 op-family circuits); ops/constants.rs L1–L12 (no FENCE/AMO major opcodes exist).
- Airbender decoder — cs/src/machine/mod.rs L271–L348 (217-entry opcode‖funct3‖funct7 bitmask table); decoder/decode_optimized_must_handle_csr.rs L47–L161 (chunk decomposition + fixed check tables), L192–L259 (immediate assembly), L287–L394 (flag allocation).
- Airbender illegal = unsatisfiable — decode_and_read_operands.rs L147–L157 (is_invalid constrained to zero under trusted code); docs/philosophy_and_logic.md L7; machine/mod.rs L29–L32 (UNIMP = 0xc0001073 = csrrw x0, cycle, x0; NON_DETERMINISM_CSR = 0x7c0).
- Airbender field — field/src/base.rs L14–L24 (Mersenne31, order 231−1); cs/src/types.rs L905–L919 (Register = 2×16-bit range-checked limbs); philosophy_and_logic.md L11–L14 (degree-≤2 AIR, ~222-cycle circuit chunks).
- Airbender ADD/SUB and relation pooling — cs/src/devices/utils.rs L6–L79 (AddSubRelation); optimization_context.rs L755–L871 (shared relation slots selected by orthogonal exec flags).
- Airbender bitwise ops — ops/binops.rs L98–L219 (four byte-table queries, funct3-indexed; range checks come free from the byte tables); definitions/table_type.rs L4–L7 (AND=7, OR=6, XOR=4 = the funct3 values).
- Airbender shifts — ops/shift.rs L142–L265 (AND-table masking, two ShiftImplementation queries, SRA filler, rotates todo!()); tables tables.rs L1609–L1650 (222-entry shift table) and L1200–L1240 (SRASignFiller).
- Airbender conditional family — ops/conditional.rs L23–L253 (SLT*+branches unified; borrow+is-zero; taken-misaligned unprovable; branches write 0 to x0); tables.rs L1242–L1383 (128-entry resolver); cs/circuit.rs L583–L611 (inverse-witness is-zero).
- Airbender jumps — ops/jump.rs L24–L125 (JAL/JALR one family; cleanup lookup clears bit 0, bit 1 unsatisfiable); tables.rs L1123–L1146 (JumpCleanupOffset).
- Airbender memory and upper-immediates — ops/load.rs L160–L355 (alignment unsatisfiability, ROM/RAM split, query repurposing), store.rs L180–L273 (no stores into ROM; byte/halfword splice), lui_auipc.rs L54–L148; tables L1386–L1416 (ROM/RAM separator) and L1735–L1801 (combined sub-word extension lookup).
- Airbender multiply/divide — ops/mul_div.rs L37–L68 (one MUL family), L272–L373 (advice-based DIV/REM, remainder bound via borrow), L499–L545 (overflow witness path), L580–L691 (signed remainder logic — verified by the in-tree z3/div_optim_signextension.z3 script — and div-by-zero quotient pinning); optimization_context.rs L37–L47 and L1104–L1400 (MulDivRelation and its chunked carry constraints).
- Airbender CSR/delegation — ops/csr.rs L35–L89 (CSRRW only; ECALL/EBREAK undecodable); csr_with_delegation.rs L31–L102 (whitelist trap, delegation requests, range-checked oracle reads); csr_properties.rs L4–L41; docs/subarguments_used.md L48–L62 (set-equality delegation argument); delegation ids u256_ops_with_control.rs L6–L7.
- Airbender registers/memory argument — minimal_state.rs L6–L33 (PC is the only persistent state); decode_and_read_operands.rs L161–L240 (3 shuffle-RAM queries, fixed in-cycle timestamps; write-side range checking induction L171–L173); ram_access.rs L3–L18; constants.rs L7–L54 (2×19-bit timestamps, 236-cycle budget); writeback_no_exceptions.rs L38–L101 (is-zero x0 masking); subarguments_used.md L15–L46 (read/write-set permutation argument, lazy init/teardown).
- Airbender fetch/ROM/variants — machine/utils.rs L50–L127 (next-PC carry constraint), L253–L282 (ROM-range fetch lookup); machine_configurations/mod.rs L125–L215 (ROM table padded with UNIMP); devices/diffs.rs L9–L110 (PC selection; PC_INC_STEP=4, no RVC); …no_signed_mul_div/mod.rs L22–L38 (shipped variant drops MULH/MULHSU/DIV/REM); ops/mop.rs L30–L106 (Mersenne31 field ops in SYSTEM space); risc_v_simulator/src/cycle/state.rs L1139–L1158 (FENCE arm commented out — illegal).
