Перейти до основного вмісту

Розуміння специфікацій EVM у Жовтій книзі

evm
Середній рівень
qbzzt
15 травня 2022 р.
17 хвилин на читання

Жовта книга (opens in a new tab) — це формальна специфікація Етеріуму. За винятком змін, внесених через процес EIP, вона містить точний опис того, як усе працює. Вона написана як математична стаття, що містить термінологію, яка може бути незнайомою для програмістів. У цій статті ви дізнаєтеся, як її читати, а отже, і як читати інші пов'язані математичні статті.

Яка саме Жовта книга?

Як і майже все інше в Етеріумі, Жовта книга з часом еволюціонує. Щоб мати можливість посилатися на конкретну версію, я завантажив поточну версію на момент написання. Номери розділів, сторінок та рівнянь, які я використовую, посилатимуться на цю версію. Рекомендується тримати її відкритою в іншому вікні під час читання цього документа.

Чому EVM?

Оригінальна Жовта книга була написана на самому початку розробки Етеріуму. Вона описує оригінальний механізм консенсусу на основі доказу виконання роботи (PoW), який спочатку використовувався для захисту мережі. Однак у вересні 2022 року Етеріум відмовився від доказу виконання роботи та почав використовувати консенсус на основі доказу частки (PoS). Цей посібник буде зосереджений на частинах Жовтої книги, що визначають віртуальну машину Етеріуму (EVM). EVM залишилася незмінною після переходу на доказ частки (за винятком значення, що повертається опкодом DIFFICULTY).

9 Модель виконання

Цей розділ (с. 12-14) містить більшу частину визначення EVM.

Термін стан системи (system state) включає все, що потрібно знати про систему для її запуску. У типовому комп'ютері це означає пам'ять, вміст регістрів тощо.

Машина Тюрінга (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)), код для конструктора контракту береться з пам'яті.
  • Під час створення будь-якого контракту виконується код конструктора, а потім повертається код самого контракту, також із пам'яті.

Термін виняткове виконання (exceptional execution) означає виняток, який спричиняє зупинку виконання поточного контракту.

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> ⌉ — це функція округлення в більшу сторону (ceiling), яка для заданого значення повертає найменше ціле число, що не є меншим за це значення. Наприклад, ⌈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⌋ — це функція округлення в меншу сторону (floor), яка для заданого значення повертає найбільше ціле число, що не перевищує це значення. Наприклад, ⌊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), etc.msg.data
IsCALLER (opens in a new tab)msg.sender
IvCALLVALUE (opens in a new tab)msg.value
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, etc.
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 Вичерпання стека (underflow), недостатньо елементів у стеку для поточного опкоду.

  • 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] — це довжина даних. Ця умова виникає, коли ви намагаєтеся прочитати за межами буфера даних повернення. Зверніть увагу, що подібної умови немає для даних виклику або для самого коду. Коли ви намагаєтеся прочитати за межами цих буферів, ви просто отримуєте нулі.

  • || μ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). Число після опкоду логу вказує, скільки тем (topics) містить запис логу.

    • 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).

ЗначенняМнемонікаδαОпис
0x01ADD21Операція додавання.
μ′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 повністю визначена.

Висновок

Математична нотація є точною і дозволила Жовтій книзі визначити кожну деталь Етеріуму. Однак вона має деякі недоліки:

  • Її можуть зрозуміти лише люди, що означає, що тести на відповідність (opens in a new tab) потрібно писати вручну.
  • Програмісти розуміють комп'ютерний код. Вони можуть розуміти або не розуміти математичну нотацію.

Можливо, з цих причин новіші специфікації рівня консенсусу (opens in a new tab) написані на Python. Існують специфікації рівня виконання на Python (opens in a new tab), але вони не є повними. Доки вся Жовта книга також не буде перекладена на Python або подібну мову, Жовта книга продовжуватиме використовуватися, і вміння її читати є корисним.