Přeskočit na hlavní obsah

Pochopení specifikací EVM ze Yellow Paperu

evm
Středně pokročilý
qbzzt
15. května 2022
17 minuta čtení

Yellow Paper (opens in a new tab) je formální specifikace pro Ethereum. Kromě případů, kdy je změněn procesem EIP, obsahuje přesný popis toho, jak vše funguje. Je napsán jako matematická práce, která obsahuje terminologii, jež programátorům nemusí být známá. V tomto dokumentu se dozvíte, jak ho číst, a potažmo i další související matematické práce.

Který Yellow Paper?

Jako téměř všechno ostatní v Ethereu se i Yellow Paper v průběhu času vyvíjí. Abych se mohl odkazovat na konkrétní verzi, nahrál jsem aktuální verzi v době psaní. Čísla sekcí, stránek a rovnic, která používám, se budou vztahovat k této verzi. Při čtení tohoto dokumentu je dobré mít ho otevřený v jiném okně.

Proč EVM?

Původní yellow paper byl napsán hned na začátku vývoje Etherea. Popisuje původní mechanismus konsensu založený na proof-of-work, který byl původně použit k zabezpečení sítě. Ethereum však v září 2022 vypnulo proof-of-work a začalo místo toho používat konsensus založený na proof-of-stake. Tento tutoriál se zaměří na části yellow paperu, které definují Ethereum Virtual Machine. EVM zůstal přechodem na proof-of-stake nezměněn (s výjimkou návratové hodnoty operačního kódu DIFFICULTY).

9 Model provádění

Tato část (str. 12–14) obsahuje většinu definice EVM.

Pojem stav systému zahrnuje vše, co potřebujete o systému vědět, abyste ho mohli spustit. U typického počítače to znamená paměť, obsah registrů atd.

Turingův stroj (opens in a new tab) je výpočetní model. V podstatě se jedná o zjednodušenou verzi počítače, u které je prokázáno, že má stejnou schopnost provádět výpočty jako normální počítač (vše, co dokáže vypočítat počítač, dokáže vypočítat i Turingův stroj a naopak). Tento model usnadňuje dokazování různých teorémů o tom, co je a co není vypočitatelné.

Termín Turingovsky úplný (opens in a new tab) označuje počítač, který může provádět stejné výpočty jako Turingův stroj. Turingovy stroje se mohou dostat do nekonečných smyček, zatímco EVM ne, protože by mu došlo palivo, takže je pouze kvazi-Turingovsky úplný.

9.1 Základy

Tato část uvádí základy EVM a jeho srovnání s jinými výpočetními modely.

Zásobníkový stroj (opens in a new tab) je počítač, který ukládá dočasná data nikoli do registrů, ale do zásobníku (opens in a new tab). Jedná se o preferovanou architekturu pro virtuální stroje, protože se snadno implementuje, což znamená, že chyby a bezpečnostní zranitelnosti jsou mnohem méně pravděpodobné. Paměť v zásobníku je rozdělena na 256bitová slova. Tato velikost byla zvolena, protože je vhodná pro klíčové kryptografické operace Etherea, jako je hašování Keccak-256 a výpočty na eliptických křivkách. Maximální velikost zásobníku je 1024 položek (1024 × 256 bitů). Při provádění operačních kódů se jejich parametry obvykle získávají ze zásobníku. Existují operační kódy speciálně pro reorganizaci prvků v zásobníku, jako jsou POP (odstraní položku z vrcholu zásobníku), DUP_N (duplikuje N-tou položku v zásobníku) atd.

EVM má také volatilní prostor zvaný paměť, který se používá k ukládání dat během provádění. Tato paměť je uspořádána do 32bajtových slov. Všechny paměťové lokace jsou inicializovány na nulu. Pokud provedete tento kód v jazyce Yul (opens in a new tab) pro přidání slova do paměti, vyplní se 32 bajtů paměti doplněním prázdného místa ve slově nulami, tj. vytvoří se jedno slovo – s nulami na pozicích 0–29, 0x60 na pozici 30 a 0xA7 na pozici 31.

1mstore(0, 0x60A7)

mstore je jedním ze tří operačních kódů, které EVM poskytuje pro interakci s pamětí – načítá slovo do paměti. Další dva jsou mstore8, který načítá jeden bajt do paměti, a mload, který přesouvá slovo z paměti do zásobníku.

EVM má také samostatný nevolatilní model úložiště, který je udržován jako součást stavu systému – tato paměť je organizována do polí slov (na rozdíl od bajtových polí adresovatelných po slovech v zásobníku). V tomto úložišti si kontrakty uchovávají trvalá data – kontrakt může interagovat pouze se svým vlastním úložištěm. Úložiště je organizováno jako mapování klíč–hodnota.

Ačkoli to v této části Yellow Paperu není zmíněno, je také užitečné vědět, že existuje čtvrtý typ paměti. Calldata je bajtově adresovatelná paměť pouze pro čtení, která se používá k uložení hodnoty předané parametrem data transakce. EVM má specifické operační kódy pro správu calldata. calldatasize vrací velikost dat. calldataload načítá data do zásobníku. calldatacopy kopíruje data do paměti.

Standardní Von Neumannova architektura (opens in a new tab) ukládá kód a data do stejné paměti. EVM se tohoto standardu z bezpečnostních důvodů nedrží – sdílení volatilní paměti umožňuje měnit kód programu. Místo toho se kód ukládá do úložiště.

Existují pouze dva případy, kdy se kód spouští z paměti:

  • Když kontrakt vytváří jiný kontrakt (pomocí CREATE (opens in a new tab) nebo CREATE2 (opens in a new tab)), kód pro konstruktor kontraktu pochází z paměti.
  • Během vytváření jakéhokoli kontraktu se spustí kód konstruktoru a ten poté vrátí kód samotného kontraktu, také z paměti.

Termín výjimečné provádění znamená výjimku, která způsobí zastavení provádění aktuálního kontraktu.

9.2 Přehled poplatků

Tato část vysvětluje, jak se počítají poplatky za palivo. Existují tři náklady:

Náklady na operační kód

Vlastní náklady na specifický operační kód. Chcete-li získat tuto hodnotu, najděte nákladovou skupinu operačního kódu v Dodatku H (str. 28, pod rovnicí (327)) a nákladovou skupinu v rovnici (324). Tím získáte nákladovou funkci, která ve většině případů používá parametry z Dodatku G (str. 27).

Například operační kód CALLDATACOPY (opens in a new tab) je členem skupiny Wcopy. Náklady na operační kód pro tuto skupinu jsou Gverylow+Gcopy×⌈μs[2]÷32⌉. Při pohledu do Dodatku G vidíme, že obě konstanty jsou 3, což nám dává 3+3×⌈μs[2]÷32⌉.

Stále potřebujeme rozluštit výraz ⌈μs[2]÷32⌉. Nejvzdálenější část, ⌈ <hodnota> ⌉, je funkce horní celé části, funkce, která pro danou hodnotu vrací nejmenší celé číslo, které stále není menší než daná hodnota. Například ⌈2,5⌉ = ⌈3⌉ = 3. Vnitřní část je μs[2]÷32. Při pohledu do části 3 (Konvence) na str. 3 je μ stav stroje. Stav stroje je definován v části 9.4.1 na str. 13. Podle této části je jedním z parametrů stavu stroje s pro zásobník. Když to všechno dáme dohromady, zdá se, že μs[2] je pozice č. 2 v zásobníku. Při pohledu na operační kód (opens in a new tab) je pozice č. 2 v zásobníku velikost dat v bajtech. Při pohledu na ostatní operační kódy ve skupině Wcopy, CODECOPY (opens in a new tab) a RETURNDATACOPY (opens in a new tab), mají také velikost dat na stejné pozici. Takže ⌈μs[2]÷32⌉ je počet 32bajtových slov potřebných k uložení kopírovaných dat. Když vše sečteme, vlastní náklady na CALLDATACOPY (opens in a new tab) jsou 3 jednotky paliva plus 3 za každé slovo kopírovaných dat.

Provozní náklady

Náklady na spuštění kódu, který voláme.

Náklady na rozšíření paměti

Náklady na rozšíření paměti (v případě potřeby).

V rovnici 324 je tato hodnota zapsána jako Cmemi')-Cmemi). Když se znovu podíváme na část 9.4.1, vidíme, že μi je počet slov v paměti. Takže μi je počet slov v paměti před operačním kódem a μi' je počet slov v paměti po operačním kódu.

Funkce Cmem je definována v rovnici 326: Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋. ⌊x⌋ je funkce dolní celé části, funkce, která pro danou hodnotu vrací největší celé číslo, které stále není větší než daná hodnota. Například ⌊2,5⌋ = ⌊2⌋ = 2. Když a < √512, a2 < 512 a výsledek funkce dolní celé části je nula. Takže u prvních 22 slov (704 bajtů) rostou náklady lineárně s počtem požadovaných paměťových slov. Za tímto bodem je ⌊a2 ÷ 512⌋ kladné. Když je požadovaná paměť dostatečně velká, náklady na palivo jsou úměrné druhé mocnině velikosti paměti.

Poznámka: Tyto faktory ovlivňují pouze vlastní náklady na palivo – neberou v úvahu trh s poplatky ani spropitné validátorům, které určují, kolik musí koncový uživatel zaplatit – toto jsou pouze hrubé náklady na provedení konkrétní operace v EVM.

Přečtěte si více o palivu.

9.3 Prostředí pro provádění

Prostředí pro provádění je n-tice, I, která obsahuje informace, jež nejsou součástí stavu blockchainu ani EVM.

ParametrOperační kód pro přístup k datůmKód v jazyce Solidity pro přístup k datům
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), atd.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
IHPole hlavičky bloku, jako je NUMBER (opens in a new tab) a DIFFICULTY (opens in a new tab)block.number, block.difficulty, atd.
IeHloubka zásobníku volání pro volání mezi kontrakty (včetně vytváření kontraktů)
IwJe EVM povoleno měnit stav, nebo běží staticky

Pro pochopení zbytku části 9 je nutných několik dalších parametrů:

ParametrDefinováno v částiVýznam
σ2 (str. 2, rovnice 1)Stav blockchainu
g9.3 (str. 13)Zbývající palivo
A6.1 (str. 8)Nahromaděný podstav (změny naplánované na konec transakce)
o9.3 (str. 13)Výstup – vrácený výsledek v případě interní transakce (když jeden kontrakt volá druhý) a volání funkcí view (když pouze žádáte o informace, takže není třeba čekat na transakci)

9.4 Přehled provádění

Nyní, když máme všechny přípravné práce za sebou, můžeme se konečně začít zabývat tím, jak EVM funguje.

Rovnice 137–142 nám dávají počáteční podmínky pro spuštění EVM:

SymbolPočáteční hodnotaVýznam
μggZbývající palivo
μpc0Čítač instrukcí, adresa další instrukce, která se má provést
μm(0, 0, ...)Paměť, inicializovaná na samé nuly
μi0Nejvyšší použitá paměťová lokace
μs()Zásobník, zpočátku prázdný
μoVýstup, prázdná množina, dokud se nezastavíme buď s návratovými daty (RETURN (opens in a new tab) nebo REVERT (opens in a new tab)), nebo bez nich (STOP (opens in a new tab) nebo SELFDESTRUCT (opens in a new tab)).

Rovnice 143 nám říká, že v každém okamžiku provádění existují čtyři možné podmínky a co s nimi dělat:

  1. Z(σ,μ,A,I). Z představuje funkci, která testuje, zda operace vytváří neplatný přechod stavu (viz výjimečné zastavení). Pokud se vyhodnotí jako Pravda, nový stav je totožný se starým (kromě toho, že se spálí palivo), protože změny nebyly implementovány.
  2. Pokud je prováděným operačním kódem REVERT (opens in a new tab), nový stav je stejný jako starý stav, ztratí se nějaké palivo.
  3. Pokud je sekvence operací dokončena, jak je naznačeno RETURN (opens in a new tab)), stav je aktualizován na nový stav.
  4. Pokud se nenacházíme v jedné z koncových podmínek 1–3, pokračujte v běhu.

9.4.1 Stav stroje

Tato část podrobněji vysvětluje stav stroje. Určuje, že w je aktuální operační kód. Pokud je μpc menší než ||Ib||, délka kódu, pak je tento bajt (Ibpc]) operačním kódem. V opačném případě je operační kód definován jako STOP (opens in a new tab).

Jelikož se jedná o zásobníkový stroj (opens in a new tab), musíme sledovat počet položek vyjmutých (δ) a vložených (α) každým operačním kódem.

9.4.2 Výjimečné zastavení

Tato část definuje funkci Z, která určuje, kdy dochází k abnormálnímu ukončení. Toto je Booleovská (opens in a new tab) funkce, takže používá pro logický součet (neboli OR) (opens in a new tab) a pro logický součin (neboli AND) (opens in a new tab).

K výjimečnému zastavení dojde, pokud je splněna některá z těchto podmínek:

  • μg < C(σ,μ,A,I) Jak jsme viděli v části 9.2, C je funkce, která určuje náklady na palivo. Nezbývá dostatek paliva na pokrytí dalšího operačního kódu.

  • δw=∅ Pokud počet položek vyjmutých pro operační kód není definován, pak je i samotný operační kód nedefinovaný.

  • || μs || < δw Podtečení zásobníku, v zásobníku není dostatek položek pro aktuální operační kód.

  • w = JUMP ∧ μs[0]∉D(Ib) Operační kód je JUMP (opens in a new tab) a adresa není JUMPDEST (opens in a new tab). Skoky jsou platné pouze tehdy, když je cíl JUMPDEST (opens in a new tab).

  • w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) Operační kód je JUMPI (opens in a new tab), podmínka je pravdivá (nenulová), takže by měl nastat skok, a adresa není JUMPDEST (opens in a new tab). Skoky jsou platné pouze tehdy, když je cíl JUMPDEST (opens in a new tab).

  • w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || Operační kód je RETURNDATACOPY (opens in a new tab). V tomto operačním kódu je prvek zásobníku μs[1] offset, od kterého se má číst ve vyrovnávací paměti návratových dat, a prvek zásobníku μs[2] je délka dat. Tato podmínka nastane, když se pokusíte číst za koncem vyrovnávací paměti návratových dat. Všimněte si, že pro calldata nebo pro samotný kód neexistuje podobná podmínka. Když se pokusíte číst za koncem těchto vyrovnávacích pamětí, dostanete pouze nuly.

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

    Přetečení zásobníku. Pokud provedení operačního kódu povede k zásobníku s více než 1024 položkami, operace se přeruší.

  • ¬Iw ∧ W(w,μ) Běžíme staticky (¬ je negace (opens in a new tab) a Iw je pravda, když smíme měnit stav blockchainu)? Pokud ano a pokoušíme se o operaci měnící stav, nemůže k ní dojít.

    Funkce W(w,μ) je definována později v rovnici 150. W(w,μ) je pravda, pokud je splněna jedna z těchto podmínek:

    • w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} Tyto operační kódy mění stav, a to buď vytvořením nového kontraktu, uložením hodnoty, nebo zničením aktuálního kontraktu.

    • LOG0≤w ∧ w≤LOG4 Pokud jsme voláni staticky, nemůžeme vydávat záznamy protokolu. Všechny operační kódy pro protokol jsou v rozsahu mezi LOG0 (A0) (opens in a new tab) a LOG4 (A4) (opens in a new tab). Číslo za operačním kódem protokolu určuje, kolik témat záznam protokolu obsahuje.

    • w=CALL ∧ μs[2]≠0 Můžete volat jiný kontrakt, když jste statický, ale pokud tak učiníte, nemůžete mu převést ETH.

  • w = SSTORE ∧ μg ≤ Gcallstipend Nemůžete spustit SSTORE (opens in a new tab), pokud nemáte více než Gcallstipend (definováno jako 2300 v dodatku G) paliva.

9.4.3 Platnost cíle skoku

Zde formálně definujeme, co jsou operační kódy JUMPDEST (opens in a new tab). Nemůžeme se jen dívat na bajtovou hodnotu 0x5B, protože by mohla být uvnitř PUSH (a tedy data, a ne operační kód).

V rovnici (153) definujeme funkci N(i,w). První parametr, i, je pozice operačního kódu. Druhý, w, je samotný operační kód. Pokud w∈[PUSH1, PUSH32], znamená to, že operační kód je PUSH (hranaté závorky definují rozsah, který zahrnuje koncové body). V takovém případě je další operační kód na pozici i+2+(w−PUSH1). Pro PUSH1 (opens in a new tab) se musíme posunout o dva bajty (samotný PUSH a jednobajtová hodnota), pro PUSH2 (opens in a new tab) se musíme posunout o tři bajty, protože se jedná o dvoubajtovou hodnotu, atd. Všechny ostatní operační kódy EVM jsou dlouhé pouze jeden bajt, takže ve všech ostatních případech je N(i,w)=i+1.

Tato funkce se používá v rovnici (152) k definování DJ(c,i), což je množina (opens in a new tab) všech platných cílů skoku v kódu c, počínaje pozicí operačního kódu i. Tato funkce je definována rekurzivně. Pokud i≥||c||, znamená to, že jsme na konci kódu nebo za ním. Už nenajdeme žádné další cíle skoku, takže jen vrátíme prázdnou množinu.

Ve všech ostatních případech se podíváme na zbytek kódu tak, že přejdeme na další operační kód a získáme množinu, která od něj začíná. c[i] je aktuální operační kód, takže N(i,c[i]) je pozice dalšího operačního kódu. DJ(c,N(i,c[i])) je tedy množina platných cílů skoku, která začíná u dalšího operačního kódu. Pokud aktuální operační kód není JUMPDEST, stačí vrátit tuto množinu. Pokud se jedná o JUMPDEST, zahrňte jej do výsledné množiny a vraťte ji.

9.4.4 Normální zastavení

Funkce zastavení H může vrátit tři typy hodnot.

  • Pokud se nenacházíme v operačním kódu pro zastavení, vraťte , prázdnou množinu. Podle konvence je tato hodnota interpretována jako booleovská hodnota nepravda (false).
  • Pokud máme operační kód zastavení, který neprodukuje výstup (buď STOP (opens in a new tab) nebo SELFDESTRUCT (opens in a new tab)), vraťte jako návratovou hodnotu posloupnost bajtů o velikosti nula. Všimněte si, že se to velmi liší od prázdné množiny. Tato hodnota znamená, že EVM se skutečně zastavil, jen nejsou k dispozici žádná návratová data ke čtení.
  • Pokud máme operační kód zastavení, který produkuje výstup (buď RETURN (opens in a new tab) nebo REVERT (opens in a new tab)), vraťte posloupnost bajtů specifikovanou tímto operačním kódem. Tato sekvence je převzata z paměti, hodnota na vrcholu zásobníku (μs[0]) je první bajt a hodnota za ní (μs[1]) je délka.

H.2 Sada instrukcí

Než přejdeme k poslední podčásti EVM, 9.5, podívejme se na samotné instrukce. Jsou definovány v dodatku H.2, který začíná na str. 29. Vše, co není specifikováno jako měnící se s daným operačním kódem, by mělo zůstat stejné. Proměnné, které se mění, jsou specifikovány jako <něco>'.

Podívejme se například na operační kód ADD (opens in a new tab).

HodnotaMnemotechnická pomůckaδαPopis
0x01ADD21Operace sčítání.
μ′s[0] ≡ μs[0] + μs[1]

δ je počet hodnot, které ze zásobníku vyjmeme. V tomto případě dvě, protože sčítáme dvě horní hodnoty.

α je počet hodnot, které vložíme zpět. V tomto případě jedna, součet.

Takže nový vrchol zásobníku (μ′s[0]) je součet starého vrcholu zásobníku (μs[0]) a staré hodnoty pod ním (μs[1]).

Namísto toho, aby se v tomto článku probíraly všechny operační kódy jako v nudném seznamu, vysvětluje pouze ty operační kódy, které zavádějí něco nového.

HodnotaMnemotechnická pomůckaδαPopis
0x20KECCAK25621Vypočítá haš Keccak-256.
μ′s[0] ≡ KEC(μms[0] . . . (μs[0] + μs[1] − 1)])
μ′i ≡ M(μis[0],μs[1])

Toto je první operační kód, který přistupuje k paměti (v tomto případě pouze pro čtení). Může se však rozšířit za současné limity paměti, takže musíme aktualizovat μi. Děláme to pomocí funkce M definované v rovnici 328 na str. 29.

HodnotaMnemotechnická pomůckaδαPopis
0x31BALANCE11Získá zůstatek daného účtu.
...

Adresa, jejíž zůstatek potřebujeme najít, je μs[0] mod 2160. Na vrcholu zásobníku je adresa, ale protože adresy mají pouze 160 bitů, vypočítáme hodnotu modulo (opens in a new tab) 2160.

Pokud σ[μs[0] mod 2160] ≠ ∅, znamená to, že o této adrese existují informace. V takovém případě je σ[μs[0] mod 2160]b zůstatek pro tuto adresu. Pokud σ[μs[0] mod 2160] = ∅, znamená to, že tato adresa je neinicializovaná a zůstatek je nulový. Seznam polí s informacemi o účtu naleznete v části 4.1 na str. 4.

Druhá rovnice, A'a ≡ Aa ∪ {μs[0] mod 2160}, souvisí s rozdílem v nákladech mezi přístupem do teplého úložiště (úložiště, ke kterému se nedávno přistupovalo a které je pravděpodobně v mezipaměti) a studeného úložiště (úložiště, ke kterému se nepřistupovalo a které je pravděpodobně v pomalejším úložišti, jehož načtení je dražší). Aa je seznam adres, ke kterým transakce dříve přistupovala, a proto by měl být přístup k nim levnější, jak je definováno v části 6.1 na str. 8. Více o tomto tématu si můžete přečíst v EIP-2929 (opens in a new tab).

HodnotaMnemotechnická pomůckaδαPopis
0x8FDUP161617Duplikovat 16. položku zásobníku.
μ′s[0] ≡ μs[15]

Všimněte si, že pro použití jakékoli položky zásobníku ji musíme vyjmout (pop), což znamená, že musíme také vyjmout všechny položky zásobníku nad ní. V případě DUP<n> (opens in a new tab) a SWAP<n> (opens in a new tab) to znamená, že je třeba vyjmout (pop) a poté vložit (push) až šestnáct hodnot.

9.5 Prováděcí cyklus

Nyní, když máme všechny části, můžeme konečně pochopit, jak je zdokumentován prováděcí cyklus EVM.

Rovnice (155) říká, že pro daný stav:

  • σ (globální stav blockchainu)
  • μ (stav EVM)
  • A (podstav, změny, které se mají provést po skončení transakce)
  • I (prostředí pro provádění)

Nový stav je (σ', μ', A', I').

Rovnice (156)–(158) definují zásobník a změnu v něm v důsledku operačního kódu (μs). Rovnice (159) je změna paliva (μg). Rovnice (160) je změna v čítači instrukcí (μpc). Nakonec rovnice (161)–(164) specifikují, že ostatní parametry zůstávají stejné, pokud nejsou explicitně změněny operačním kódem.

Tím je EVM plně definován.

Závěr

Matematický zápis je přesný a umožnil Yellow Paperu specifikovat každý detail Etherea. Má však i některé nevýhody:

  • Mohou mu rozumět pouze lidé, což znamená, že testy shody (opens in a new tab) se musí psát ručně.
  • Programátoři rozumí počítačovému kódu. Matematickému zápisu rozumět mohou, ale nemusí.

Možná z těchto důvodů jsou novější specifikace konsensuální vrstvy (opens in a new tab) napsány v Pythonu. Existují specifikace exekuční vrstvy v Pythonu (opens in a new tab), ale nejsou úplné. Dokud nebude celý Yellow Paper přeložen do Pythonu nebo podobného jazyka, bude Yellow Paper nadále sloužit a je užitečné umět ho číst.

Stránka naposledy aktualizována: 1. února 2026

Byl tento tutoriál užitečný?