Перейти к основному содержанию

Понимание спецификаций EVM Желтой книги

evm
Intermediate
qbzzt
15 мая 2022 г.
17 минута прочтения

Желтая книга (opens in a new tab) — это официальная спецификация Ethereum. За исключением поправок, внесенных в рамках процесса EIP, она содержит точное описание того, как все работает. Она написана в виде математического документа, который включает в себя терминологию, незнакомую программистам. В этой статье вы узнаете, как ее читать и, как следствие, другие связанные с ней математические документы.

Какая Желтая книга?

Как и почти все остальное в Ethereum, Желтая книга со временем развивается. Чтобы иметь возможность ссылаться на конкретную версию, я загрузил текущую версию на момент написания статьи. Номера разделов, страниц и уравнений, которые я использую, будут относиться к этой версии. Рекомендуется открыть ее в другом окне во время чтения этого документа.

Почему EVM?

Оригинальная Желтая книга была написана в самом начале разработки Ethereum. В ней описывается первоначальный механизм консенсуса на основе доказательства выполнения работы, который изначально использовался для обеспечения безопасности сети. Однако в сентябре 2022 года Ethereum отказался от доказательства выполнения работы и начал использовать консенсус на основе доказательства доли владения. Это руководство посвящено тем частям Желтой книги, которые определяют виртуальную машину Ethereum. EVM не изменилась после перехода на доказательство доли владения (за исключением возвращаемого значения опкода DIFFICULTY).

9. Модель выполнения

Этот раздел (с. 12–14) включает большую часть определения EVM.

Термин состояние системы включает в себя все, что вам нужно знать о системе для ее запуска. В обычном компьютере это означает память, содержимое регистров и т. д.

Машина Тьюринга (opens in a new tab) — это вычислительная модель. По сути, это упрощенная версия компьютера, для которой доказано, что она обладает той же способностью выполнять вычисления, что и обычный компьютер (все, что может вычислить компьютер, может вычислить и машина Тьюринга, и наоборот). Эта модель облегчает доказательство различных теорем о том, что вычислимо, а что нет.

Термин «полный по Тьюрингу» (opens in a new tab) означает компьютер, который может выполнять те же вычисления, что и машина Тьюринга. Машины Тьюринга могут входить в бесконечные циклы, а EVM — нет, потому что у нее закончится газ, поэтому она является лишь квазиполной по Тьюрингу.

9.1. Основы

В этом разделе приведены основы EVM и ее сравнение с другими вычислительными моделями.

Стековая машина (opens in a new tab) — это компьютер, который хранит промежуточные данные не в регистрах, а в стеке (opens in a new tab). Это предпочтительная архитектура для виртуальных машин, потому что ее легко реализовать, а это означает, что вероятность появления ошибок и уязвимостей безопасности намного меньше. Память в стеке разделена на 256-битные слова. Этот размер был выбран потому, что он удобен для основных криптографических операций Ethereum, таких как хэширование Keccak-256 и вычисления на эллиптических кривых. Максимальный размер стека составляет 1024 элемента (1024 x 256 бит). При выполнении опкодов они обычно получают свои параметры из стека. Существуют опкоды, специально предназначенные для реорганизации элементов в стеке, такие как POP (удаляет элемент с вершины стека), DUP_N (дублирует N-й элемент в стеке) и т. д.

EVM также имеет энергозависимую область под названием «память», которая используется для хранения данных во время выполнения. Эта память организована в виде 32-байтовых слов. Все ячейки памяти инициализируются нулем. Если вы выполните этот код на Yul (opens in a new tab), чтобы добавить слово в память, он заполнит 32 байта памяти, дополнив пустое пространство в слове нулями, т. е. создаст одно слово — с нулями в ячейках 0–29, 0x60 в 30-й ячейке и 0xA7 в 31-й.

1mstore(0, 0x60A7)

mstore — это один из трех опкодов, предоставляемых EVM для взаимодействия с памятью — он загружает слово в память. Два других — это mstore8, который загружает один байт в память, и mload, который перемещает слово из памяти в стек.

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

Хотя об этом не упоминается в данном разделе Желтой книги, полезно также знать, что существует четвертый тип памяти. Calldata — это адресуемая по байтам память только для чтения, используемая для хранения значения, передаваемого с параметром data транзакции. В EVM есть специальные опкоды для управления calldata. calldatasize возвращает размер данных. calldataload загружает данные в стек. calldatacopy копирует данные в память.

Стандартная архитектура фон Неймана (opens in a new tab) хранит код и данные в одной и той же памяти. EVM не следует этому стандарту по соображениям безопасности — совместное использование энергозависимой памяти позволяет изменять программный код. Вместо этого код сохраняется в хранилище.

Есть только два случая, когда код выполняется из памяти:

  • Когда контракт создает другой контракт (с помощью CREATE (opens in a new tab) или CREATE2 (opens in a new tab)), код конструктора контракта берется из памяти.
  • Во время создания любого контракта код конструктора запускается, а затем возвращает код фактического контракта, также из памяти.

Термин «исключительное выполнение» означает исключение, которое приводит к остановке выполнения текущего контракта.

9.2. Обзор комиссий

В этом разделе объясняется, как рассчитывается плата за газ. Существует три вида затрат:

Стоимость опкода

Собственная стоимость конкретного опкода. Чтобы получить это значение, найдите группу стоимости опкода в Приложении H (с. 28, под уравнением (327)) и найдите группу стоимости в уравнении (324). Это дает вам функцию стоимости, которая в большинстве случаев использует параметры из Приложения G (с. 27).

Например, опкод CALLDATACOPY (opens in a new tab) является членом группы Wcopy. Стоимость опкода для этой группы составляет Gverylow+Gcopy×⌈μs[2]÷32⌉. Глядя на Приложение G, мы видим, что обе константы равны 3, что дает нам 3+3×⌈μs[2]÷32⌉.

Нам все еще нужно расшифровать выражение ⌈μs[2]÷32⌉. Внешняя часть, ⌈ <value> ⌉ — это функция округления вверх, которая для заданного значения возвращает наименьшее целое число, которое не меньше этого значения. Например, ⌈2,5⌉ = ⌈3⌉ = 3. Внутренняя часть — μs[2]÷32. В разделе 3 («Соглашения») на с. 3 указано, что μ — это состояние машины. Состояние машины определено в разделе 9.4.1 на с. 13. Согласно этому разделу, одним из параметров состояния машины является s для стека. Таким образом, μs[2] — это ячейка № 2 в стеке. Глядя на опкод (opens in a new tab), ячейка № 2 в стеке — это размер данных в байтах. Глядя на другие опкоды в группе Wcopy, CODECOPY (opens in a new tab) и RETURNDATACOPY (opens in a new tab), у них также есть размер данных в той же ячейке. Таким образом, ⌈μs[2]÷32⌉ — это количество 32-байтовых слов, необходимых для хранения копируемых данных. В итоге собственная стоимость CALLDATACOPY (opens in a new tab) составляет 3 ед. газа плюс 3 ед. за каждое копируемое слово данных.

Стоимость выполнения

Стоимость выполнения вызываемого кода.

Стоимость расширения памяти

Стоимость расширения памяти (при необходимости).

В уравнении 324 это значение записывается как Cmemi')-Cmemi). Снова взглянув на раздел 9.4.1, мы видим, что μi — это количество слов в памяти. Таким образом, μi — это количество слов в памяти до выполнения опкода, а μi' — количество слов в памяти после выполнения опкода.

Функция Cmem определена в уравнении 326: Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋. ⌊x⌋ — это функция округления вниз, которая для заданного значения возвращает наибольшее целое число, которое не больше этого значения. Например, ⌊2,5⌋ = ⌊2⌋ = 2. Когда a < √512, a2 < 512 и результат функции округления вниз равен нулю. Таким образом, для первых 22 слов (704 байт) стоимость растет линейно с количеством требуемых слов памяти. За этой точкой ⌊a2 ÷ 512⌋ становится положительным. Когда требуемый объем памяти достаточно велик, стоимость газа пропорциональна квадрату объема памяти.

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

Подробнее о газе.

9.3. Среда выполнения

Среда выполнения — это кортеж I, который включает информацию, не являющуюся частью состояния блокчейна или EVM.

ПараметрОпкод для доступа к даннымКод на Solidity для доступа к данным
IaADDRESS (opens in a new tab)address(this)
IoORIGIN (opens in a new tab)tx.origin
IpGASPRICE (opens in a new tab)tx.gasprice
IdCALLDATALOAD (opens in a new tab) и др.msg.data
IsCALLER (opens in a new tab)msg.sender
IvCALLVALUE (opens in a new tab)уточнить сумму
IbCODECOPY (opens in a new tab)address(this).code
IHПоля заголовка блока, такие как NUMBER (opens in a new tab) и DIFFICULTY (opens in a new tab)block.number, block.difficulty и т. д.
IeГлубина стека вызовов для вызовов между контрактами (включая создание контрактов)
IwРазрешено ли EVM изменять состояние, или она работает статически

Для понимания остальной части раздела 9 необходимо еще несколько параметров:

ПараметрОпределено в разделеЗначение
σ2 (с. 2, уравнение 1)Состояние блокчейна
g9.3 (с. 13)Оставшийся газ
A6.1 (с. 8)Накопленное подсостояние (изменения, запланированные на момент завершения транзакции)
o9.3 (с. 13)Вывод — возвращаемый результат в случае внутренней транзакции (когда один контракт вызывает другой) и вызовов функций просмотра (когда вы просто запрашиваете информацию, поэтому нет необходимости ждать транзакции)

9.4. Обзор выполнения

Теперь, когда мы рассмотрели все предварительные сведения, мы можем наконец-то приступить к изучению того, как работает EVM.

Уравнения 137–142 дают нам начальные условия для запуска EVM:

СимволНачальное значениеЗначение
μggОставшийся газ
μpc0Счетчик команд, адрес следующей инструкции для выполнения
μm(0, 0, ...)Память, инициализированная нулями
μi0Самая высокая используемая ячейка памяти
μs()Стек, изначально пустой
μoВыходные данные, пустое множество до тех пор, пока мы не остановимся либо с возвращаемыми данными (RETURN (opens in a new tab) или REVERT (opens in a new tab)), либо без них (STOP (opens in a new tab) или SELFDESTRUCT (opens in a new tab)).

Уравнение 143 говорит нам, что в каждый момент времени во время выполнения есть четыре возможных условия и что с ними делать:

  1. Z(σ,μ,A,I). Z представляет функцию, которая проверяет, создает ли операция недопустимый переход состояния (см. исключительная остановка). Если ее значение равно True, новое состояние идентично старому (за исключением того, что сжигается газ), поскольку изменения не были реализованы.
  2. Если выполняемый опкод — REVERT (opens in a new tab), новое состояние совпадает со старым, при этом теряется некоторое количество газа.
  3. Если последовательность операций завершена, о чем свидетельствует RETURN (opens in a new tab)), состояние обновляется до нового.
  4. Если мы не находимся в одном из конечных условий 1-3, продолжаем выполнение.

9.4.1. Состояние машины

В этом разделе более подробно объясняется состояние машины. Здесь указано, что w — это текущий опкод. Если μpc меньше ||Ib||, длины кода, то этот байт (Ibpc]) является опкодом. В противном случае опкод определяется как STOP (opens in a new tab).

Поскольку это стековая машина (opens in a new tab), нам необходимо отслеживать количество элементов, извлеченных (δ) и помещенных (α) каждым опкодом.

9.4.2. Исключительная остановка

В этом разделе определяется функция Z, которая указывает, когда происходит нештатное завершение. Это логическая (opens in a new tab) функция, поэтому она использует для логического «или» (opens in a new tab) и для логического «и» (opens in a new tab).

Исключительная остановка происходит, если выполняется любое из этих условий:

  • μg < C(σ,μ,A,I) Как мы видели в разделе 9.2, C — это функция, которая определяет стоимость газа. Недостаточно газа для покрытия следующего опкода.

  • δw=∅ Если количество элементов, извлеченных для опкода, не определено, то сам опкод не определен.

  • || μs || < δw Нехватка элементов в стеке; в стеке недостаточно элементов для текущего опкода.

  • w = JUMP ∧ μs[0]∉D(Ib) Опкод — JUMP (opens in a new tab), а адрес не является JUMPDEST (opens in a new tab). Переходы действительны только тогда, когда место назначения — JUMPDEST (opens in a new tab).

  • w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) Опкод — JUMPI (opens in a new tab), условие истинно (не равно нулю), поэтому переход должен произойти, а адрес не является JUMPDEST (opens in a new tab). Переходы действительны только тогда, когда место назначения — JUMPDEST (opens in a new tab).

  • w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || Опкод — RETURNDATACOPY (opens in a new tab). В этом опкоде элемент стека μs[1] — это смещение для чтения из буфера возвращаемых данных, а элемент стека μs[2] — это длина данных. Это условие возникает, когда вы пытаетесь читать за пределами буфера возвращаемых данных. Обратите внимание, что для calldata или для самого кода подобного условия нет. Когда вы пытаетесь читать за пределами этих буферов, вы просто получаете нули.

  • || μs || - δw + αw > 1024

    Переполнение стека. Если выполнение опкода приведет к тому, что стек превысит 1024 элемента, выполнение прерывается.

  • ¬Iw ∧ W(w,μ) Работаем ли мы статически (¬ — это отрицание (opens in a new tab), а Iw — истина, когда нам разрешено изменять состояние блокчейна)? Если да, и мы пытаемся выполнить операцию, изменяющую состояние, этого не произойдет.

    Функция W(w,μ) определяется далее в уравнении 150. W(w,μ) истинно, если выполняется одно из этих условий:

    • w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} Эти опкоды изменяют состояние либо путем создания нового контракта, сохранения значения, либо уничтожения текущего контракта.

    • LOG0≤w ∧ w≤LOG4 Если нас вызывают статически, мы не можем создавать записи в журнале. Опкоды журнала находятся в диапазоне между LOG0 (A0) (opens in a new tab) и LOG4 (A4) (opens in a new tab). Число после опкода журнала указывает, сколько тем содержит запись журнала.

    • w=CALL ∧ μs[2]≠0 Вы можете вызвать другой контракт, будучи статичным, но в этом случае вы не можете передать ему ETH.

  • w = SSTORE ∧ μg ≤ Gcallstipend Вы не можете запустить SSTORE (opens in a new tab), если у вас нет больше Gcallstipend (определено как 2300 в Приложении G) газа.

9.4.3. Проверка действительности адреса назначения перехода

Здесь мы формально определяем, что такое опкоды JUMPDEST (opens in a new tab). Мы не можем просто искать байтовое значение 0x5B, потому что оно может находиться внутри PUSH (и, следовательно, быть данными, а не опкодом).

В уравнении (153) мы определяем функцию N(i,w). Первый параметр, i, — это расположение опкода. Второй, w, — это сам опкод. Если w∈[PUSH1, PUSH32], это означает, что опкод является PUSH (квадратные скобки определяют диапазон, включающий конечные точки). В этом случае следующий опкод находится в i+2+(w−PUSH1). Для PUSH1 (opens in a new tab) нам нужно продвинуться на два байта (сам PUSH и однобайтовое значение), для PUSH2 (opens in a new tab) — на три байта, потому что это двухбайтовое значение, и т. д. Все остальные опкоды EVM имеют длину всего один байт, поэтому во всех остальных случаях N(i,w)=i+1.

Эта функция используется в уравнении (152) для определения DJ(c,i), которое является множеством (opens in a new tab) всех допустимых адресов назначения переходов в коде c, начиная с расположения опкода i. Эта функция определяется рекурсивно. Если i≥||c||, это означает, что мы находимся в конце кода или после него. Мы больше не найдем адресов назначения переходов, поэтому просто вернем пустое множество.

Во всех остальных случаях мы смотрим на остальную часть кода, переходя к следующему опкоду и получая множество, начинающееся с него. c[i] — это текущий опкод, поэтому N(i,c[i]) — это расположение следующего опкода. Следовательно, DJ(c,N(i,c[i])) — это множество допустимых адресов назначения переходов, которое начинается со следующего опкода. Если текущий опкод не является JUMPDEST, просто верните это множество. Если это JUMPDEST, включите его в результирующее множество и верните его.

9.4.4. Нормальная остановка

Функция остановки H может возвращать три типа значений.

  • Если мы не находимся в опкоде остановки, возвращаем , пустое множество. По соглашению, это значение интерпретируется как логическое «ложь».
  • Если у нас есть опкод остановки, который не производит вывода (либо STOP (opens in a new tab), либо SELFDESTRUCT (opens in a new tab)), возвращаем последовательность байтов нулевого размера в качестве возвращаемого значения. Обратите внимание, что это сильно отличается от пустого множества. Это значение означает, что EVM действительно остановилась, просто нет возвращаемых данных для чтения.
  • Если у нас есть опкод остановки, который производит вывод (либо RETURN (opens in a new tab), либо REVERT (opens in a new tab)), возвращаем последовательность байтов, указанную этим опкодом. Эта последовательность берется из памяти, значение на вершине стека (μs[0]) — это первый байт, а значение после него (μs[1]) — это длина.

H.2. Набор инструкций

Прежде чем мы перейдем к последнему подразделу EVM, 9.5, давайте посмотрим на сами инструкции. Они определены в Приложении H.2, которое начинается на с. 29. Все, что не указано как изменяющееся с этим конкретным опкодом, должно оставаться неизменным. Переменные, которые изменяются, обозначаются как <что-то>′.

Например, давайте посмотрим на опкод ADD (opens in a new tab).

ЗначениеМнемоникаδαОписание
0x01Добавление21Операция сложения.
μ′s[0] ≡ μs[0] + μs[1]

δ — это количество значений, которые мы извлекаем из стека. В данном случае два, потому что мы складываем два верхних значения.

α — это количество значений, которые мы помещаем обратно. В данном случае одно, сумма.

Таким образом, новая вершина стека (μ′s[0]) — это сумма старой вершины стека (μs[0]) и старого значения под ней (μs[1]).

Вместо того чтобы перечислять все опкоды в виде скучного списка, в этой статье объясняются только те опкоды, которые вводят что-то новое.

ЗначениеМнемоникаδαОписание
0x20KECCAK25621Вычислить хэш Keccak-256.
μ′s[0] ≡ KEC(μms[0] . . . (μs[0] + μs[1] − 1)])
μ′i ≡ M(μis[0],μs[1])

Это первый опкод, который обращается к памяти (в данном случае, только для чтения). Однако он может выйти за текущие пределы памяти, поэтому нам нужно обновить μi. Мы делаем это с помощью функции M, определенной в уравнении 328 на с. 29.

ЗначениеМнемоникаδαОписание
0x31BALANCE11Получить баланс данного аккаунта.
...

Адрес, баланс которого нам нужно найти, — μs[0] mod 2160. На вершине стека находится адрес, но поскольку адреса состоят всего из 160 бит, мы вычисляем значение по модулю (opens in a new tab) 2160.

Если σ[μs[0] mod 2160] ≠ ∅, это означает, что об этом адресе есть информация. В этом случае σ[μs[0] mod 2160]b — это баланс для этого адреса. Если σ[μs[0] mod 2160] = ∅, это означает, что этот адрес не инициализирован, и баланс равен нулю. Вы можете увидеть список полей информации об аккаунте в разделе 4.1 на с. 4.

Второе уравнение, A'a ≡ Aa ∪ {μs[0] mod 2160}, связано с разницей в стоимости доступа к «теплому» хранилищу (хранилищу, к которому недавно обращались и которое, вероятно, кэшировано) и «холодному» хранилищу (хранилищу, к которому не обращались и которое, вероятно, находится в более медленном хранилище, извлечение из которого дороже). Aa — это список адресов, к которым ранее обращалась транзакция, и поэтому доступ к которым должен быть дешевле, как определено в разделе 6.1 на с. 8. Вы можете прочитать больше по этой теме в EIP-2929 (opens in a new tab).

ЗначениеМнемоникаδαОписание
0x8FDUP161617Дублировать 16-й элемент стека.
μ′s[0] ≡ μs[15]

Обратите внимание, что для использования любого элемента стека нам нужно его извлечь, что означает, что нам также нужно извлечь все элементы стека над ним. В случае DUP<n> (opens in a new tab) и SWAP<n> (opens in a new tab) это означает необходимость извлечения, а затем помещения до шестнадцати значений.

9.5. Цикл выполнения

Теперь, когда у нас есть все части, мы можем наконец понять, как документирован цикл выполнения EVM.

Уравнение (155) гласит, что при заданном состоянии:

  • σ (глобальное состояние блокчейна)
  • μ (состояние EVM)
  • A (подсостояние, изменения, которые должны произойти по завершении транзакции)
  • I (среда выполнения)

Новое состояние — (σ', μ', A', I').

Уравнения (156)-(158) определяют стек и его изменение из-за опкода (μs). Уравнение (159) — это изменение газа (μg). Уравнение (160) — это изменение счетчика команд (μpc). Наконец, уравнения (161)-(164) указывают, что остальные параметры остаются неизменными, если они не изменены явно опкодом.

Таким образом, EVM полностью определена.

Заключение

Математическая нотация точна и позволила Желтой книге определить каждую деталь Ethereum. Однако у нее есть и некоторые недостатки:

  • Она может быть понята только людьми, что означает, что тесты на соответствие (opens in a new tab) должны писаться вручную.
  • Программисты понимают компьютерный код. Они могут понимать или не понимать математическую нотацию.

Возможно, по этим причинам более новые спецификации уровня консенсуса (opens in a new tab) написаны на Python. Существуют спецификации уровня выполнения на Python (opens in a new tab), но они неполные. До тех пор, пока вся Желтая книга не будет также переведена на Python или подобный язык, Желтая книга будет продолжать использоваться, и умение ее читать будет полезным.

Последнее обновление страницы: 1 февраля 2026 г.

Было ли это руководство полезным?