EVM — виртуальная машина Ethereum: детерминизм, байткод и газ

EVM — это изолированная среда выполнения смарт-контрактов для сети Ethereum и всех EVM-совместимых блокчейнов. Она задаёт правила изменения глобального состояния: как транзакции и вызовы меняют балансы, хранилище контрактов, логи и пр. В отличие от обычной «виртуалки», EVM жёстко лимитирована по ресурсам и цене операций через газ, а вычисления детерминированы — одинаковый байткод при одинаковом входе даст одинаковый результат на любом узле.

Эта статья — «центральный хаб» по теме EVM. В ней мы:

  • разложим по полочкам архитектуру (стек, память, хранилище, calldata/returndata, логи);
  • объясним модель счетов и состояние сети;
  • разберём ключевые опкоды и их эволюцию на хардфорках (включая PUSH0, MCOPY, TSTORE/TLOAD, изменения SELFDESTRUCT);
  • дадим практические советы для разработчиков и аудиторов;
  • покажем, что такое EVM-совместимость/эквивалентность на L2 и почему это важно;
  • оставим дорожную карту «что почитать дальше» в нашей вики для хорошей перелинковки.

1) Что такое EVM простыми словами

EVM — это стековая (stack-based) машина, исполняющая байткод смарт-контрактов. Контракты компилируются из языков высокого уровня (чаще всего Solidity, реже Vyper, либо из промежуточного языка Yul) в набор инструкций (опкодов). Каждая инструкция стоит газ, а у транзакции есть лимит газа — он и ограничивает вычисления. Такой дизайн решает две задачи:

  • предотвращает атаки типа «бесконечного цикла» — газ закончится;
  • делает стоимость вычислений предсказуемой — каждая операция тарифицируется.

EVM — детерминирована и изолирована: она не может «выйти в интернет», запросить системное время или файл — все данные должны прийти через транзакцию (calldata) или из состояния блокчейна. Это позволяет всем узлам сети прийти к одному и тому же новому состоянию после выполнения блока.

2) Архитектура EVM: из чего она состоит

2.1 Стек, слово, глубина

Стек — LIFO-структура максимум на 1024 элемента.

Слово EVM — 256-битное (32 байта). Все арифметические операции идут над 256-битными беззнаковыми整数ами.

Типичные стек-инструкции: PUSHn, POP, DUPn, SWAPn, ADD, MUL, SHL/SHR, AND, OR, XOR.

Это сделано ради удобства криптографии (256-битные числа), адресов и хешей (Keccak-256). См. также Opcodes для систематизации опкодов.

2.2 Память (memory)

  • Временная, байтовая область, очищается после завершения вызова.
  • Используется для сборки/парсинга данных, ABI-кодирования/декодирования, временных структур.
  • Расширение памяти дороже чем чтение/запись уже «освоенных» областей (существует формула «memory expansion cost»).

2.3 Хранилище (storage)

  • Постоянное KV-хранилище контракта, адресуемое по 32-байтным слотам slot → 32 bytes.
  • Это основная «плата за ресурсы»: операции SSTORE/SLOAD самые дорогие.
  • Компиляторы (Solidity/Vyper) отображают переменные/структуры/маппинги в слоты; для маппингов используется хеширование keccak256(key . slot). Правильная работа со слотами — база для апгрейд-прокси, минимизации газа и безопасности.

2.4 Calldata и returndata

Calldata — входные байты вызова (транзакции/внутреннего вызова), только для чтения.

Returndata — байты, возвращённые последним внешним вызовом: читаются RETURNDATASIZE/RETURNDATACOPY.

2.5 Логи (events)

  • Контракт может эмитить события LOG0 … LOG4. Они пишутся в receipt транзакции и индексируются topics (до 4 штук).

3) Модель аккаунтов и мировое состояние

Ethereum использует account-based модель:

  • EOA (Externally-Owned Account) — «кошелёк», управляемый приватным ключом; может инициировать транзакции.
  • Контракт-аккаунт — имеет код и хранилище; не может инициировать транзакции сам, только реагирует на входящие сообщения/транзакции.
  • Состояние — это отображение адрес → (nonce, balance, storageRoot, codeHash). Полный узел хранит это как модифицированное Patricia-дерево; именно хэш-корни делают состояние проверяемым. Подробнее см. состояние Ethereum.

Создание контракта:

CREATE — адрес = keccak256(rlp(sender, nonce))[12:].

CREATE2 — адрес = keccak256(0xff ++ deployer ++ salt ++ keccak256(init_code))[12:]. Это позволяет детерминированные адреса (важно для фабрик, мета-транзакций, адресов «на будущее»).

4) Газ и цена вычислений

4.1 Базовые принципы

  • Каждая операция имеет стоимость в газе; у транзакции — лимит газа и предлагаемая цена (в эпоху EIP-1559 это base fee + tip).
  • Есть база транзакции (обычно 21 000 газа), далее — плата за calldata и за исполнение опкодов.
  • Вызовы в другие контракты следуют правилу «63/64»: по умолчанию потомку нельзя передать весь газ (для устойчивости), и есть нюансы со «стипендией» 2300 газа при отправке ETH без явной передачи газа.

4.2 «Дорогие» части EVM

  • Хранилище: SSTORE и SLOAD — самые чувствительные к изменениям EIP-ов (см. ниже).
  • Хеширование: KECCAK256 — часто используется (ABI, маппинги, селекторы).
  • Копирование/память: раньше типичные шаблоны кода тратили много газа на CODECOPY/MSTORE/MLOAD; с появлением MCOPY стало дешевле.

4.3 Эволюция газовой модели на хардфорках (важно разработчикам)

  • Istanbul: EIP-1884 репрайс опкодов, EIP-2200 — новая модель SSTORE (net gas metering).
  • Berlin: EIP-2929 «тёплые/холодные» доступы к состоянию; EIP-2930 — Access List-транзакции (предзаявляете адреса/слоты для удешевления «первого» доступа).
  • London: EIP-3529 сокращение газ-рефандов (например, за очищение слота).
  • Shanghai: EIP-3855 PUSH0 и EIP-3860 лимит и тарификация initcode.
  • Cancun/Dencun: добавлены EVM-оптимизации TSTORE/TLOAD (transient storage, не сохраняется между транзакциями) и MCOPY (быстрое копирование в памяти) — сильно полезно для конструкторов данных, пулов, AMM и др.

См. раздел «Эволюция опкодов» ниже для деталей и безопасных практик.

5) Ядро опкодов, которые обязан знать каждый разработчик

5.1 Вызовы и выполнение

CALL — обычный внешний вызов (можно передать ETH и газ).

STATICCALL — «только чтение» (запрещены изменения состояния).

DELEGATECALL — выполняет код другого контракта в контексте текущего хранилища (ключ к прокси-паттернам, но и к уязвимостям).

CREATE / CREATE2 — развёртывание контрактов.

5.2 Память и код

MLOAD/MSTORE, CALLDATASIZE/CALLDATALOAD/CALLDATACOPY, RETURNDATASIZE/RETURNDATACOPY.

CODECOPY — копирование байткода контракта в память.

MCOPY — эффективное копирование внутри памяти (см. «Новые опкоды»).

5.3 Хеширование и арифметика

KECCAK256 (в байткоде исторически именуется SHA3), ADD/MUL/SUB, SHL/SHR, AND/OR/XOR.

5.4 Хранилище

SLOAD, SSTORE — читать/писать слоты (учитывайте net gas metering, warm/cold).

5.5 Логи

LOG0…LOG4 — события (topics + data), см. Abi и Function Selector.

5.6 Управление потоком

JUMP/JUMPI, JUMPDEST, REVERT, RETURN.

6) Эволюция опкодов и поведения EVM на хардфорках

Byzantium принесла критические опкоды:

  • REVERT (аккуратный откат с данными);
  • STATICCALL (жёсткое «только чтение»);
  • новые предкомпилы для zk-криптографии (см. ниже).

Constantinople / Petersburg:

  • CREATE2 (детерминированное развёртывание);
  • уточнения SSTORE (дальше — в EIP-2200).

Istanbul:

  • Репрайс SLOAD, BALANCE, EXTCODE* и пр. (EIP-1884);
  • Вводится модель net gas metering для SSTORE (EIP-2200).

Berlin:

  • Ввод «тёплых/холодных» доступов к адресам/слотам (EIP-2929): первый доступ дороже, последующие дешевле в рамках транзакции;
  • Появляются access list-транзакции (EIP-2930): объявляете наперёд, чтобы снизить стоимость «холодного» доступа.

London:

  • Сильное сокращение refunds (EIP-3529): нельзя «майнить рефанды» через массовые очищения слотов.

Shanghai:

  • PUSH0 — теперь пуш «нуля» не требует 2-байтного PUSH1 0x00 (меньше байткода/газа);
  • EIP-3860: лимит initcode = 49 152 байта и дополнительная тарификация за каждые 32 байта (важно для больших фабрик/прокси).

Dencun (Cancun-Deneb):

  • Transient storage: TSTORE/TLOAD — сверхдешёвое временное хранилище внутри одной транзакции (идеально для «переноса состояния» между внутренними вызовами без записи в постоянное storage);
  • MCOPY — быстрый memory-to-memory copy.

Shanghai также изменила SELFDESTRUCT:

  • EIP-6780: SELFDESTRUCT больше не удаляет контракт и не очищает storage, если контракт не был создан в той же транзакции. По сути — возвращает эфир на адрес-бенефициар, но код/хранилище остаются (важно для миграций и безопасности).

Практика: не используйте SELFDESTRUCT как «кнопку удаления» — после EIP-6780 это не очистка состояния. Для «отключения» логики закладывайте флаги/роллинг-админа или апгрейдируемую архитектуру.

7) Предкомпилированные контракты (precompiles)

Ряд «тяжёлых» операций вынесен в фиксированные адреса 0x01–0x09. Чаще всего используются:

0x01 — ECRECOVER (восстановление адреса из подписи).

0x02 — SHA-256.

0x03 — RIPEMD-160.

0x04 — IDENTITY.

0x05 — модульное возведение в степень (MODEXP).

0x06/0x07/0x08 — операции над alt_bn128 (BN254): сложение, умножение, паринг — для zk-доказательств.

0x09 — BLAKE2f (компрессионная функция; используется редко).

Нюанс: набор и цена предкомпилов — часть консенсуса L1. На L2/парачейнах список может различаться. При портировании кода на «EVM-совместимые» сети проверяйте поддержку и цены предкомпилов.

8) Keccak-256, селекторы и ABI

В байткоде EVM используется Keccak-256 (исторически опкод называется SHA3, но это не NIST SHA-3). Отсюда ряд правил:

  • Селектор функции — первые 4 байта keccak256(«foo(address,uint256)») из ABI-сигнатуры. Именно эти 4 байта стоят в начале calldata.
  • Сигнатура события — topic[0] = keccak256(«Transfer(address,address,uint256)»).
  • Маппинги/слоты — хеширование keccak256(key . slot).

Практика: всегда формируйте сигнатуры канонически (без пробелов, с точными типами), иначе селекторы/события не совпадут.

9) Безопасность: что чаще всего ломают в EVM

Реинэнтрантность (особенно вокруг call{value:…} и взаимодействия с внешними контрактами). Используйте checks-effects-interactions, ReentrancyGuard, избегайте логики в fallback/receive. Помните про «стипендию» 2300 газа — она защищала только «старый мир», сейчас её недостаточно.

DELEGATECALL и прокси: при ошибках в инициализации/слотах хранения возможны критические уязвимости. Всегда фиксируйте storage layout и используйте проверенные шаблоны (UUPS/Transparent).

Неучёт gas repricing: жёстко захардкоженные лимиты газа к внешним вызовам/петлям могут ломаться после репрайсов.

SELFDESTRUCT после EIP-6780: не рассчитывайте на очистку состояния; прорабатывайте «деактивацию» логики флагами.

CREATE2-коллизии и «подмена кода»: адрес заранее известен — подписывайте данные и проверяйте «код по адресу после деплоя», если от этого зависит безопасность.

См. Reentrancy, Delegatecall, Create2 Risks (перспективные статьи).

10) Совместимость/эквивалентность EVM на L2

Термины:

  • EVM-совместимость — «код исполняется», но поведение/цены/предкомпилы могут отличаться.
  • EVM-эквивалентность — цельная гарантия: «то же поведение байткода и опкодов», минимальные дельты по газу/краевым случаям. Это снижает риски «сюрпризов» при миграциях, важнее всего для аудита.
  • В мире L2 (Optimism/OP Stack, Arbitrum, zkEVM-решения) проекты стремятся к эквивалентности, но детали критичны: сравнивайте поддержку новых опкодов (PUSH0/MCOPY/TSTORE), предкомпилов и лимитов.

11) Практика для разработчиков: короткая «шпаргалка»

Оптимизация газа:

  • Минимизируйте SSTORE (батчи, флаги-битпак, инкрементальные обновления).
  • Используйте PUSH0, MCOPY, transient storage (TSTORE/TLOAD) там, где допустимо.
  • Предзаявляйте access list (тип-1/тип-3 транзакции) при массовых внешних чтениях.

Структуры данных:

  • Хеш-ключи маппингов и структуры в слотах документируйте в Storage Layout.
  • Для больших массивов — думайте о event-логах вместо постоянного storage, если данные не нужны в EVM-логике.

CREATE2:

  • Храните salt и init_code_hash для воспроизводимости.
  • Проверяйте «что по адресу уже есть код» перед деплоем (защита от подмен).

Логи/индексация:

  • Используйте indexed для полей-фильтров; помните лимит «до 3 indexed» у неанонимных событий.

Обновления сети:

  • Отслеживайте, как новые EIP-ы меняют газ/поведение (SSTORE, SELFDESTRUCT, новые опкоды).
  • Не полагайтесь на «магические числа газа» — используйте безопасные паттерны.

12) Частые вопросы (FAQ)

EVM использует SHA-3? Исторически опкод назван SHA3, но в EVM используется Keccak-256 (пред-NIST версия), а не финальный NIST SHA-3. Для селекторов/событий берётся именно Keccak-256.

Сколько «памяти» у контракта? Память динамична и очищается после вызова. Постоянные данные — только в storage, который дорог по газу.

Почему мой контракт «не удаляется» через SELFDESTRUCT? Потому что после EIP-6780 удаление/очистка состояния фактически отключены (кроме случая «создан и тут же удалён в одной транзакции»).

Можно ли обойти лимит размера кода? Есть лимит размера развернутого кода (по EIP-170) и initcode (EIP-3860). Обходят архитектурой (разбиение на модули/библиотеки, прокси, апгрейды), оптимизируют компиляцию, используют PUSH0/MCOPY.

13) Перспективы и «куда движется EVM»

  • EOF (EVM Object Format) — модульность и структурированный формат кода (пакеты, секции), уменьшение неоднозначностей исполнения и новые оптимизации. Следим за включением в будущие апгрейды L1 и поддержкой на L2.
  • Новые EIP-ы по лимитам кода — обсуждаются поднятия/метризация лимита развернутого кода сверх 24 576 байт; проверяйте актуальный статус перед релизами.
  • Account Abstraction — влияет на типы транзакций, но не меняет базовую модель EVM; важно для UX и газовой экономики.

14) Перелинковка по нашей вики (что читать далее)

Газ в Ethereum — полная таблица цен, base fee/priority fee, calldata-pricing.

ABI и кодирование — как кодируется calldata/returndata, динамические типы.

Keccak-256 — разница с SHA-3, практические советы.

Solidity и Vyper — языки для EVM.

Yul/Yul+ — промежуточный язык, инлайн-ассемблер.

Селекторы функций и события — 4-байтовые метод-ID и topic[0].

Реинэнтрантность и DELEGATECALL-риски — ключевые векторы атак.

CREATE2 и детерминированные адреса — фабрики, «адреса-заглушки».

Предкомпилы EVM — назначение и цена на L1/L2.

EVM-эквивалентность на L2 — чем отличается от «совместимости».

Памятка по storage-layout в Solidity · Шаблоны оптимизации газа · EOF: формат объектов EVM · SELFDESTRUCT после EIP-6780.

15) Мини-глоссарий

Word (слово) — 32 байта; базовая единица для большинства операций EVM.

Warm/Cold — тёплый/холодный доступ к адресу/слоту в рамках одной транзакции; первый доступ дороже (Berlin).

Transient storage — временное «хранилище» уровня транзакции (TSTORE/TLOAD), не сохраняется в состоянии.

Precompile — «вшитые» по адресам контракты для дорогих криптоопераций.

Access list — список адресов/слотов, объявленных заранее, чтобы удешевить «холодные» доступы.

16) Чек-лист для аудита EVM-контрактов

Флоу ценностей: где и как уходят ETH/tokens (порядок checks-effects-interactions, отсутствие «голых» call без лимита газа, обоснованный transfer).

DELEGATECALL-поверхность: строго фиксированный implementation слот, защита инициализации, «сторож» апгрейдов.

SSTORE-экономика: нет ли «лишних записей»; конструирование значений (битпак, merge-update), batched-модификации.

CREATE2: проверка адресов до/после деплоя, невозможность «подмены» кода.

События: все критичные state-change отражены в событиях; корректные indexed поля.

EIP-совместимость: PUSH0/MCOPY/TSTORE/TLOAD/SELFDESTRUCT-семантика тестируется на целевой сети (особенно L2).

Итог

EVM — строгая, минималистичная машина, идеально приспособленная для детерминированных вычислений с измеримой стоимостью. Глубокое понимание стека-памяти-хранилища, газовой модели и эволюции опкодов — ключ к безопасным и экономным контрактам. Сохраняйте эту страницу в закладки — мы будем дополнять разделы и расширять перелинковку по мере развития нашей вики.

Task Runner