Skip to main content

ISA & processor

This page describes the ZisK instruction set and the machine that runs it. It covers why ZisK wraps RV64IMA in a proving-friendly machine model with four registers and a fixed (c, flag) = op(a, b) instruction shape, where operands come from and results go, the copyb shim for loads and stores, the VM's state and ROM, how a single instruction is emulated, phantom instructions, initial state and termination, and the opcode catalogue.

The previous pages described the proof system from the outside in: the lifecycle, the trust boundary, the chips, and how the work scales. This page steps back to the level underneath all of that, the ZisK ISA and the processor model that implements it.

Why this design?

RISC-V is a physical ISA shaped by hardware constraints, optimised for execution speed on actual silicon. Among other things, it carries thirty-two general-purpose registers because flip-flops are cheap and the compiler benefits from having lots of them around.

ZisK is a virtual ISA doesn't target hardware, so it doesn't have hardware's reasons for those choices. What it does have is a proving system that turns every register read, every memory access, and every operand into a polynomial constraint. A loose, general-purpose layout in that setting is expensive.

GoalWhat it costs in a hardware ISAWhat it costs in ZisK
Compatibility with existing toolchainsConstrains how registers, memory, and instruction encodings can look.Achieved by mirroring RV64IMA on the front end and transpiling into ZisK's own shape on the back.
Proving efficiencyDoesn't matter — never proved.The dominant constraint. Every choice gets re-evaluated through "how expensive is this in a proof?"
32 general-purpose registersCheap (flip-flops). Compiler gets flexibility, instructions get short.Expensive as six degrees of freedom per instruction in the constraint system.
4 special-purpose registersFewer slots for the compiler; you'd need spills.Cheap as every instruction lands in the same row shape.

ZisK ends up with two layers stacked on top of each other:

  • A front end that looks like RV64IMA at the source level, so a Rust crate compiles for ZisK with the standard toolchain.
  • A machine model that the proving system actually constrains — four registers, one fixed instruction shape, and a small set of source/store options that wire those registers to memory and to the RISC-V register file the front end expects.

The pages above talked about what gets proved. This one is about what gets proved against what — the instruction contract, and the machine that runs it.

Compatibility with RISC-V

The ZisK ISA is anchored to RV64IMA, RISC-V's 64-bit base plus its three core extensions:

  • I — base integer instructions
  • M — integer multiplication and division
  • A — atomic memory instructions

64-bit registers and 32-bit-aligned instruction encoding are the defaults across the toolchain. On top of that, three optional extensions are supported so compilers beyond Rust (C, C#, Go, …) can target ZisK without modification:

ExtensionWhat it addsWhat it costs in ZisK
FSingle-precision floating-point.Implemented in a software float library; float operations are expensive to constrain. Use sparingly.
DDouble-precision floating-point.Same as F — software emulation, slow.
CCompressed (16-bit) instructions.Useful for hardware-RISC-V code locality. ZisK doesn't consume RISC-V bytes directly, so almost no proving impact.

Precompile calls — SHA-256, Keccak-256, secp256k1 ECDSA, and the rest — ride on the standard RISC-V ecall mechanism with a fixed calling convention. The transpiler recognises the convention and maps each call to the matching ZisK precompile opcode. From the guest program's perspective, calling a precompile looks identical to calling any other Rust function.

The instruction shape

Every ZisK instruction performs (c, flag) = op(a, b) with a, b, c ∈ [0, 2^64) and flag ∈ {0, 1}. The instruction is, in effect, a single execution step transforming two 64-bit inputs into a 64-bit output and a boolean. That fixed shape is what makes proving cheap: every instruction lands in the same row layout, with the same constraint template covering every opcode.

The four registers

ZisK exposes four 64-bit registers, each with a fixed role:

RegisterRole
aFirst operand. Always one of the inputs to the current operation.
bSecond operand. Always the other input.
cResult. Whatever the operation produces.
flagOne-bit side-channel result. Branch decisions and a few comparisons surface here.

Where a and b come from, and where c ends up, are part of the instruction's encoding rather than its opcode. The constraint for any given operation only ever has to talk about the values in these four registers — it never has to enumerate which of 32 RISC-V registers happened to be involved.

The 32 RISC-V registers are still there. The ISA addresses them inside the same numerical address space as data memory (more on that in Transpilation) as a load or store targeting the register region behaves exactly as if it targeted RAM, which keeps the ISA uniform for compiler-emitted code. Under the hood, ZisK handles register accesses inside the Main chip rather than routing them through the Memory Bus; that detail is on Specialized chips, and it's invisible to the program either way.

The flow looks like this. Inputs feed a and b, the operation produces c and flag, and the result is routed to wherever the instruction says it should go:

For comparison, the same picture for a plain RISC-V processor — three input sources, one output destination, but the operand layout is open-ended and the register file is wide:

The ZisK register file is essentially a wrapper around the RISC-V one: every RISC-V x register is still reachable, but the operation itself is always expressed in terms of a, b, c, and flag. The same constraint template covers every instruction regardless of which x registers it happened to touch.

The copyb shim

The fixed c = op(a, b) shape creates one immediate problem: plain loads and stores have no obvious place. There's no "operation" being performed when a program copies a memory location into a register, but the encoding still expects an opcode.

ZisK solves this with a tiny instruction called copyb. It is literally c = b — read b from wherever the instruction says, then copy it through c to wherever the instruction wants it stored. A RISC-V load from address 0x1000 into x5 becomes a copyb whose b source is memory and whose c store is a RISC-V register, with no actual computation involved.

The shim also has a strategic payoff: because the operand and destination paths are decoupled from the register file itself, ZisK could in principle wrap a different front-end ISA in the future without disturbing the constraint shape. Today it wraps RISC-V; the same machinery could wrap something else later.

Where operands come from and results go

Each instruction has to declare where its operands come from and where its result goes. The encoding picks one option from each menu, and the constraint template covers them all uniformly.

Where a comes from

a is the first operand, used by every instruction that performs computation. The encoding picks one of five sources:

SourceMeaning
ImmediateA constant baked into the instruction.
MemoryA read from a memory address that is constant for this instruction.
RegisterA read from a RISC-V register whose index is constant for this instruction.
cThe result of the previous instruction, threaded directly into the next without a write-back.
StepThe current step counter, used by instructions that need to know their position in the trace.

The "c" source is the one that ties consecutive instructions together cheaply, when the next operation needs the previous result, no intermediate write to memory or to a RISC-V register is required.

Where b comes from

b is the second operand. It has the same first three sources as a, but instead of the "c" and "Step" options it gains a sixth that turns out to be load/store's bread and butter:

SourceMeaning
ImmediateA constant baked into the instruction.
MemoryA read from a memory address that is constant for this instruction.
RegisterA read from a RISC-V register whose index is constant for this instruction.
Indirect memoryA read from memory at address a + const, with a width of 1, 2, 4, or 8 bytes (also part of the encoding). This is what a base-pointer-plus-offset load compiles down to.

The indirect-memory source is what makes the addressing modes useful in practice. Combined with copyb, it covers everything a RISC-V load does in a single ZisK instruction.

Where c ends up

The result c is written to one of three destinations:

StoreMeaning
MemoryWritten to a memory address that is constant for this instruction.
RegisterWritten to a RISC-V register whose index is constant for this instruction.
Indirect memoryWritten to memory at address a + const, with the same 1/2/4/8-byte width options as the indirect read.

The result either lives in memory, in a RISC-V register the front end expects to see, or it's picked up by the next instruction through the "c" source above.

Where pc comes from

The program counter pc advances after every instruction. Three rules cover every control-flow case:

RuleWhen it applies
pc ← c + constRegister-relative jump — the target is computed from the result.
pc ← pc + const1Conditional branch when flag == 1.
pc ← pc + const2Conditional branch when flag == 0. For unconditional sequencing, the instruction sets const1 == const2.

All three offsets are constants encoded in the instruction, so control flow lands in the same fixed row shape as everything else.

State

The VM's state is everything that changes from one step to the next. ZisK splits it cleanly between guest and host:

ComponentSymbolMutable?Held by
Program ROM(code + data)Noguest
Program counterpc_tYesguest
Previous resultc_tYesguest
Previous flagflag_tYesguest
Data memorymem_t[]Yesguest
Input streamin[]No (consumed)host
Hint streamhint[]No (consumed)host

Formally, the guest state at step t is

S_t = (pc_t, c_t, flag_t, mem_t[])

and the host state is H = (in[], hint[]). c_t and flag_t are part of the state because the next instruction may consume them — a can take the "c" source mode, and the next-pc rules read flag directly. Without them in state, the single-step transition wouldn't be self-contained.

Public outputs are not a separate component. They live in a designated region of writable RAM (covered on Transpilation) — the guest writes to that region through ordinary memory stores during execution, and when the run terminates, the contents of that region are extracted from mem_T[] and exposed as the proof's public output. The buffer is filled in place, not accumulated as a separate per-step stream.

The Trust boundary page covered the host/guest split in detail; the rest of this section focuses on the parts the ISA exposes.

Program ROM

A ZisK program is two things stitched together:

  • Program code — a map from 32-bit instruction addresses to ZisK instructions, loaded before execution begins.
  • ROM data — an immutable segment holding program constants (string literals, lookup tables, static data baked into the binary).

Both are determined entirely by the compiled binary and are identical across every execution of that binary. The two parts are committed and validated by different mechanisms:

PartHow it's bound to the proof
Program codeHashed into a program commitment included in the verifier's context. The Main chip queries this committed table on every fetch via the ROM Bus. Substituting an instruction invalidates the commitment.
ROM dataTreated as an immutable memory region. Written once at initialisation; thereafter only reads are permitted. The immutability is enforced by the arithmetization, not the ISA — a read from ROM data looks like any other read.

Input and hint streams

The host loads two queues into the VM:

  • Input stream.
  • Hint stream.

Both streams are consumed by copying data into guest memory via explicit instructions (for example dma_inputcpy for the input stream). Anything received from the host is untrusted until the guest has verified it.

How an instruction is emulated

With the contract specified and the machine model fixed, every instruction goes through the same five-stage step. The emulator loops over this step until the program halts:

pc = 0x1000 // start address
for step in 0..max_step
inst = rom.get_inst(pc) // fetch
a = source(inst.source_a) // resolve a
b = source(inst.source_b) // resolve b
c, flag = inst.op(a, b) // operate
store(inst.store_c, c) // commit c
if inst.end break
pc = inst.set_pc(flag) // advance pc

The same five things happen for every instruction the program contains:

Formally, the single-step transition is

S_{t+1} = Step(S_t, hint_t[])

where hint_t[] are the hint values supplied by the host at step t. The function Step is fully determined by the ZisK ISA — it decodes the instruction at pc_t, evaluates the operation, updates mem_{t+1}[], and sets pc_{t+1} according to the instruction's control-flow rule.

Putting all the choices together

The full picture of an instruction step is a fan-in of source options on the left, an operation in the middle, and a fan-out of store and next-pc options on the right:

The diagram is busy on first read, but the underlying picture is the same one as before: each instruction picks a source for a, a source for b, an operation, a store for c, and a rule for the next pc. Everything in the constraint system is anchored to that template.

Phantom instructions

Most instructions correspond to a single provable opcode and generate a proof obligation for that operation. Some, however, are phantom instructions: they leave the guest state unchanged — they're no-ops from the VM's perspective — but may trigger side effects in the host state (advancing the input or hint streams, emitting debug information).

Phantom instructions appear in the main execution trace but carry no proof obligation of their own. Two notable cases:

  • Precompile dispatch — instructions that implement the calling convention for precompiles. What gets proven is the precompile computation itself, not the bookkeeping rows in the Main trace.
  • Halting — the halt instruction terminates the emulator immediately with an error condition. Its opcode cannot appear in a valid execution trace; if it is reached, proof generation fails. It exists as a sentinel for invalid or unreachable code paths.

Initial state and termination

Execution begins at a designated boot address with memory initialised from the program binary:

S_0 = (pc_0, 0, 0, mem_0[])

where mem_0[] reflects the pre-loaded ROM data and input data, with all writable memory otherwise zeroed. Specifically:

  • The ROM data region is populated with program constants from the binary.
  • The input data region is loaded from the public input stream.
  • All RISC-V registers (memory-mapped at the start of the system region) are set to zero.
  • All other writable memory is zeroed.

A T-step execution is successful if every transition is valid and the final program counter reaches a designated exit address. It fails if any step violates an ISA invariant:

  • An invalid opcode is encountered.
  • An out-of-bounds memory access is performed.
  • The halt instruction is reached.
  • The program counter never reaches the exit address.

A failed execution cannot be proved. When a run terminates successfully, the public-outputs region of mem_T[] is extracted and exposed as the proof's public data.

Opcode catalogue

The full opcode list lives in zisk_ops.rs. At a glance, instructions group into the following categories:

CategoryExamplesHandled by
Internalflag, copyb, haltMain chip directly.
Fcallfcall_param, fcall, fcall_get — the precompile calling convention.Phantom in Main; the precompile chip proves the actual work.
Puboutpubout — emit a public output.Main chip; output region.
Binary (64-bit + 32-bit _w)add, sub, and, or, xor, lt/ltu, eq, min/max, …Binary chip.
BinaryExsll, srl, sra, signextend_*.Binary-extended chip.
ArithAm32mul, mulh, mulu, div, rem (and 32-bit variants).Arith chip.
ArithA3232-bit divu_w, remu_w, div_w, rem_w.Arith chip.
DMAdma_memcpy, dma_memcmp, dma_inputcpy, dma_xmemcpy, dma_xmemcmp, dma_xmemset.DMA precompile.
BigIntadd256.BigInt precompile.
ArithEqarith256, arith256_mod, secp256k1_*, secp256r1_*, bn254_curve_*, bn254_complex_*.256-bit arithmetic precompile.
ArithEq384arith384_mod, bls12_381_curve_*, bls12_381_complex_*.384-bit arithmetic precompile.
Keccakkeccak.Keccak precompile.
Sha256sha256 (one compression).SHA-256 precompile.
Blake2blake2 (one round of BLAKE2b compression).BLAKE2 precompile.
Poseidon2poseidon2.Poseidon2 precompile (ZK-friendly hash).

Each precompile category corresponds to an independent chip covered on the Specialized chips page. Adding a new precompile means defining a new chip and a new opcode group; existing categories are untouched.

Hard limits

The ISA is bounded — maximum input size, maximum ROM size, maximum program length, and so on. The full table, with how much of each limit a real workload (Ethereum-block validation) actually consumes, is on the Limits page.

Where this picks up

You now know what a ZisK program is in contract terms (a two-stream input model, a committed ROM split into code and data, an explicit execution model, a catalogued opcode set) and how the machine actually runs it (four registers, the (c, flag) = op(a, b) shape, the source/store options, the five-stage step). The next page, Transpilation, shows how the RISC-V binary you compiled becomes the ZisK ROM that this machine actually runs against, and where everything lives in the address space at runtime.