Блокчейн
EVM: виртуальная машина Ethereum
Каждый день EVM исполняет миллионы транзакций на сумму в миллиарды долларов. Но вот парадокс: эта машина по мощности уступает калькулятору 1970-х. У неё нет файловой системы, нет сетевого доступа, и один вызов SSTORE обходится дороже, чем миллион операций на вашем ноутбуке. Почему же именно эта примитивная 256-битная стековая машина стала мировым компьютером, на котором работает DeFi стоимостью в десятки миллиардов?
- **Uniswap V3** — каждый swap это цепочка EVM opcodes: SLOAD для чтения ликвидности, вычисления на стеке, SSTORE для обновления позиций. Оптимизация одного SLOAD может сэкономить пользователям миллионы долларов в gas за год
- **Proxy-контракты (OpenZeppelin)** — upgradeable паттерн целиком построен на одном opcode: DELEGATECALL. Он позволяет менять логику контракта, сохраняя адрес и storage. Так работают USDC, Aave, Compound
- **The DAO hack (2016)** — 60M украдены через reentrancy: атакующий эксплуатировал порядок execution context, вызывая CALL до обновления storage. Понимание EVM предотвращает такие атаки
Предварительные знания
Стековая машина EVM
Когда вы вызываете функцию смарт-контракта, каждый узел сети запускает один и тот же код на одной и той же виртуальной машине - **EVM (Ethereum Virtual Machine)**. EVM - это не обычный процессор и не интерпретатор Python. Это **стековая машина**: все вычисления происходят через операции со стеком.
**Стековая машина** - архитектура, в которой операнды берутся с вершины стека и результат кладётся обратно на стек. В отличие от регистровой архитектуры (x86, ARM), здесь нет именованных регистров - только стек.
Ключевые характеристики EVM, зафиксированные в **Ethereum Yellow Paper**:
- **256-bit words** — каждый элемент стека занимает 256 бит (32 байта). Это не случайно: 256 бит = размер хеша Keccak-256, адреса после паддинга, и значения uint256 в Solidity
- **Максимальная глубина стека: 1024** — попытка положить 1025-й элемент вызовет Stack Overflow и revert транзакции
- **Big-endian порядок байтов** — старший байт хранится первым (в отличие от x86, который использует little-endian)
- **Turing-complete** — EVM может выполнить любую вычислимую программу (в отличие от Bitcoin Script, который намеренно ограничен и не имеет циклов)
Turing-completeness EVM - палка о двух концах. Она даёт мощь смарт-контрактов, но создаёт проблему остановки: невозможно заранее определить, завершится ли программа. Поэтому Ethereum ввёл механизм **gas** - принудительный лимит вычислений.
Почему EVM использует 256-битные слова, а не 64-битные как современные процессоры?
Opcodes: инструкции EVM
EVM исполняет **bytecode** - последовательность однобайтовых инструкций, называемых **opcodes**. Когда Solidity-компилятор компилирует ваш контракт, он превращает высокоуровневый код в эти примитивные операции. Всего EVM поддерживает около **140 opcodes**, и каждому присвоен фиксированный gas cost.
Opcodes делятся на несколько категорий:
Посмотрим как простая операция `a + b` в Solidity превращается в bytecode:
**DELEGATECALL** - один из самых мощных и опасных opcodes. Он выполняет код другого контракта, но в **контексте вызывающего**: msg.sender, msg.value и storage - все от вызывающего контракта. Это основа **proxy-паттерна** для upgradeable контрактов.
Чем DELEGATECALL отличается от обычного CALL?
Модель памяти: Stack, Memory, Storage
EVM работает с **тремя отдельными областями данных**, каждая со своими правилами доступа, временем жизни и стоимостью. Понимание различий между ними - ключ к написанию газо-эффективных контрактов.
**Storage в 100-700 раз дороже memory!** Запись нового значения в storage (SSTORE cold) стоит 20,000 gas, а запись в memory (MSTORE) - всего 3 gas. Это самая дорогая операция в EVM, и именно поэтому оптимизация storage-доступов - главная задача при написании газо-эффективного Solidity.
Рассмотрим, как это выглядит в Solidity:
**Calldata** - четвёртая область, доступная только для чтения. Содержит входные данные транзакции. В Solidity параметры `calldata` дешевле `memory`, потому что данные не копируются - используются прямо из входных данных транзакции.
Контракт читает переменную из storage (SLOAD) и затем записывает результат вычисления обратно (SSTORE). Какая из операций потребляет больше gas?
Контекст исполнения
Каждый раз, когда EVM исполняет код, она создаёт **execution context** (фрейм исполнения) - изолированную среду с информацией о том, *кто* вызвал, *сколько* отправил, *в каком* блоке находимся. Это не просто технические детали - от правильного использования контекста зависит безопасность контракта.
Критически важно различать **msg.sender** и **tx.origin**:
**Никогда не используйте tx.origin для авторизации!** Атака: злоумышленник создаёт контракт-приманку. Жертва вызывает его, а он вызывает контракт жертвы - и tx.origin будет адресом жертвы. Классический phishing attack.
EVM различает два типа исполнения:
- **Contract creation** — транзакция без поля `to`. EVM исполняет init code (конструктор), а результат (runtime bytecode) сохраняется по новому адресу. Адрес вычисляется детерминированно из адреса отправителя и nonce (или через CREATE2 — из salt и bytecode hash)
- **Message call** — вызов существующего контракта. EVM загружает bytecode по адресу `to`, создаёт новый execution frame и начинает исполнение с позиции 0
- **Internal transactions** — вызовы между контрактами через CALL/DELEGATECALL/STATICCALL. Это не настоящие транзакции (их нет в блоке), но они создают вложенные execution frames. Каждый frame имеет свой стек и memory, но может делить storage (при DELEGATECALL)
tx.origin и msg.sender - одно и то же, просто разные названия
msg.sender - непосредственный вызывающий (может быть контракт), а tx.origin - всегда оригинальный EOA, инициировавший транзакцию. При цепочке вызовов A→B→C в контексте C они будут разными
Эта путаница приводит к реальным уязвимостям. Использование tx.origin для авторизации позволяет атакующему создать промежуточный контракт-приманку и выполнить действия от имени жертвы. Множество контрактов были взломаны именно через эту ошибку.
Пользователь (EOA) вызывает Контракт A, который через CALL вызывает Контракт B. Чему равен msg.sender внутри Контракта B?
Итоги
- **EVM — 256-битная стековая машина** с глубиной стека 1024. Все данные — 32-байтные слова, что совпадает с размером хешей и адресов Ethereum
- **~140 opcodes** охватывают стековые операции (PUSH/POP/DUP/SWAP), арифметику (ADD/MUL), работу с хранилищем (SLOAD/SSTORE) и вызовы контрактов (CALL/DELEGATECALL/STATICCALL)
- **Три области памяти**: stack (быстрый, volatile, 3 gas), memory (byte array, volatile, 3 gas), storage (key-value, persistent, до 20000 gas). Оптимизация storage-доступов — главный способ снизить gas cost
- **msg.sender vs tx.origin**: msg.sender — непосредственный вызывающий, tx.origin — оригинальный EOA. Использование tx.origin для авторизации — уязвимость
- Итак, EVM при своей примитивности стала мировым компьютером именно *благодаря* ограничениям: детерминизм стековой машины гарантирует, что каждый узел получит одинаковый результат, а gas делает Turing-completeness безопасной
Связанные темы
EVM находится в центре стека Ethereum - понимание её архитектуры связывает аккаунты, газ и высокоуровневые языки:
- Ethereum: аккаунты и state — EVM оперирует данными из world state - балансами, nonce и storage аккаунтов
- Gas: экономика вычислений — Каждый opcode EVM имеет фиксированную стоимость в gas - механизм, ограничивающий Turing-complete вычисления
- Solidity: основы — Solidity компилируется в EVM bytecode - высокоуровневые конструкции превращаются в последовательности opcodes
Вопросы для размышления
- Почему Ethereum выбрал стековую архитектуру, а не регистровую? Какие преимущества это даёт для виртуальной машины, работающей в децентрализованной сети?
- Storage стоит в сотни раз дороже memory. Какие паттерны проектирования контрактов помогают минимизировать использование storage?
- Если бы EVM не была Turing-complete (как Bitcoin Script), какие приложения стали бы невозможны? А какие проблемы безопасности исчезли бы?