Понимание спецификаций EVM в желтой книге
Желтая книга (opens in a new tab) — это формальная спецификация Эфириума. За исключением поправок, внесенных в ходе процесса EIP, она содержит точное описание того, как все работает. Она написана как математическая статья, включающая терминологию, которая может быть незнакома программистам. В этой статье вы узнаете, как ее читать, а следовательно, и другие связанные с ней математические статьи.
Какая желтая книга?
Как и почти все остальное в Эфириуме, желтая книга со временем развивается. Чтобы иметь возможность ссылаться на конкретную версию, я загрузил текущую версию на момент написания. Номера разделов, страниц и уравнений, которые я использую, будут относиться к этой версии. Рекомендуется держать ее открытой в другом окне во время чтения этого документа.
Почему EVM?
Оригинальная желтая книга была написана в самом начале разработки Эфириума. Она описывает оригинальный механизм консенсуса на основе доказательства выполнения работы (PoW), который изначально использовался для защиты сети. Однако в сентябре 2022 года Эфириум отказался от доказательства выполнения работы (PoW) и начал использовать консенсус на основе доказательства доли владения. В этом руководстве основное внимание будет уделено частям желтой книги, определяющим виртуальную машину Эфириума (EVM). 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-битные слова. Это было выбрано потому, что это удобно для основных криптографических операций Эфириума, таких как хеширование Keccak-256 и вычисления на эллиптической кривой. Максимальный размер стека составляет 1024 элемента (1024 x 256 бит). Когда выполняются коды операций, они обычно получают свои параметры из стека. Существуют коды операций специально для реорганизации элементов в стеке, такие как POP (удаляет элемент с вершины стека), DUP_N (дублирует N-й элемент в стеке) и т. д.
EVM также имеет энергозависимое пространство, называемое памятью (memory), которое используется для хранения данных во время выполнения. Эта память организована в виде 32-байтовых слов. Все ячейки памяти инициализируются нулем. Если вы выполните этот код на Yul (opens in a new tab) для добавления слова в память, он заполнит 32 байта памяти, дополнив пустое пространство в слове нулями, т. е. он создаст одно слово — с нулями в позициях 0-29, 0x60 в 30 и 0xA7 в 31.
mstore(0, 0x60A7)
mstore — это один из трех кодов операций, которые EVM предоставляет для взаимодействия с памятью: он загружает слово в память. Два других — это mstore8, который загружает один байт в память, и mload, который перемещает слово из памяти в стек.
EVM также имеет отдельную энергонезависимую модель хранилища (storage), которая поддерживается как часть состояния системы — эта память организована в виде массивов слов (в отличие от массивов байтов с адресацией по словам в стеке). В этом хранилище контракты хранят постоянные данные — контракт может взаимодействовать только со своим собственным хранилищем. Хранилище организовано в виде сопоставлений «ключ-значение».
Хотя это не упоминается в данном разделе желтой книги, также полезно знать, что существует четвертый тип памяти. Данные вызова (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 за каждое копируемое слово данных.
Стоимость выполнения
Стоимость выполнения вызываемого нами кода.
- В случае
CREATE(opens in a new tab) иCREATE2(opens in a new tab) — конструктор для нового контракта. - В случае
CALL(opens in a new tab),CALLCODE(opens in a new tab),STATICCALL(opens in a new tab) илиDELEGATECALL(opens in a new tab) — вызываемый нами контракт.
Стоимость расширения памяти
Стоимость расширения памяти (при необходимости).
В уравнении 324 это значение записывается как Cmem(μi')-Cmem(μi). Снова взглянув на раздел 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 для доступа к данным |
|---|---|---|
| Ia | ADDRESS (opens in a new tab) | address(this) |
| Io | ORIGIN (opens in a new tab) | tx.origin |
| Ip | GASPRICE (opens in a new tab) | tx.gasprice |
| Id | CALLDATALOAD (opens in a new tab), etc. | msg.data |
| Is | CALLER (opens in a new tab) | msg.sender |
| Iv | CALLVALUE (opens in a new tab) | msg.value |
| Ib | CODECOPY (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, etc. |
| Ie | Глубина стека вызовов для вызовов между контрактами (включая создание контракта) | |
| Iw | Разрешено ли EVM изменять состояние, или она работает статически |
Для понимания остальной части раздела 9 необходимы еще несколько параметров:
| Параметр | Определено в разделе | Значение |
|---|---|---|
| σ | 2 (стр. 2, уравнение 1) | Состояние блокчейна |
| g | 9.3 (стр. 13) | Оставшийся газ |
| A | 6.1 (стр. 8) | Накопленное подсостояние (изменения, запланированные на момент завершения транзакции) |
| o | 9.3 (стр. 13) | Вывод — возвращаемый результат в случае внутренней транзакции (когда один контракт вызывает другой) и вызовов функций просмотра (когда вы просто запрашиваете информацию, поэтому нет необходимости ждать транзакции) |
9.4 Обзор исполнения
Теперь, когда у нас есть все предварительные данные, мы можем наконец начать разбираться в том, как работает EVM.
Уравнения 137-142 дают нам начальные условия для запуска EVM:
| Символ | Начальное значение | Значение |
|---|---|---|
| μg | g | Оставшийся газ |
| μpc | 0 | Счетчик команд, адрес следующей инструкции для выполнения |
| μm | (0, 0, ...) | Память, инициализированная нулями |
| μi | 0 | Самая старшая используемая ячейка памяти |
| μ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 говорит нам, что в каждый момент времени во время выполнения возможны четыре условия, и что с ними делать:
Z(σ,μ,A,I). Z представляет собой функцию, которая проверяет, создает ли операция недопустимый переход состояния (см. исключительную остановку). Если она возвращает True, новое состояние идентично старому (за исключением того, что сжигается газ), поскольку изменения не были реализованы.- Если выполняемый код операции —
REVERT(opens in a new tab), новое состояние совпадает со старым состоянием, часть газа теряется. - Если последовательность операций завершена, о чем свидетельствует
RETURN(opens in a new tab)), состояние обновляется до нового состояния. - Если мы не находимся ни в одном из конечных условий 1-3, продолжаем выполнение.
9.4.1 Состояние машины
В этом разделе более подробно объясняется состояние машины. В нем указывается, что w — это текущий код операции. Если μpc меньше ||Ib||, длины кода, то этот байт (Ib[μpc]) является кодом операции. В противном случае код операции определяется как 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 может возвращать три типа значений.
- Если мы не находимся в коде операции остановки, возвращается ∅, пустое множество. По соглашению это значение интерпретируется как логическое ложь (false).
- Если у нас есть код операции остановки, который не производит вывод (либо
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 | ADD | 2 | 1 | Операция сложения. |
| μ′s[0] ≡ μs[0] + μs[1] |
δ — это количество значений, которые мы извлекаем из стека. В данном случае два, потому что мы складываем два верхних значения.
α — это количество значений, которые мы помещаем обратно. В данном случае одно — сумма.
Таким образом, новая вершина стека (μ′s[0]) — это сумма старой вершины стека (μs[0]) и старого значения под ней (μs[1]).
Вместо того чтобы перебирать все коды операций в виде «списка, от которого рябит в глазах», в этой статье объясняются только те коды операций, которые привносят что-то новое.
| Значение | Мнемоника | δ | α | Описание |
|---|---|---|---|---|
| 0x20 | KECCAK256 | 2 | 1 | Вычислить хеш Keccak-256. |
| μ′s[0] ≡ KEC(μm[μs[0] . . . (μs[0] + μs[1] − 1)]) | ||||
| μ′i ≡ M(μi,μs[0],μs[1]) |
Это первый код операции, который обращается к памяти (в данном случае только для чтения). Однако он может выйти за текущие пределы памяти, поэтому нам нужно обновить μi. Мы делаем это с помощью функции M, определенной в уравнении 328 на стр. 29.
| Значение | Мнемоника | δ | α | Описание |
|---|---|---|---|---|
| 0x31 | BALANCE | 1 | 1 | Получить баланс заданного аккаунта. |
| ... |
Адрес, баланс которого нам нужно найти, — это μ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).
| Значение | Мнемоника | δ | α | Описание |
|---|---|---|---|---|
| 0x8F | DUP16 | 16 | 17 | Дублировать 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 полностью определена.
Заключение
Математическая нотация точна и позволила желтой книге специфицировать каждую деталь Эфириума. Однако у нее есть некоторые недостатки:
- Ее могут понять только люди, а это означает, что тесты на соответствие (opens in a new tab) должны быть написаны вручную.
- Программисты понимают компьютерный код. Они могут понимать или не понимать математическую нотацию.
Возможно, по этим причинам более новые спецификации уровня консенсуса (opens in a new tab) написаны на Python. Существуют спецификации уровня исполнения на Python (opens in a new tab), но они не полные. До тех пор, пока вся желтая книга также не будет переведена на Python или аналогичный язык, желтая книга будет продолжать использоваться, и умение ее читать будет полезным.