Разработка кастомной виртуальной машины блокчейна
EVM — это не единственная возможная архитектура. Она была спроектирована в 2014 году для конкретных компромиссов того времени: 256-битная стековая машина с дорогим storage, без параллелизма, с синхронным выполнением. За десять лет появились задачи, для которых EVM — субоптимальный выбор: игровые движки с тысячами состояний, ZK-friendly вычисления, специализированные финансовые примитивы. Кастомная VM — это не переизобретение колеса, а инженерное решение для конкретного performance/expressivity требования.
Зачем создавать VM с нуля
Прежде чем проектировать кастомную VM, нужно честно ответить: не решается ли задача через существующие опции?
Когда существующего достаточно:
- EVM + Solidity + Foundry закрывают 95% DeFi задач
- Solana SBF (sBPF) — высокопроизводительный DeFi, gaming
- CosmWasm — кастомная L1 с Cosmos SDK без написания VM
- Move (Aptos/Sui) — resource-oriented programming для asset safety
Когда нужна кастомная VM:
- ZK-proof generation: существующие VM (EVM, WASM) не оптимизированы под арифметику конечных полей. ZK-friendly VM строится вокруг Plonky2, Halo2 или STARK-совместимых операций.
- Domain-specific language нужен на уровне исполнения: например, chess engine VM, где каждый opcode — операция с шахматной позицией, а state transitions верифицируются on-chain.
- Параллельное выполнение: EVM последовательный. Solana Sealevel выполняет транзакции параллельно, зная заранее какие account'ы они затрагивают. Аналогичная архитектура для EVM-несовместимой L1.
- Minified execution environment: бортовые системы, IoT — минимальный bytecode интерпретатор для встраиваемых устройств с верификацией on-chain.
Архитектурные решения при проектировании VM
Тип VM: стековая vs регистровая
Stack-based VM (EVM, WASM, JVM): операнды берутся со стека, результат кладётся на стек. Проще компилятор, компактный bytecode, но сложная оптимизация — нет явных registers.
Register-based VM (Lua VM, Dalvik, SBF): операнды — именованные регистры. Быстрее при интерпретации (меньше push/pop операций), проще JIT-компиляция, bytecode чуть объёмнее.
Для блокчейна обычно выбирают стековую VM: она проще формально верифицировать (важно для аудита), bytecode компактнее (меньше calldata), и большинство компиляторов для неё хорошо изучены.
Arithmetic: 256-bit vs field arithmetic
EVM использует 256-bit integers — удобно для Ethereum адресов и хэшей, но расточительно для ZK-proof систем. ZK-friendly VM строится вокруг arithmetic в prime field F_p, где p — prime modulus конкретной proof system:
- Plonky2: F_{2^64 - 2^32 + 1} (Goldilocks field)
- Groth16 / Plonk на BN254: p ≈ 2^254
- STARKs: F_{2^{64}-2^{32}+1} или F_{2^{31}-1}
Операции в этих полях нативно дешевле для proof generation. Если конечная цель — верифицировать выполнение VM в ZK proof, arithmetic field должен совпадать с proof system.
Instruction Set Architecture (ISA)
Проектируя ISA, нужно минимизировать количество opcodes, сохраняя Turing-completeness. Минимальный жизнеспособный набор:
Arithmetic: ADD, SUB, MUL, DIV, MOD
Bitwise: AND, OR, XOR, NOT, SHL, SHR
Comparison: EQ, LT, GT
Memory: LOAD, STORE (или PUSH/POP для stack-based)
Control flow: JUMP, JUMPI, HALT
Stack ops: PUSH_N, POP, DUP, SWAP
Crypto: HASH (keccak256 или poseidon для ZK)
Специализированные opcodes для domain: например, для финансовой VM — MULPERCENT (умножение с процентами без overflow), SQRT (для AMM формул). Каждый новый opcode — это дополнительная сложность в реализации интерпретатора, JIT-компиляторе и формальной верификации.
Реализация интерпретатора
Dispatch loop
Основа любого VM — dispatch loop. Три подхода по производительности:
Switch-case (простейший):
loop {
let opcode = bytecode[pc];
pc += 1;
match opcode {
0x01 => { // ADD
let a = stack.pop()?;
let b = stack.pop()?;
stack.push(a.wrapping_add(b));
}
0x60 => { // PUSH1
let val = bytecode[pc];
pc += 1;
stack.push(val as u64);
}
0x00 => break, // HALT
_ => return Err(InvalidOpcode(opcode)),
}
}
Computed goto / threaded dispatch (C/C++, GCC extension): вместо switch каждый opcode handler заканчивается jump к следующему handler напрямую. ~20-30% быстрее switch в горячем цикле.
JIT-компиляция: компиляция bytecode в native machine code перед выполнением. Cranelift (Rust, используется в Wasmtime) или LLVM backend. Даёт 5–10x ускорение для compute-intensive контрактов. Добавляет значительную сложность: нужен JIT-компилятор, security sandbox для generated code.
Для блокчейн VM JIT используется редко — execution environment должен быть детерминированным и sandboxed. WebAssembly (Wasmtime/Wasmer) берёт это готовым решением.
Memory model
EVM имеет три типа памяти: stack (1024 элементов), memory (линейная, растёт по требованию, оплачивается), storage (persistent, дорого). Кастомная VM может упростить это до двух:
Execution memory — локальная память функции, не персистентна. Flat array с bounds checking:
struct Memory {
data: Vec<u8>,
gas_used: u64,
}
impl Memory {
fn load(&mut self, offset: u32, size: u32) -> Result<&[u8]> {
self.expand_to(offset + size)?;
Ok(&self.data[offset as usize..(offset + size) as usize])
}
fn expand_to(&mut self, size: u32) -> Result<()> {
if size as usize > self.data.len() {
let gas = memory_expansion_cost(self.data.len(), size as usize);
self.gas_used += gas;
self.data.resize(size as usize, 0);
}
Ok(())
}
}
State storage — persistent key-value. За bloчейном — обычно Merkle Patricia Trie (Ethereum) или Jellyfish Merkle Tree (Aptos/Diem) для state commitment. Реализация поверх RocksDB или LevelDB.
Gas metering и determinism
Детерминизм — обязательное требование для блокчейн VM. Одинаковый bytecode + state + input должны давать одинаковый результат на любом узле. Это значит:
- Никакого floating point arithmetic (IEEE 754 может давать разные результаты)
- Детерминированный порядок обхода hash map (в Rust используйте
BTreeMap, неHashMap) - Версионированные opcodes: новые opcodes вводятся только через hard fork
- Bounds для всех операций: максимальный stack size, максимальный memory, максимальный gas
Gas metering — механизм предотвращения DoS через бесконечные циклы. Каждый opcode имеет фиксированную gas стоимость. Перед выполнением проверяем и списываем:
fn execute_opcode(&mut self, opcode: u8) -> Result<()> {
let cost = GAS_TABLE[opcode as usize];
self.gas_remaining = self.gas_remaining
.checked_sub(cost)
.ok_or(OutOfGas)?;
// ... execute opcode
}
Gas table для кастомной VM проектируется на основе benchmarking реальных opcode execution times. Дорогие операции (HASH, STORE) стоят пропорционально больше.
ZK-совместимая VM: специфика
Если цель — доказывать выполнение VM через ZK proof (zkEVM или кастомный zkVM), архитектура существенно усложняется.
Execution trace: нужно генерировать trace выполнения программы — таблицу всех state transitions для каждого шага. Proof system верифицирует, что trace корректен относительно ISA.
Step | PC | Opcode | Stack[0] | Stack[1] | Memory | ...
-----|-----|--------|----------|----------|--------|----
0 | 0 | PUSH 5 | — | — | [] | ...
1 | 2 | PUSH 3 | 5 | — | [] | ...
2 | 4 | ADD | 5 | 3 | [] | ...
3 | 5 | HALT | 8 | — | [] | ...
Constraint system: для каждого opcode пишутся полиномиальные constraints, которые доказывают корректность перехода. Например, для ADD: stack_next[0] = stack[0] + stack[1]. Всё это — algebraic circuit для proof system.
Проекты для reference: RISC Zero (zkVM на RISC-V ISA), Valida (специально для ZK-proof friendly ISA), Polygon Miden (STARK-based zkVM). Изучение их открытых исходников — обязательный шаг перед проектированием собственного zkVM.
Этапы разработки
| Фаза | Содержание | Длительность |
|---|---|---|
| ISA design | Спецификация opcodes, gas table, memory model | 2–4 нед |
| Interpreter | Базовый интерпретатор, тесты на корректность | 4–6 нед |
| Gas metering & limits | Полная реализация gas системы, DoS защита | 2–3 нед |
| State storage | Merkle tree, persistence, state root | 3–5 нед |
| Compiler/toolchain | Компилятор с high-level языка в bytecode | 4–8 нед |
| Integration | Интеграция в consensus layer блокчейна | 3–6 нед |
| ZK circuit (опционально) | Constraint system, proof generation | 8–16 нед |
| Formal verification | Математическая верификация ключевых свойств | 4–8 нед |
Разработка production-ready кастомной VM — это 12–24 месяца работы для команды из 3–5 инженеров. Большинство проектов, которые заявляют о "кастомной VM за квартал", реализуют тонкую обёртку над WASM или EVM, а не подлинно кастомную архитектуру.







