Skip to main content
The Minichain VM uses a register-based architecture with 16 general-purpose registers and linear memory.

Register File

The VM has 16 registers (R0-R15), each holding a 64-bit unsigned integer.

Register Structure

From crates/vm/src/memory.rs:7:
pub const NUM_REGISTERS: usize = 16;

pub struct Registers {
    values: [u64; NUM_REGISTERS],
}
All registers are:
  • General-purpose: No special-purpose registers (unlike some architectures with stack pointers, frame pointers, etc.)
  • 64-bit: Each register stores a 64-bit unsigned integer
  • Zero-initialized: All registers start at 0

Register Operations

// Get value from register R5
let value = registers.get(5);

Register Naming

RegisterNumberUsage
R00General purpose
R11General purpose
R22General purpose
R1515General purpose
Unlike some architectures, Minichain has no calling conventions or reserved registers. All 16 registers are available for any purpose.

Memory Model

The VM has two distinct memory spaces:

RAM (Memory)

Temporary, fast, cheap

Storage (Persistent)

Permanent, slow, expensive

RAM (Volatile Memory)

Linear byte-addressable memory that exists only during execution.

Memory Structure

From crates/vm/src/memory.rs:37:
pub struct Memory {
    data: Vec<u8>,         // Dynamically allocated
    max_size: usize,       // 1MB limit
}

Memory Features

  • Byte-addressable: Access individual bytes or 64-bit words
  • Dynamically allocated: Grows as needed (up to max size)
  • 1MB maximum: Default limit is 1,048,576 bytes
  • Zero-initialized: Unallocated memory reads as 0
  • Volatile: Cleared after execution completes

Memory Operations

// Load single byte
LOAD8 R0, [R1]    // R0 = memory[R1]

// Store single byte
STORE8 [R0], R1   // memory[R0] = R1 & 0xFF
Use for:
  • Character data
  • Boolean flags
  • Small integers

Memory Example

// Store and load a value in memory
let bytecode = vec![
    // LOADI R0, 0x100      (memory address)
    0x70, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    
    // LOADI R1, 42         (value to store)
    0x70, 0x10, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    
    // STORE64 [R0], R1     (write to memory)
    0x43, 0x01,
    
    // LOAD64 R2, [R0]      (read back from memory)
    0x41, 0x20,
    
    // LOG R2               (should log 42)
    0xF0, 0x20,
    
    // HALT
    0x00,
];

Storage (Persistent Memory)

Key-value store that persists across transactions.

Storage Structure

  • Keys: 32-byte fixed-size keys (256 bits)
  • Values: 32-byte fixed-size values (256 bits)
  • Persistent: Data survives after execution ends
  • Expensive: SSTORE costs 5,000-20,000 gas

Storage Interface

From crates/vm/src/executor.rs:558:
pub trait StorageBackend {
    /// Read 32 bytes from storage slot
    fn sload(&self, key: &[u8; 32]) -> [u8; 32];
    
    /// Write 32 bytes to storage slot
    fn sstore(&mut self, key: &[u8; 32], value: &[u8; 32]);
}

Storage Operations

// Read from storage
LOADI R0, 5        // key = 5
SLOAD R1, R0       // R1 = storage[5]

Storage Gas Costs

Storage operations are expensive by design to prevent state bloat.
OperationScenarioGas Cost
SLOADAlways100 gas
SSTORENew slot (zero → non-zero)20,000 gas
SSTOREUpdate existing (non-zero → non-zero)5,000 gas
From crates/vm/src/executor.rs:598:
let cost = if let Some(storage) = &self.storage {
    let current = storage.sload(&key);
    let is_empty = current == [0u8; 32];
    if is_empty {
        GasCosts::SSTORE_SET     // 20,000 gas
    } else {
        GasCosts::SSTORE_RESET   // 5,000 gas
    }
} else {
    GasCosts::SSTORE_SET
};

Memory vs Storage

Use RAM for:
  • Temporary calculations
  • Loop variables
  • Stack frames
  • Buffer data
  • Intermediate results
Advantages:
  • Fast (3 gas per operation)
  • Byte-addressable
  • No size restrictions on individual values
Disadvantages:
  • Volatile (lost after execution)
  • Limited to 1MB

Memory Safety

The VM includes several safety mechanisms:

Overflow Protection

From crates/vm/src/memory.rs:62:
if offset >= self.max_size {
    return Err(VmError::MemoryOverflow);
}

Automatic Expansion

// Memory grows automatically when needed
if offset >= self.data.len() {
    self.data.resize(offset + 1, 0);
}

Zero Reads

// Reading uninitialized memory returns 0
pub fn load8(&self, offset: u32) -> u8 {
    self.data.get(offset as usize).copied().unwrap_or(0)
}

Examples

Example 1: Using Registers

// Swap two registers without a temporary
LOADI R0, 10
LOADI R1, 20
XOR R0, R0, R1    // R0 = 10 ^ 20
XOR R1, R1, R0    // R1 = 20 ^ (10 ^ 20) = 10
XOR R0, R0, R1    // R0 = (10 ^ 20) ^ 10 = 20

Example 2: Memory as Stack

// Implement a simple stack
LOADI R15, 0x1000     // Stack pointer (SP)

// Push 42
LOADI R0, 42
STORE64 [R15], R0
ADDI R15, R15, 8

// Push 99
LOADI R0, 99
STORE64 [R15], R0
ADDI R15, R15, 8

// Pop to R1
ADDI R15, R15, -8
LOAD64 R1, [R15]      // R1 = 99

Example 3: Persistent Counter

// Increment a persistent counter
LOADI R0, 0           // key = 0
SLOAD R1, R0          // R1 = storage[0]
ADDI R1, R1, 1        // R1++
SSTORE R0, R1         // storage[0] = R1

Next Steps

Opcodes

Complete reference of all memory operations

Gas Metering

Understand memory and storage costs

Build docs developers (and LLMs) love