Zrozumienie specyfikacji EVM w żółtej księdze
Żółta księga (opens in a new tab) to formalna specyfikacja Ethereum. Z wyjątkiem poprawek wprowadzonych przez proces EIP, zawiera ona dokładny opis tego, jak wszystko działa. Jest napisana w formie pracy matematycznej, co obejmuje terminologię, która może nie być znana programistom. Z tego artykułu dowiesz się, jak ją czytać, a co za tym idzie, jak czytać inne powiązane prace matematyczne.
Która żółta księga?
Jak prawie wszystko w Ethereum, żółta księga ewoluuje w czasie. Aby móc odnieść się do konkretnej wersji, przesłałem wersję aktualną w momencie pisania. Numery sekcji, stron i równań, których używam, będą odnosić się do tej wersji. Dobrym pomysłem jest otwarcie jej w innym oknie podczas czytania tego dokumentu.
Dlaczego EVM?
Oryginalna żółta księga została napisana na samym początku rozwoju Ethereum. Opisuje ona oryginalny mechanizm konsensusu oparty na dowodzie pracy (PoW), który był pierwotnie używany do zabezpieczania sieci. Jednakże we wrześniu 2022 roku Ethereum wyłączyło dowód pracy i zaczęło używać konsensusu opartego na dowodzie stawki (PoS). Ten samouczek skupi się na częściach żółtej księgi definiujących maszynę wirtualną Ethereum (EVM). EVM pozostała niezmieniona po przejściu na dowód stawki (z wyjątkiem wartości zwracanej przez kod operacji DIFFICULTY).
9 Model wykonania
Ta sekcja (str. 12-14) zawiera większość definicji EVM.
Termin stan systemu obejmuje wszystko, co musisz wiedzieć o systemie, aby go uruchomić. W typowym komputerze oznacza to pamięć, zawartość rejestrów itp.
Maszyna Turinga (opens in a new tab) to model obliczeniowy. Zasadniczo jest to uproszczona wersja komputera, o której udowodniono, że ma taką samą zdolność do wykonywania obliczeń jak normalny komputer (wszystko, co może obliczyć komputer, może obliczyć maszyna Turinga i odwrotnie). Ten model ułatwia udowadnianie różnych twierdzeń o tym, co jest, a co nie jest obliczalne.
Termin kompletność w sensie Turinga (opens in a new tab) oznacza komputer, który może wykonywać te same obliczenia co maszyna Turinga. Maszyny Turinga mogą wpaść w nieskończone pętle, a EVM nie może, ponieważ zabrakłoby jej gazu, więc jest tylko quasi-kompletna w sensie Turinga.
9.1 Podstawy
Ta sekcja przedstawia podstawy EVM i jej porównanie z innymi modelami obliczeniowymi.
Maszyna stosowa (opens in a new tab) to komputer, który przechowuje dane pośrednie nie w rejestrach, ale na stosie (opens in a new tab). Jest to preferowana architektura dla maszyn wirtualnych, ponieważ jest łatwa do wdrożenia, co oznacza, że błędy i luki w zabezpieczeniach są znacznie mniej prawdopodobne. Pamięć na stosie jest podzielona na 256-bitowe słowa. Zostało to wybrane, ponieważ jest to wygodne dla podstawowych operacji kryptograficznych Ethereum, takich jak haszowanie Keccak-256 i obliczenia na krzywych eliptycznych. Maksymalny rozmiar stosu to 1024 elementy (1024 x 256 bitów). Kiedy kody operacji są wykonywane, zazwyczaj pobierają swoje parametry ze stosu. Istnieją kody operacji przeznaczone specjalnie do reorganizacji elementów na stosie, takie jak POP (usuwa element ze szczytu stosu), DUP_N (duplikuje N-ty element na stosie) itp.
EVM posiada również ulotną przestrzeń zwaną pamięcią (memory), która służy do przechowywania danych podczas wykonywania. Ta pamięć jest zorganizowana w 32-bajtowe słowa. Wszystkie lokalizacje w pamięci są inicjowane zerami. Jeśli wykonasz ten kod Yul (opens in a new tab), aby dodać słowo do pamięci, wypełni on 32 bajty pamięci, uzupełniając puste miejsce w słowie zerami, tj. utworzy jedno słowo - z zerami w lokalizacjach 0-29, 0x60 w 30 i 0xA7 w 31.
mstore(0, 0x60A7)
mstore to jeden z trzech kodów operacji, które EVM udostępnia do interakcji z pamięcią - ładuje on słowo do pamięci. Pozostałe dwa to mstore8, który ładuje pojedynczy bajt do pamięci, oraz mload, który przenosi słowo z pamięci na stos.
EVM posiada również oddzielny, nieulotny model pamięci masowej (storage), który jest utrzymywany jako część stanu systemu - ta pamięć jest zorganizowana w tablice słów (w przeciwieństwie do adresowalnych słowami tablic bajtów na stosie). W tej pamięci masowej kontrakty przechowują trwałe dane - kontrakt może wchodzić w interakcje tylko z własną pamięcią masową. Pamięć masowa jest zorganizowana w mapowania klucz-wartość.
Chociaż nie wspomniano o tym w tej sekcji żółtej księgi, warto również wiedzieć, że istnieje czwarty rodzaj pamięci. Dane wywołania (calldata) to adresowalna bajtowo pamięć tylko do odczytu, używana do przechowywania wartości przekazanej z parametrem data transakcji. EVM posiada specyficzne kody operacji do zarządzania calldata. calldatasize zwraca rozmiar danych. calldataload ładuje dane na stos. calldatacopy kopiuje dane do pamięci.
Standardowa architektura von Neumanna (opens in a new tab) przechowuje kod i dane w tej samej pamięci. EVM nie podąża za tym standardem ze względów bezpieczeństwa - współdzielenie pamięci ulotnej umożliwia zmianę kodu programu. Zamiast tego kod jest zapisywany w pamięci masowej.
Istnieją tylko dwa przypadki, w których kod jest wykonywany z pamięci:
- Kiedy kontrakt tworzy inny kontrakt (używając
CREATE(opens in a new tab) lubCREATE2(opens in a new tab)), kod konstruktora kontraktu pochodzi z pamięci. - Podczas tworzenia dowolnego kontraktu, kod konstruktora jest uruchamiany, a następnie zwraca kod właściwego kontraktu, również z pamięci.
Termin wyjątkowe wykonanie (exceptional execution) oznacza wyjątek, który powoduje zatrzymanie wykonywania bieżącego kontraktu.
9.2 Przegląd opłat
Ta sekcja wyjaśnia, jak obliczane są opłaty za gaz. Istnieją trzy koszty:
Koszt kodu operacji
Nieodłączny koszt konkretnego kodu operacji. Aby uzyskać tę wartość, znajdź grupę kosztów kodu operacji w Dodatku H (str. 28, pod równaniem (327)) i znajdź grupę kosztów w równaniu (324). Daje to funkcję kosztu, która w większości przypadków wykorzystuje parametry z Dodatku G (str. 27).
Na przykład kod operacji CALLDATACOPY (opens in a new tab) jest członkiem grupy Wcopy. Koszt kodu operacji dla tej grupy to Gverylow+Gcopy×⌈μs[2]÷32⌉. Patrząc na Dodatek G, widzimy, że obie stałe wynoszą 3, co daje nam 3+3×⌈μs[2]÷32⌉.
Musimy jeszcze rozszyfrować wyrażenie ⌈μs[2]÷32⌉. Zewnętrzna część, ⌈ <value> ⌉ to funkcja sufitu (ceiling), funkcja, która dla danej wartości zwraca najmniejszą liczbę całkowitą, która wciąż nie jest mniejsza od tej wartości. Na przykład ⌈2.5⌉ = ⌈3⌉ = 3. Wewnętrzna część to μs[2]÷32. Patrząc na sekcję 3 (Konwencje) na str. 3, μ to stan maszyny. Stan maszyny jest zdefiniowany w sekcji 9.4.1 na str. 13. Zgodnie z tą sekcją, jednym z parametrów stanu maszyny jest s dla stosu. Łącząc to wszystko, wydaje się, że μs[2] to lokalizacja nr 2 na stosie. Patrząc na kod operacji (opens in a new tab), lokalizacja nr 2 na stosie to rozmiar danych w bajtach. Patrząc na inne kody operacji w grupie Wcopy, CODECOPY (opens in a new tab) i RETURNDATACOPY (opens in a new tab), one również mają rozmiar danych w tej samej lokalizacji. Więc ⌈μs[2]÷32⌉ to liczba 32-bajtowych słów wymaganych do przechowania kopiowanych danych. Podsumowując, nieodłączny koszt CALLDATACOPY (opens in a new tab) to 3 jednostki gazu plus 3 za każde kopiowane słowo danych.
Koszt uruchomienia
Koszt uruchomienia kodu, który wywołujemy.
- W przypadku
CREATE(opens in a new tab) iCREATE2(opens in a new tab), konstruktor dla nowego kontraktu. - W przypadku
CALL(opens in a new tab),CALLCODE(opens in a new tab),STATICCALL(opens in a new tab) lubDELEGATECALL(opens in a new tab), kontrakt, który wywołujemy.
Koszt rozszerzenia pamięci
Koszt rozszerzenia pamięci (jeśli to konieczne).
W równaniu 324 ta wartość jest zapisana jako Cmem(μi')-Cmem(μi). Patrząc ponownie na sekcję 9.4.1, widzimy, że μi to liczba słów w pamięci. Więc μi to liczba słów w pamięci przed kodem operacji, a μi' to liczba słów w pamięci po kodzie operacji.
Funkcja Cmem jest zdefiniowana w równaniu 326: Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋. ⌊x⌋ to funkcja podłogi (floor), funkcja, która dla danej wartości zwraca największą liczbę całkowitą, która wciąż nie jest większa od tej wartości. Na przykład ⌊2.5⌋ = ⌊2⌋ = 2. Kiedy a < √512, a2 < 512, a wynik funkcji podłogi wynosi zero. Więc dla pierwszych 22 słów (704 bajtów) koszt rośnie liniowo wraz z liczbą wymaganych słów pamięci. Powyżej tego punktu ⌊a2 ÷ 512⌋ jest dodatnie. Gdy wymagana pamięć jest wystarczająco duża, koszt gazu jest proporcjonalny do kwadratu ilości pamięci.
Uwaga, te czynniki wpływają tylko na nieodłączny koszt gazu - nie biorą pod uwagę rynku opłat ani napiwków dla walidatorów, które określają, ile użytkownik końcowy musi zapłacić - jest to tylko surowy koszt uruchomienia konkretnej operacji w EVM.
9.3 Środowisko wykonawcze
Środowisko wykonawcze to krotka I, która zawiera informacje niebędące częścią stanu blockchaina ani EVM.
| Parametr | Kod operacji dostępu do danych | Kod Solidity dostępu do danych |
|---|---|---|
| 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), itp. | 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 | Pola nagłówka bloku, takie jak NUMBER (opens in a new tab) i DIFFICULTY (opens in a new tab) | block.number, block.difficulty, itp. |
| Ie | Głębokość stosu wywołań dla wywołań między kontraktami (w tym tworzenia kontraktów) | |
| Iw | Czy EVM ma pozwolenie na zmianę stanu, czy działa statycznie |
Kilka innych parametrów jest niezbędnych do zrozumienia reszty sekcji 9:
| Parametr | Zdefiniowane w sekcji | Znaczenie |
|---|---|---|
| σ | 2 (str. 2, równanie 1) | Stan blockchaina |
| g | 9.3 (str. 13) | Pozostały gaz |
| A | 6.1 (str. 8) | Naliczony podstan (zmiany zaplanowane na moment zakończenia transakcji) |
| o | 9.3 (str. 13) | Wyjście - zwrócony wynik w przypadku transakcji wewnętrznej (gdy jeden kontrakt wywołuje inny) i wywołań funkcji widoku (gdy tylko prosisz o informacje, więc nie ma potrzeby czekać na transakcję) |
9.4 Przegląd wykonania
Teraz, gdy mamy już wszystkie wstępne informacje, możemy wreszcie zacząć pracę nad tym, jak działa EVM.
Równania 137-142 dają nam warunki początkowe do uruchomienia EVM:
| Symbol | Wartość początkowa | Znaczenie |
|---|---|---|
| μg | g | Pozostały gaz |
| μpc | 0 | Licznik programu, adres następnej instrukcji do wykonania |
| μm | (0, 0, ...) | Pamięć, zainicjowana samymi zerami |
| μi | 0 | Najwyższa użyta lokalizacja pamięci |
| μs | () | Stos, początkowo pusty |
| μo | ∅ | Wyjście, zbiór pusty, dopóki nie zatrzymamy się ze zwracanymi danymi (RETURN (opens in a new tab) lub REVERT (opens in a new tab)) lub bez nich (STOP (opens in a new tab) lub SELFDESTRUCT (opens in a new tab)). |
Równanie 143 mówi nam, że w każdym momencie podczas wykonywania istnieją cztery możliwe warunki i co z nimi zrobić:
Z(σ,μ,A,I). Z reprezentuje funkcję, która testuje, czy operacja tworzy nieprawidłowe przejście stanu (zobacz wyjątkowe zatrzymanie). Jeśli jej wynikiem jest Prawda, nowy stan jest identyczny ze starym (z wyjątkiem tego, że gaz zostaje spalony), ponieważ zmiany nie zostały wdrożone.- Jeśli wykonywanym kodem operacji jest
REVERT(opens in a new tab), nowy stan jest taki sam jak stary stan, tracona jest pewna ilość gazu. - Jeśli sekwencja operacji jest zakończona, co sygnalizuje
RETURN(opens in a new tab)), stan jest aktualizowany do nowego stanu. - Jeśli nie znajdujemy się w jednym z warunków końcowych 1-3, kontynuuj działanie.
9.4.1 Stan maszyny
Ta sekcja wyjaśnia stan maszyny bardziej szczegółowo. Określa, że w to bieżący kod operacji. Jeśli μpc jest mniejsze niż ||Ib||, długość kodu, to ten bajt (Ib[μpc]) jest kodem operacji. W przeciwnym razie kod operacji jest zdefiniowany jako STOP (opens in a new tab).
Ponieważ jest to maszyna stosowa (opens in a new tab), musimy śledzić liczbę elementów zdjętych (δ) i odłożonych (α) przez każdy kod operacji.
9.4.2 Wyjątkowe zatrzymanie
Ta sekcja definiuje funkcję Z, która określa, kiedy mamy do czynienia z nieprawidłowym zakończeniem. Jest to funkcja logiczna (zmienna typu Boolean) (opens in a new tab), więc używa ∨ dla logicznego lub (OR) (opens in a new tab) i ∧ dla logicznego i (AND) (opens in a new tab).
Mamy wyjątkowe zatrzymanie, jeśli którykolwiek z tych warunków jest prawdziwy:
-
μg < C(σ,μ,A,I) Jak widzieliśmy w sekcji 9.2, C to funkcja określająca koszt gazu. Nie ma wystarczającej ilości gazu, aby pokryć następny kod operacji.
-
δw=∅ Jeśli liczba elementów zdjętych dla kodu operacji jest niezdefiniowana, to sam kod operacji jest niezdefiniowany.
-
|| μs || < δw Niedomiar stosu, za mało elementów na stosie dla bieżącego kodu operacji.
-
w = JUMP ∧ μs[0]∉D(Ib) Kodem operacji jest
JUMP(opens in a new tab), a adres nie jestJUMPDEST(opens in a new tab). Skoki są ważne tylko wtedy, gdy miejscem docelowym jestJUMPDEST(opens in a new tab). -
w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) Kodem operacji jest
JUMPI(opens in a new tab), warunek jest prawdziwy (niezerowy), więc skok powinien nastąpić, a adres nie jestJUMPDEST(opens in a new tab). Skoki są ważne tylko wtedy, gdy miejscem docelowym jestJUMPDEST(opens in a new tab). -
w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || Kodem operacji jest
RETURNDATACOPY(opens in a new tab). W tym kodzie operacji element stosu μs[1] to przesunięcie, od którego należy czytać w buforze zwracanych danych, a element stosu μs[2] to długość danych. Ten warunek występuje, gdy próbujesz czytać poza końcem bufora zwracanych danych. Zauważ, że nie ma podobnego warunku dla danych wywołania (calldata) ani dla samego kodu. Kiedy próbujesz czytać poza końcem tych buforów, po prostu otrzymujesz zera. -
|| μs || - δw + αw > 1024
Przepełnienie stosu. Jeśli uruchomienie kodu operacji spowoduje, że na stosie znajdzie się ponad 1024 elementów, przerwij.
-
¬Iw ∧ W(w,μ) Czy działamy statycznie (¬ to negacja (opens in a new tab), a Iw jest prawdziwe, gdy mamy pozwolenie na zmianę stanu blockchaina)? Jeśli tak, a próbujemy wykonać operację zmieniającą stan, nie może się to wydarzyć.
Funkcja W(w,μ) jest zdefiniowana później w równaniu 150. W(w,μ) jest prawdziwe, jeśli jeden z tych warunków jest prawdziwy:
-
w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} Te kody operacji zmieniają stan, tworząc nowy kontrakt, przechowując wartość lub niszcząc bieżący kontrakt.
-
LOG0≤w ∧ w≤LOG4 Jeśli jesteśmy wywoływani statycznie, nie możemy emitować wpisów logów. Wszystkie kody operacji logów znajdują się w przedziale od
LOG0(A0) (opens in a new tab) doLOG4(A4) (opens in a new tab). Liczba po kodzie operacji logu określa, ile tematów zawiera wpis logu. -
w=CALL ∧ μs[2]≠0 Możesz wywołać inny kontrakt, gdy jesteś statyczny, ale jeśli to zrobisz, nie możesz przesłać do niego ETH.
-
-
w = SSTORE ∧ μg ≤ Gcallstipend Nie możesz uruchomić
SSTORE(opens in a new tab), chyba że masz więcej niż Gcallstipend (zdefiniowane jako 2300 w Dodatku G) gazu.
9.4.3 Ważność miejsca docelowego skoku
Tutaj formalnie definiujemy, czym są kody operacji JUMPDEST (opens in a new tab). Nie możemy po prostu szukać wartości bajtu 0x5B, ponieważ może ona znajdować się wewnątrz PUSH (i w związku z tym być danymi, a nie kodem operacji).
W równaniu (153) definiujemy funkcję N(i,w). Pierwszy parametr, i, to lokalizacja kodu operacji. Drugi, w, to sam kod operacji. Jeśli w∈[PUSH1, PUSH32], oznacza to, że kodem operacji jest PUSH (nawiasy kwadratowe definiują przedział, który obejmuje punkty końcowe). W takim przypadku następny kod operacji znajduje się pod adresem i+2+(w−PUSH1). Dla PUSH1 (opens in a new tab) musimy przesunąć się o dwa bajty (samo PUSH i jednobajtowa wartość), dla PUSH2 (opens in a new tab) musimy przesunąć się o trzy bajty, ponieważ jest to wartość dwubajtowa itp. Wszystkie inne kody operacji EVM mają długość tylko jednego bajtu, więc we wszystkich innych przypadkach N(i,w)=i+1.
Ta funkcja jest używana w równaniu (152) do zdefiniowania DJ(c,i), co jest zbiorem (opens in a new tab) wszystkich ważnych miejsc docelowych skoków w kodzie c, zaczynając od lokalizacji kodu operacji i. Ta funkcja jest zdefiniowana rekurencyjnie. Jeśli i≥||c||, oznacza to, że jesteśmy na końcu lub po końcu kodu. Nie znajdziemy już żadnych miejsc docelowych skoków, więc po prostu zwracamy zbiór pusty.
We wszystkich innych przypadkach patrzymy na resztę kodu, przechodząc do następnego kodu operacji i pobierając zbiór zaczynający się od niego. c[i] to bieżący kod operacji, więc N(i,c[i]) to lokalizacja następnego kodu operacji. DJ(c,N(i,c[i])) jest zatem zbiorem ważnych miejsc docelowych skoków, który zaczyna się od następnego kodu operacji. Jeśli bieżący kod operacji nie jest JUMPDEST, po prostu zwróć ten zbiór. Jeśli jest to JUMPDEST, dołącz go do zbioru wynikowego i zwróć go.
9.4.4 Normalne zatrzymanie
Funkcja zatrzymania H może zwracać trzy typy wartości.
- Jeśli nie jesteśmy w kodzie operacji zatrzymania, zwróć ∅, zbiór pusty. Zgodnie z konwencją, ta wartość jest interpretowana jako logiczny fałsz.
- Jeśli mamy kod operacji zatrzymania, który nie generuje wyjścia (albo
STOP(opens in a new tab), alboSELFDESTRUCT(opens in a new tab)), zwróć sekwencję o rozmiarze zero bajtów jako wartość zwracaną. Zauważ, że bardzo różni się to od zbioru pustego. Ta wartość oznacza, że EVM naprawdę się zatrzymała, po prostu nie ma żadnych zwracanych danych do odczytania. - Jeśli mamy kod operacji zatrzymania, który generuje wyjście (albo
RETURN(opens in a new tab), alboREVERT(opens in a new tab)), zwróć sekwencję bajtów określoną przez ten kod operacji. Ta sekwencja jest pobierana z pamięci, wartość na szczycie stosu (μs[0]) to pierwszy bajt, a wartość po niej (μs[1]) to długość.
H.2 Zestaw instrukcji
Zanim przejdziemy do ostatniej podsekcji EVM, 9.5, przyjrzyjmy się samym instrukcjom. Są one zdefiniowane w Dodatku H.2, który zaczyna się na str. 29. Oczekuje się, że wszystko, co nie zostało określone jako zmieniające się wraz z tym konkretnym kodem operacji, pozostanie takie samo. Zmienne, które ulegają zmianie, są określane jako <coś>′.
Na przykład spójrzmy na kod operacji ADD (opens in a new tab).
| Wartość | Mnemonik | δ | α | Opis |
|---|---|---|---|---|
| 0x01 | ADD | 2 | 1 | Operacja dodawania. |
| μ′s[0] ≡ μs[0] + μs[1] |
δ to liczba wartości, które zdejmujemy ze stosu. W tym przypadku dwie, ponieważ dodajemy dwie górne wartości.
α to liczba wartości, które odkładamy z powrotem. W tym przypadku jedna, suma.
Więc nowy szczyt stosu (μ′s[0]) to suma starego szczytu stosu (μs[0]) i starej wartości pod nim (μs[1]).
Zamiast przeglądać wszystkie kody operacji w postaci "usypiającej listy", ten artykuł wyjaśnia tylko te kody operacji, które wprowadzają coś nowego.
| Wartość | Mnemonik | δ | α | Opis |
|---|---|---|---|---|
| 0x20 | KECCAK256 | 2 | 1 | Oblicz hash Keccak-256. |
| μ′s[0] ≡ KEC(μm[μs[0] . . . (μs[0] + μs[1] − 1)]) | ||||
| μ′i ≡ M(μi,μs[0],μs[1]) |
To pierwszy kod operacji, który uzyskuje dostęp do pamięci (w tym przypadku tylko do odczytu). Jednakże może on wykraczać poza obecne limity pamięci, więc musimy zaktualizować μi. Robimy to za pomocą funkcji M zdefiniowanej w równaniu 328 na str. 29.
| Wartość | Mnemonik | δ | α | Opis |
|---|---|---|---|---|
| 0x31 | BALANCE | 1 | 1 | Pobierz saldo danego konta. |
| ... |
Adres, którego saldo musimy znaleźć, to μs[0] mod 2160. Szczyt stosu to adres, ale ponieważ adresy mają tylko 160 bitów, obliczamy wartość modulo (opens in a new tab) 2160.
Jeśli σ[μs[0] mod 2160] ≠ ∅, oznacza to, że istnieją informacje o tym adresie. W takim przypadku σ[μs[0] mod 2160]b to saldo dla tego adresu. Jeśli σ[μs[0] mod 2160] = ∅, oznacza to, że ten adres jest niezainicjowany, a saldo wynosi zero. Listę pól informacji o koncie można zobaczyć w sekcji 4.1 na str. 4.
Drugie równanie, A'a ≡ Aa ∪ {μs[0] mod 2160}, jest związane z różnicą w kosztach między dostępem do ciepłej pamięci masowej (pamięci masowej, do której niedawno uzyskano dostęp i która prawdopodobnie znajduje się w pamięci podręcznej) a zimnej pamięci masowej (pamięci masowej, do której nie uzyskano dostępu i która prawdopodobnie znajduje się w wolniejszej pamięci masowej, której pobranie jest droższe). Aa to lista adresów, do których transakcja uzyskała wcześniej dostęp, a zatem dostęp do nich powinien być tańszy, jak zdefiniowano w sekcji 6.1 na str. 8. Możesz przeczytać więcej na ten temat w EIP-2929 (opens in a new tab).
| Wartość | Mnemonik | δ | α | Opis |
|---|---|---|---|---|
| 0x8F | DUP16 | 16 | 17 | Zduplikuj 16. element stosu. |
| μ′s[0] ≡ μs[15] |
Zauważ, że aby użyć dowolnego elementu stosu, musimy go zdjąć, co oznacza, że musimy również zdjąć wszystkie elementy stosu znajdujące się nad nim. W przypadku DUP<n> (opens in a new tab) i SWAP<n> (opens in a new tab) oznacza to konieczność zdjęcia, a następnie odłożenia do szesnastu wartości.
9.5 Cykl wykonania
Teraz, gdy mamy już wszystkie części, możemy wreszcie zrozumieć, jak udokumentowany jest cykl wykonania EVM.
Równanie (155) mówi, że biorąc pod uwagę stan:
- σ (globalny stan blockchaina)
- μ (stan EVM)
- A (podstan, zmiany, które mają nastąpić po zakończeniu transakcji)
- I (środowisko wykonawcze)
Nowy stan to (σ', μ', A', I').
Równania (156)-(158) definiują stos i jego zmianę spowodowaną kodem operacji (μs). Równanie (159) to zmiana gazu (μg). Równanie (160) to zmiana licznika programu (μpc). Wreszcie, równania (161)-(164) określają, że pozostałe parametry pozostają takie same, chyba że zostaną wyraźnie zmienione przez kod operacji.
Dzięki temu EVM jest w pełni zdefiniowana.
Wnioski
Notacja matematyczna jest precyzyjna i pozwoliła żółtej księdze określić każdy szczegół Ethereum. Ma jednak pewne wady:
- Może być zrozumiana tylko przez ludzi, co oznacza, że testy zgodności (opens in a new tab) muszą być pisane ręcznie.
- Programiści rozumieją kod komputerowy. Mogą, ale nie muszą rozumieć notacji matematycznej.
Być może z tych powodów nowsze specyfikacje warstwy konsensusu (opens in a new tab) są napisane w języku Python. Istnieją specyfikacje warstwy wykonawczej w języku Python (opens in a new tab), ale nie są one kompletne. Dopóki cała żółta księga nie zostanie również przetłumaczona na język Python lub podobny, żółta księga będzie nadal w użyciu i warto umieć ją czytać.