Skip to main content
The Minichain VM supports 40+ opcodes organized into functional categories. Each opcode has a unique byte encoding and specific operand format.

Opcode Format

Opcodes are encoded as:
[opcode_byte] [operands...]

Register Encoding

Registers are packed into bytes using 4-bit fields:
  • Single register: [opcode, RRRR____] (1 register in upper nibble)
  • Two registers: [opcode, RRRR_SSSS] (2 registers, 4 bits each)
  • Three registers: [opcode, DDDD_SSS1, SSS2____] (3 registers across 2 bytes)
Register fields use 4 bits (0-15) to encode R0-R15. The underscore _ represents unused bits (typically zero).

Opcode Categories

Control Flow

7 opcodes for program control

Arithmetic

6 opcodes for math operations

Bitwise

6 opcodes for bit manipulation

Comparison

7 opcodes for comparisons

Memory

6 opcodes for RAM access

Storage

2 opcodes for persistent storage

Immediate

2 opcodes for constants

Context

6 opcodes for execution context

Debug

1 opcode for debugging

Complete Opcode Table

Control Flow (0x00-0x0F)

OpcodeByteFormatSizeGasDescription
HALT0x00HALT10Stop execution successfully
NOP0x01NOP10No operation
JUMP0x02JUMP Rt28Jump to address in Rt
JUMPI0x03JUMPI Rc, Rt38Jump to Rt if Rc ≠ 0
CALL0x04CALL1700Call external contract
RET0x05RET10Return from execution
REVERT0x0FREVERT10Revert execution (error)
// Conditional jump
LOADI R0, 100
LOADI R1, 100
EQ R2, R0, R1      // R2 = 1 (true)
LOADI R3, 0x10     // target address
JUMPI R2, R3       // jumps to 0x10

// Unconditional jump
LOADI R0, 0x20
JUMP R0            // jumps to 0x20

Arithmetic (0x10-0x1F)

OpcodeByteFormatSizeGasDescription
ADD0x10ADD Rd, Rs1, Rs232Rd = Rs1 + Rs2
SUB0x11SUB Rd, Rs1, Rs232Rd = Rs1 - Rs2
MUL0x12MUL Rd, Rs1, Rs233Rd = Rs1 × Rs2
DIV0x13DIV Rd, Rs1, Rs235Rd = Rs1 ÷ Rs2
MOD0x14MOD Rd, Rs1, Rs235Rd = Rs1 % Rs2
ADDI0x15ADDI Rd, Rs, imm3262Rd = Rs + imm32
// Basic arithmetic
LOADI R0, 10
LOADI R1, 3
ADD R2, R0, R1     // R2 = 13
SUB R3, R0, R1     // R3 = 7
MUL R4, R0, R1     // R4 = 30
DIV R5, R0, R1     // R5 = 3
MOD R6, R0, R1     // R6 = 1

// Add immediate
LOADI R0, 100
ADDI R1, R0, 50    // R1 = 150
DIV and MOD will raise VmError::DivisionByZero if Rs2 is zero.

Bitwise (0x20-0x2F)

OpcodeByteFormatSizeGasDescription
AND0x20AND Rd, Rs1, Rs232Rd = Rs1 & Rs2
OR0x21OR Rd, Rs1, Rs232Rd = Rs1 | Rs2
XOR0x22XOR Rd, Rs1, Rs232Rd = Rs1 ^ Rs2
NOT0x23NOT Rd22Rd = ~Rd (in-place)
SHL0x24SHL Rd, Rs1, Rs235Rd = Rs1 left shift Rs2
SHR0x25SHR Rd, Rs1, Rs235Rd = Rs1 right shift Rs2
// Bit masks
LOADI R0, 0xFF00
LOADI R1, 0x00FF
AND R2, R0, R1     // R2 = 0x0000
OR R3, R0, R1      // R3 = 0xFFFF
XOR R4, R0, R1     // R4 = 0xFFFF

// Bit manipulation
LOADI R0, 0b1010
NOT R0             // R0 = 0xFFFFFFFFFFFFFFF5
LOADI R1, 5
LOADI R2, 2
SHL R3, R1, R2     // R3 = 20 (5 << 2)
SHR R4, R1, R2     // R4 = 1 (5 >> 2)
Shift amounts are masked to 6 bits (0-63) to prevent undefined behavior.

Comparison (0x30-0x3F)

OpcodeByteFormatSizeGasDescription
EQ0x30EQ Rd, Rs1, Rs232Rd = (Rs1 == Rs2) ? 1 : 0
NE0x31NE Rd, Rs1, Rs232Rd = (Rs1 != Rs2) ? 1 : 0
LT0x32LT Rd, Rs1, Rs232Rd = (Rs1 < Rs2) ? 1 : 0
GT0x33GT Rd, Rs1, Rs232Rd = (Rs1 > Rs2) ? 1 : 0
LE0x34LE Rd, Rs1, Rs232Rd = (Rs1 ≤ Rs2) ? 1 : 0
GE0x35GE Rd, Rs1, Rs232Rd = (Rs1 ≥ Rs2) ? 1 : 0
ISZERO0x36ISZERO Rd22Rd = (Rd == 0) ? 1 : 0
// Comparisons
LOADI R0, 10
LOADI R1, 20
EQ R2, R0, R1      // R2 = 0 (false)
NE R3, R0, R1      // R3 = 1 (true)
LT R4, R0, R1      // R4 = 1 (true)
GT R5, R0, R1      // R5 = 0 (false)
LE R6, R0, R1      // R6 = 1 (true)
GE R7, R0, R1      // R7 = 0 (false)

// Check zero
LOADI R0, 0
ISZERO R0          // R0 = 1 (true)

Memory (0x40-0x4F)

OpcodeByteFormatSizeGasDescription
LOAD80x40LOAD8 Rd, [Ra]23Rd = memory[Ra] (1 byte)
LOAD640x41LOAD64 Rd, [Ra]23Rd = memory[Ra] (8 bytes, LE)
STORE80x42STORE8 [Ra], Rv23memory[Ra] = Rv & 0xFF
STORE640x43STORE64 [Ra], Rv23memory[Ra] = Rv (8 bytes, LE)
MSIZE0x44MSIZE Rd22Rd = memory.size()
MCOPY0x45MCOPY Rd, Rs, Rl33Copy Rl bytes from Rs to Rd
// Store and load
LOADI R0, 0x1000      // address
LOADI R1, 42          // value
STORE64 [R0], R1      // write
LOAD64 R2, [R0]       // R2 = 42

// Copy memory
LOADI R0, 0x2000      // dest
LOADI R1, 0x1000      // src
LOADI R2, 64          // length
MCOPY R0, R1, R2      // copy 64 bytes

// Get memory size
MSIZE R0              // R0 = current size

Storage (0x50-0x5F)

OpcodeByteFormatSizeGasDescription
SLOAD0x50SLOAD Rd, Rk2100Rd = storage[Rk]
SSTORE0x51SSTORE Rk, Rv25,000-20,000storage[Rk] = Rv
// Read from storage
LOADI R0, 5           // key
SLOAD R1, R0          // R1 = storage[5]

// Write to storage
LOADI R0, 5           // key
LOADI R1, 100         // value
SSTORE R0, R1         // storage[5] = 100
SSTORE costs 20,000 gas for new slots and 5,000 gas for updates. Use storage sparingly!

Immediate (0x70-0x7F)

OpcodeByteFormatSizeGasDescription
LOADI0x70LOADI Rd, imm64102Rd = imm64 (64-bit immediate)
MOV0x71MOV Rd, Rs22Rd = Rs
// Load immediate values
LOADI R0, 0x123456789ABCDEF0  // 64-bit constant
LOADI R1, 42                  // small constant

// Move between registers
MOV R2, R0                    // R2 = R0

Context (0x80-0x8F)

OpcodeByteFormatSizeGasDescription
CALLER0x80CALLER Rd22Rd = caller address (first 8 bytes)
CALLVALUE0x81CALLVALUE Rd22Rd = call value
ADDRESS0x82ADDRESS Rd22Rd = contract address (first 8 bytes)
BLOCKNUMBER0x83BLOCKNUMBER Rd22Rd = current block number
TIMESTAMP0x84TIMESTAMP Rd22Rd = block timestamp
GAS0x85GAS Rd22Rd = remaining gas
// Get execution context
CALLER R0          // who called this contract
CALLVALUE R1       // how much value was sent
ADDRESS R2         // this contract's address
BLOCKNUMBER R3     // current block height
TIMESTAMP R4       // current block time
GAS R5             // gas remaining
CALLER and ADDRESS return only the first 8 bytes of the 32-byte address in little-endian format.

Debug (0xF0-0xFF)

OpcodeByteFormatSizeGasDescription
LOG0xF0LOG Rs22Emit Rs to logs
// Log values for debugging
LOADI R0, 42
LOG R0             // logs: [42]

LOADI R1, 100
LOG R1             // logs: [42, 100]

Opcode Encoding Details

Three-Register Format

Most ALU operations use 3 registers:
Byte 0: [opcode]
Byte 1: [DDDD][SSS1]  (Destination + Source1 upper bit)
Byte 2: [SSS2][____]  (Source2)
From crates/vm/src/executor.rs:533:
fn decode_rrr(&self) -> (usize, usize, usize) {
    let b1 = self.bytecode[self.pc + 1];
    let b2 = self.bytecode[self.pc + 2];
    let dst = ((b1 >> 4) & 0x0F) as usize;  // Upper nibble
    let s1 = (b1 & 0x0F) as usize;          // Lower nibble
    let s2 = ((b2 >> 4) & 0x0F) as usize;   // Upper nibble
    (dst, s1, s2)
}

Two-Register Format

Byte 0: [opcode]
Byte 1: [RRRR][SSSS]  (Two 4-bit register fields)

Immediate Format (LOADI)

Byte 0: [opcode = 0x70]
Byte 1: [RRRR][____]  (Destination register)
Bytes 2-9: [imm64]    (64-bit immediate, little-endian)

Instruction Sizes

From crates/vm/src/opcodes.rs:130:
SizeOpcodes
1 byteHALT, NOP, RET
2 bytesJUMP, NOT, LOG, MSIZE, CALLER, CALLVALUE, ADDRESS, BLOCKNUMBER, TIMESTAMP, GAS, MOV, LOAD8, STORE8, LOAD64, STORE64, SLOAD, SSTORE, ISZERO
3 bytesADD, SUB, MUL, DIV, MOD, AND, OR, XOR, SHL, SHR, EQ, NE, LT, GT, LE, GE, MCOPY, JUMPI
6 bytesADDI
10 bytesLOADI

Example Programs

Fibonacci Sequence

// Calculate 10th Fibonacci number
LOADI R0, 0           // fib(0) = 0
LOADI R1, 1           // fib(1) = 1
LOADI R2, 10          // counter
LOADI R3, 1           // constant 1

loop:
  ISZERO R2
  LOADI R4, end
  JUMPI R2, R4        // if counter == 0, exit
  
  ADD R5, R0, R1      // next = fib(n-1) + fib(n-2)
  MOV R0, R1          // shift window
  MOV R1, R5
  SUB R2, R2, R3      // counter--
  
  LOADI R4, loop
  JUMP R4

end:
  LOG R1              // result in R1
  HALT

Factorial

// Calculate 5!
LOADI R0, 5           // n
LOADI R1, 1           // result = 1
LOADI R2, 1           // constant 1

loop:
  ISZERO R0
  LOADI R3, end
  JUMPI R0, R3        // if n == 0, exit
  
  MUL R1, R1, R0      // result *= n
  SUB R0, R0, R2      // n--
  
  LOADI R3, loop
  JUMP R3

end:
  LOG R1              // result = 120
  HALT

Error Conditions

Opcodes can fail with these errors:
  • InvalidOpcode: Unknown byte encountered
  • OutOfGas: Insufficient gas for operation
  • DivisionByZero: DIV or MOD with zero divisor
  • MemoryOverflow: Memory access beyond 1MB limit
  • InvalidJump: Jump to invalid bytecode address
  • Reverted: REVERT instruction executed
From crates/vm/src/executor.rs:12:
pub enum VmError {
    OutOfGas { required: u64, remaining: u64 },
    InvalidOpcode(u8),
    DivisionByZero,
    MemoryOverflow,
    InvalidJump(usize),
    StackUnderflow,
    Reverted,
}

Next Steps

Gas Metering

Learn about gas costs for each opcode

Execution Model

Understand how opcodes are executed

Registers & Memory

Review memory model and register usage

Build docs developers (and LLMs) love