Розуміння специфікацій EVM у Yellow Paper
Yellow Paper (opens in a new tab) — це формальна специфікація для Ethereum. За винятком випадків, коли вносяться зміни в процесі EIP, він містить точний опис того, як все працює. Він написаний як математична праця, яка містить термінологію, незнайому для програмістів. У цій статті ви дізнаєтеся, як її читати, а отже, й інші пов’язані математичні праці.
Який Yellow Paper?
Як і майже все інше в Ethereum, Yellow Paper з часом розвивається. Щоб мати змогу посилатися на конкретну версію, я завантажив поточну версію на момент написання статті. Номери розділів, сторінок і рівнянь, які я використовую, стосуватимуться цієї версії. Радимо відкрити його в іншому вікні під час читання цього документа.
Навіщо EVM?
Оригінальний yellow paper був написаний на самому початку розробки Ethereum. Він описує оригінальний механізм консенсусу на основі підтвердження роботи (proof-of-work), який спочатку використовувався для захисту мережі. Однак у вересні 2022 року Ethereum відмовився від підтвердження роботи (proof-of-work) і почав використовувати консенсус на основі доказу частки (proof-of-stake). Цей посібник буде зосереджений на частинах yellow paper, що визначають віртуальну машину Ethereum. EVM не зазнала змін унаслідок переходу на доказ частки (proof-of-stake) (за винятком значення, що повертається опкодом 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 також має окрему енергонезалежну модель сховища, яка підтримується як частина стану системи — ця пам’ять організована у вигляді масивів слів (на відміну від байтових масивів у стеку, що адресуються за словами). У цьому сховищі контракти зберігають постійні дані — контракт може взаємодіяти лише з власним сховищем. Сховище організоване у вигляді зіставлень «ключ-значення».
Хоча це не згадується в цьому розділі Yellow Paper, корисно знати, що існує і четвертий тип пам’яті. 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) і т. д. | 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 і т.д. |
| 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 може повертати три типи значень.
- Якщо ми не на опкоді зупинки, повертається ∅, порожня множина. За угодою, це значення інтерпретується як булеве «хибно».
- Якщо у нас є опкод зупинки, який не створює вихідних даних (або
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. Усе, що не вказано як те, що змінюється цим конкретним опкодом, має залишатися незмінним. Змінні, які змінюються, позначаються як <something>′.
Наприклад, розгляньмо опкод 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 повністю визначена.
Висновок
Математична нотація є точною і дозволила Yellow Paper детально описати кожну деталь Ethereum. Однак вона має деякі недоліки:
- Її можуть зрозуміти лише люди, що означає, що тести на відповідність (opens in a new tab) повинні писатися вручну.
- Програмісти розуміють комп’ютерний код. Вони можуть розуміти або не розуміти математичну нотацію.
Можливо, з цих причин новіші специфікації шару консенсусу (opens in a new tab) написані на Python. Існують специфікації виконавчого рівня на Python (opens in a new tab), але вони неповні. Доки весь Yellow Paper не буде перекладено на Python або схожу мову, Yellow Paper залишатиметься в силі, і вміння його читати є корисним.
Останні оновлення сторінки: 1 лютого 2026 р.