Zum Hauptinhalt springen

Die EVM-Spezifikationen des Yellow Papers verstehen

evm
Fortgeschritten
qbzzt
15. Mai 2022
19 Minuten Lesezeit

Das Yellow Paper (öffnet in einem neuen Tab) ist die formale Spezifikation für Ethereum. Abgesehen von Änderungen durch den EIP-Prozess enthält es die genaue Beschreibung, wie alles funktioniert. Es ist als mathematisches Papier verfasst, das Terminologie enthält, die Programmierern möglicherweise nicht vertraut ist. In diesem Artikel lernen Sie, wie man es liest, und im weiteren Sinne auch andere verwandte mathematische Papiere.

Welches Yellow Paper?

Wie fast alles andere bei Ethereum entwickelt sich auch das Yellow Paper im Laufe der Zeit weiter. Um mich auf eine bestimmte Version beziehen zu können, habe ich die zum Zeitpunkt des Schreibens aktuelle Version (öffnet in einem neuen Tab) hochgeladen. Die von mir verwendeten Abschnitts-, Seiten- und Gleichungsnummern beziehen sich auf diese Version. Es ist eine gute Idee, es in einem anderen Fenster geöffnet zu haben, während Sie dieses Dokument lesen.

Warum die EVM?

Das ursprüngliche Yellow Paper wurde direkt zu Beginn der Entwicklung von Ethereum geschrieben. Es beschreibt den ursprünglichen auf Proof-of-Work (PoW) basierenden Konsensmechanismus, der ursprünglich zur Sicherung des Netzwerks verwendet wurde. Ethereum hat jedoch Proof-of-Work abgeschaltet und im September 2022 begonnen, einen auf Proof-of-Stake (PoS) basierenden Konsens zu verwenden. Dieses Tutorial konzentriert sich auf die Teile des Yellow Papers, die die Ethereum Virtual Machine definieren. Die EVM blieb durch den Übergang zu Proof-of-Stake unverändert (mit Ausnahme des Rückgabewerts des Opcodes DIFFICULTY).

9 Ausführungsmodell

Dieser Abschnitt (S. 14-16) enthält den Großteil der Definition der EVM.

Der Begriff Systemzustand (system state) umfasst alles, was Sie über das System wissen müssen, um es auszuführen. In einem typischen Computer bedeutet dies den Speicher, den Inhalt von Registern usw.

Eine Turingmaschine (öffnet in einem neuen Tab) ist ein Berechnungsmodell. Im Wesentlichen handelt es sich um eine vereinfachte Version eines Computers, von der bewiesen ist, dass sie dieselbe Fähigkeit zur Ausführung von Berechnungen besitzt wie ein normaler Computer (alles, was ein Computer berechnen kann, kann auch eine Turingmaschine berechnen und umgekehrt). Dieses Modell erleichtert es, verschiedene Theoreme darüber zu beweisen, was berechenbar ist und was nicht.

Der Begriff Turing-vollständig (öffnet in einem neuen Tab) bezeichnet einen Computer, der dieselben Berechnungen wie eine Turingmaschine ausführen kann. Turingmaschinen können in Endlosschleifen geraten, die EVM jedoch nicht, da ihr das Gas ausgehen würde. Daher ist sie nur quasi-Turing-vollständig.

9.1 Grundlagen

Dieser Abschnitt vermittelt die Grundlagen der EVM und wie sie im Vergleich zu anderen Berechnungsmodellen abschneidet.

Eine Kellermaschine (Stack Machine) (öffnet in einem neuen Tab) ist ein Computer, der Zwischendaten nicht in Registern, sondern in einem Stack (Stapel) (öffnet in einem neuen Tab) speichert. Dies ist die bevorzugte Architektur für virtuelle Maschinen, da sie einfach zu implementieren ist, was bedeutet, dass Fehler und Sicherheitslücken viel unwahrscheinlicher sind. Der Speicher im Stack ist in 256-Bit-Wörter unterteilt. Dies wurde gewählt, weil es für die kryptografischen Kernoperationen von Ethereum wie Keccak-256-Hashing und Berechnungen auf elliptischen Kurven praktisch ist. Die maximale Größe des Stacks beträgt 1024 Elemente (1024 x 256 Bit). Wenn Opcodes ausgeführt werden, beziehen sie ihre Parameter normalerweise aus dem Stack. Es gibt Opcodes speziell für die Neuorganisation von Elementen im Stack, wie z. B. POP (entfernt das oberste Element vom Stack), DUP_N (dupliziert das N-te Element im Stack) usw.

Die EVM verfügt auch über einen flüchtigen Bereich namens Memory (Speicher), der zum Speichern von Daten während der Ausführung verwendet wird. Dieser Speicher ist in 32-Byte-Wörter unterteilt. Alle Speicherorte werden mit null initialisiert. Wenn Sie diesen Yul (öffnet in einem neuen Tab)-Code ausführen, um dem Speicher ein Wort hinzuzufügen, füllt er 32 Byte Speicher, indem er den leeren Platz im Wort mit Nullen auffüllt, d. h. er erstellt ein Wort – mit Nullen an den Positionen 0-29, 0x60 an 30 und 0xA7 an 31.

mstore(0, 0x60A7)

mstore ist einer von drei Opcodes, die die EVM für die Interaktion mit dem Speicher bereitstellt – er lädt ein Wort in den Speicher. Die anderen beiden sind mstore8, der ein einzelnes Byte in den Speicher lädt, und mload, der ein Wort aus dem Speicher in den Stack verschiebt.

Die EVM verfügt außerdem über ein separates, nichtflüchtiges Storage-Modell (Speicher), das als Teil des Systemzustands verwaltet wird – dieser Speicher ist in Wort-Arrays organisiert (im Gegensatz zu wortadressierbaren Byte-Arrays im Stack). In diesem Storage bewahren Verträge persistente Daten auf – ein Vertrag kann nur mit seinem eigenen Storage interagieren. Der Storage ist in Schlüssel-Wert-Zuordnungen (Key-Value-Mappings) organisiert.

Obwohl es in diesem Abschnitt des Yellow Papers nicht erwähnt wird, ist es auch nützlich zu wissen, dass es eine vierte Art von Speicher gibt. Calldata (Aufrufdaten) ist ein byteadressierbarer Nur-Lese-Speicher, der verwendet wird, um den Wert zu speichern, der mit dem Parameter data einer Transaktion übergeben wird. Die EVM verfügt über spezifische Opcodes zur Verwaltung von calldata. calldatasize gibt die Größe der Daten zurück. calldataload lädt die Daten in den Stack. calldatacopy kopiert die Daten in den Memory-Speicher.

Die standardmäßige Von-Neumann-Architektur (öffnet in einem neuen Tab) speichert Code und Daten im selben Speicher. Die EVM folgt diesem Standard aus Sicherheitsgründen nicht – die gemeinsame Nutzung von flüchtigem Speicher macht es möglich, Programmcode zu ändern. Stattdessen wird Code im Storage gespeichert.

Es gibt nur zwei Fälle, in denen Code aus dem Memory-Speicher ausgeführt wird:

  • Wenn ein Vertrag einen anderen Vertrag erstellt (unter Verwendung von CREATE (öffnet in einem neuen Tab) oder CREATE2 (öffnet in einem neuen Tab)), stammt der Code für den Konstruktor des Vertrags aus dem Memory-Speicher.
  • Während der Erstellung jedes Vertrags wird der Konstruktor-Code ausgeführt und gibt dann den Code des eigentlichen Vertrags zurück, ebenfalls aus dem Memory-Speicher.

Der Begriff der außergewöhnlichen Ausführung (exceptional execution) bezeichnet eine Ausnahme, die dazu führt, dass die Ausführung des aktuellen Vertrags angehalten wird.

9.2 Gebührenübersicht

Dieser Abschnitt erklärt, wie die Gasgebühren berechnet werden. Es gibt drei Kostenarten:

Opcode-Kosten

Die inhärenten Kosten des spezifischen Opcodes. Um diesen Wert zu erhalten, suchen Sie die Kostengruppe des Opcodes in Anhang H (S. 29, unter Gleichung (329)) und finden Sie die Kostengruppe in Gleichung (326). Dies liefert Ihnen eine Kostenfunktion, die in den meisten Fällen Parameter aus Anhang G (S. 28) verwendet.

Zum Beispiel ist der Opcode CALLDATACOPY (öffnet in einem neuen Tab) ein Mitglied der Gruppe Wcopy. Die Opcode-Kosten für diese Gruppe betragen Gverylow+Gcopy×⌈μs[2]÷32⌉. Ein Blick in Anhang G zeigt, dass beide Konstanten 3 sind, was uns 3+3×⌈μs[2]÷32⌉ ergibt.

Wir müssen noch den Ausdruck ⌈μs[2]÷32⌉ entschlüsseln. Der äußerste Teil, ⌈ <value> ⌉, ist die Aufrundungsfunktion (Ceiling-Funktion), eine Funktion, die für einen gegebenen Wert die kleinste ganze Zahl zurückgibt, die nicht kleiner als der Wert ist. Zum Beispiel ⌈2.5⌉ = ⌈3⌉ = 3. Der innere Teil ist μs[2]÷32. Ein Blick auf Abschnitt 3 (Konventionen) auf S. 3 zeigt, dass μ der Maschinenzustand ist. Der Maschinenzustand ist in Abschnitt 9.4.1 auf S. 15 definiert. Laut diesem Abschnitt ist einer der Parameter des Maschinenzustands s für den Stack. Wenn man alles zusammennimmt, scheint μs[2] die Position #2 im Stack zu sein. Wenn man sich den Opcode (öffnet in einem neuen Tab) ansieht, ist die Position #2 im Stack die Größe der Daten in Bytes. Wenn man sich die anderen Opcodes in der Gruppe Wcopy ansieht, CODECOPY (öffnet in einem neuen Tab) und RETURNDATACOPY (öffnet in einem neuen Tab), haben diese ebenfalls eine Datengröße an derselben Position. Also ist ⌈μs[2]÷32⌉ die Anzahl der 32-Byte-Wörter, die erforderlich sind, um die zu kopierenden Daten zu speichern. Alles in allem betragen die inhärenten Kosten von CALLDATACOPY (öffnet in einem neuen Tab) 3 Gas plus 3 pro kopiertem Datenwort.

Ausführungskosten

Die Kosten für die Ausführung des Codes, den wir aufrufen.

Kosten für die Speichererweiterung

Die Kosten für die Erweiterung des Memory-Speichers (falls erforderlich).

In Gleichung 326 wird dieser Wert als Cmemi')-Cmemi) geschrieben. Wenn wir uns Abschnitt 9.4.1 noch einmal ansehen, sehen wir, dass μi die Anzahl der Wörter im Memory-Speicher ist. Also ist μi die Anzahl der Wörter im Memory-Speicher vor dem Opcode und μi' ist die Anzahl der Wörter im Memory-Speicher nach dem Opcode.

Die Funktion Cmem ist in Gleichung 328 definiert: Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋. ⌊x⌋ ist die Abrundungsfunktion (Floor-Funktion), eine Funktion, die für einen gegebenen Wert die größte ganze Zahl zurückgibt, die nicht größer als der Wert ist. Zum Beispiel ⌊2.5⌋ = ⌊2⌋ = 2. Wenn a < √512, ist a2 < 512, und das Ergebnis der Abrundungsfunktion ist null. Für die ersten 22 Wörter (704 Bytes) steigen die Kosten also linear mit der Anzahl der benötigten Speicherwörter. Jenseits dieses Punktes ist ⌊a2 ÷ 512⌋ positiv. Wenn der benötigte Speicher groß genug ist, sind die Gas-Kosten proportional zum Quadrat der Speichermenge.

Hinweis, dass diese Faktoren nur die inhärenten Gas-Kosten beeinflussen – sie berücksichtigen nicht den Gebührenmarkt oder Trinkgelder für Validatoren, die bestimmen, wie viel ein Endbenutzer zahlen muss – dies sind nur die reinen Kosten für die Ausführung einer bestimmten Operation auf der EVM.

Lesen Sie mehr über Gas.

9.3 Ausführungsumgebung

Die Ausführungsumgebung ist ein Tupel, I, das Informationen enthält, die nicht Teil des Blockchain-Zustands oder der EVM sind.

ParameterOpcode für den Zugriff auf die DatenSolidity-Code für den Zugriff auf die Daten
IaADDRESS (öffnet in einem neuen Tab)address(this)
IoORIGIN (öffnet in einem neuen Tab)tx.origin
IpGASPRICE (öffnet in einem neuen Tab)tx.gasprice
IdCALLDATALOAD (öffnet in einem neuen Tab) usw.msg.data
IsCALLER (öffnet in einem neuen Tab)msg.sender
IvCALLVALUE (öffnet in einem neuen Tab)msg.value
IbCODECOPY (öffnet in einem neuen Tab)address(this).code
IHBlock-Header-Felder, wie NUMBER (öffnet in einem neuen Tab) und DIFFICULTY (öffnet in einem neuen Tab)block.number, block.difficulty usw.
IeTiefe des Aufruf-Stacks für Aufrufe zwischen Verträgen (einschließlich Vertragserstellung)
IwDarf die EVM den Zustand ändern oder wird sie statisch ausgeführt?

Ein paar weitere Parameter sind notwendig, um den Rest von Abschnitt 9 zu verstehen:

ParameterDefiniert in AbschnittBedeutung
σ2 (S. 2, Gleichung 1)Der Zustand der Blockchain
g9.3 (S. 14)Verbleibendes Gas
A6.1 (S. 9)Aufgelaufener Teilzustand (Änderungen, die für das Ende der Transaktion geplant sind)
o9.3 (S. 14)Ausgabe – das zurückgegebene Ergebnis im Falle einer internen Transaktion (wenn ein Vertrag einen anderen aufruft) und bei Aufrufen von View-Funktionen (wenn Sie nur nach Informationen fragen, sodass Sie nicht auf eine Transaktion warten müssen)

9.4 Ausführungsübersicht

Nachdem wir nun alle Vorbereitungen getroffen haben, können wir uns endlich damit befassen, wie die EVM funktioniert.

Die Gleichungen 146-151 geben uns die Anfangsbedingungen für die Ausführung der EVM:

SymbolAnfangswertBedeutung
μggVerbleibendes Gas
μpc0Programmzähler (Program Counter), die Adresse der nächsten auszuführenden Anweisung
μm(0, 0, ...)Memory-Speicher, komplett mit Nullen initialisiert
μi0Höchste verwendete Speicheradresse
μs()Der Stack, anfangs leer
μoDie Ausgabe, eine leere Menge, bis und sofern wir nicht entweder mit Rückgabedaten (RETURN (öffnet in einem neuen Tab) oder REVERT (öffnet in einem neuen Tab)) oder ohne diese (STOP (öffnet in einem neuen Tab) oder SELFDESTRUCT (öffnet in einem neuen Tab)) anhalten.

Gleichung 152 besagt, dass es zu jedem Zeitpunkt während der Ausführung vier mögliche Bedingungen gibt und was mit ihnen zu tun ist:

  1. Z(σ,μ,A,I). Z stellt eine Funktion dar, die testet, ob eine Operation einen ungültigen Zustandsübergang erzeugt (siehe außergewöhnliches Anhalten). Wenn sie als True (Wahr) ausgewertet wird, ist der neue Zustand identisch mit dem alten (außer dass Gas verbrannt wird), da die Änderungen nicht implementiert wurden.
  2. Wenn der ausgeführte Opcode REVERT (öffnet in einem neuen Tab) ist, entspricht der neue Zustand dem alten Zustand, wobei etwas Gas verloren geht.
  3. Wenn die Abfolge von Operationen abgeschlossen ist, was durch ein RETURN (öffnet in einem neuen Tab) signalisiert wird, wird der Zustand auf den neuen Zustand aktualisiert.
  4. Wenn wir uns nicht in einer der Endbedingungen 1-3 befinden, wird die Ausführung fortgesetzt.

9.4.1 Maschinenzustand

Dieser Abschnitt erklärt den Maschinenzustand genauer. Er legt fest, dass w der aktuelle Opcode ist. Wenn μpc kleiner als ||Ib||, die Länge des Codes, ist, dann ist dieses Byte (Ibpc]) der Opcode. Andernfalls ist der Opcode als STOP (öffnet in einem neuen Tab) definiert.

Da es sich um eine Kellermaschine (öffnet in einem neuen Tab) handelt, müssen wir die Anzahl der Elemente im Auge behalten, die von jedem Opcode entnommen (δ) und hinzugefügt (α) werden.

9.4.2 Außergewöhnliches Anhalten (Exceptional Halting)

Dieser Abschnitt definiert die Funktion Z, die angibt, wann ein abnormaler Abbruch vorliegt. Dies ist eine Boolesche (öffnet in einem neuen Tab) Funktion, daher verwendet sie für ein logisches Oder (öffnet in einem neuen Tab) und für ein logisches Und (öffnet in einem neuen Tab).

Wir haben ein außergewöhnliches Anhalten, wenn eine dieser Bedingungen wahr ist:

  • μg < C(σ,μ,A,I) Wie wir in Abschnitt 9.2 gesehen haben, ist C die Funktion, die die Gas-Kosten angibt. Es ist nicht mehr genug Gas übrig, um den nächsten Opcode abzudecken.

  • δw=∅ Wenn die Anzahl der für einen Opcode entnommenen Elemente undefiniert ist, dann ist der Opcode selbst undefiniert.

  • || μs || < δw Stack-Unterlauf (Stack underflow), nicht genügend Elemente im Stack für den aktuellen Opcode.

  • w = JUMP ∧ μs[0]∉D(Ib) Der Opcode ist JUMP (öffnet in einem neuen Tab) und die Adresse ist kein JUMPDEST (öffnet in einem neuen Tab). Sprünge sind nur gültig, wenn das Ziel ein JUMPDEST (öffnet in einem neuen Tab) ist.

  • w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) Der Opcode ist JUMPI (öffnet in einem neuen Tab), die Bedingung ist wahr (ungleich null), sodass der Sprung stattfinden sollte, und die Adresse ist kein JUMPDEST (öffnet in einem neuen Tab). Sprünge sind nur gültig, wenn das Ziel ein JUMPDEST (öffnet in einem neuen Tab) ist.

  • w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || Der Opcode ist RETURNDATACOPY (öffnet in einem neuen Tab). Bei diesem Opcode ist das Stack-Element μs[1] der Offset, ab dem im Rückgabedatenpuffer gelesen werden soll, und das Stack-Element μs[2] ist die Datenlänge. Diese Bedingung tritt auf, wenn Sie versuchen, über das Ende des Rückgabedatenpuffers hinaus zu lesen. Beachten Sie, dass es für die Aufrufdaten (Calldata) oder für den Code selbst keine ähnliche Bedingung gibt. Wenn Sie versuchen, über das Ende dieser Puffer hinaus zu lesen, erhalten Sie einfach Nullen.

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

    Stack-Überlauf (Stack overflow). Wenn die Ausführung des Opcodes zu einem Stack von über 1024 Elementen führt, wird abgebrochen.

  • ¬Iw ∧ W(w,μ) Führen wir statisch aus (¬ ist die Negation (öffnet in einem neuen Tab) und Iw ist wahr, wenn wir den Zustand der Blockchain ändern dürfen)? Wenn ja, und wir versuchen eine zustandsändernde Operation, kann diese nicht stattfinden.

    Die Funktion W(w,μ) wird später in Gleichung 159 definiert. W(w,μ) ist wahr, wenn eine dieser Bedingungen wahr ist:

    • w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} Diese Opcodes ändern den Zustand, entweder durch Erstellen eines neuen Vertrags, Speichern eines Wertes oder Zerstören des aktuellen Vertrags.

    • LOG0≤w ∧ w≤LOG4 Wenn wir statisch aufgerufen werden, können wir keine Log-Einträge ausgeben. Die Log-Opcodes liegen alle im Bereich zwischen LOG0 (A0) (öffnet in einem neuen Tab) und LOG4 (A4) (öffnet in einem neuen Tab). Die Zahl nach dem Log-Opcode gibt an, wie viele Topics der Log-Eintrag enthält.

    • w=CALL ∧ μs[2]≠0 Sie können einen anderen Vertrag aufrufen, wenn Sie statisch sind, aber wenn Sie dies tun, können Sie keine ETH an ihn transferieren.

  • w = SSTORE ∧ μg ≤ Gcallstipend Sie können SSTORE (öffnet in einem neuen Tab) nicht ausführen, es sei denn, Sie haben mehr als Gcallstipend (in Anhang G als 2300 definiert) Gas.

9.4.3 Gültigkeit des Sprungziels (Jump Destination Validity)

Hier definieren wir formal, was die JUMPDEST (öffnet in einem neuen Tab)-Opcodes sind. Wir können nicht einfach nach dem Bytewert 0x5B suchen, da er sich innerhalb eines PUSH befinden könnte (und somit Daten und kein Opcode wäre).

In Gleichung (162) definieren wir eine Funktion, N(i,w). Der erste Parameter, i, ist die Position des Opcodes. Der zweite, w, ist der Opcode selbst. Wenn w∈[PUSH1, PUSH32], bedeutet das, dass der Opcode ein PUSH ist (eckige Klammern definieren einen Bereich, der die Endpunkte einschließt). In diesem Fall befindet sich der nächste Opcode bei i+2+(w−PUSH1). Für PUSH1 (öffnet in einem neuen Tab) müssen wir um zwei Bytes vorrücken (das PUSH selbst und der Ein-Byte-Wert), für PUSH2 (öffnet in einem neuen Tab) müssen wir um drei Bytes vorrücken, da es sich um einen Zwei-Byte-Wert handelt, usw. Alle anderen EVM-Opcodes sind nur ein Byte lang, also ist in allen anderen Fällen N(i,w)=i+1.

Diese Funktion wird in Gleichung (161) verwendet, um DJ(c,i) zu definieren, was die Menge (öffnet in einem neuen Tab) aller gültigen Sprungziele im Code c ist, beginnend mit der Opcode-Position i. Diese Funktion ist rekursiv definiert. Wenn i≥||c||, bedeutet das, dass wir uns am oder nach dem Ende des Codes befinden. Wir werden keine weiteren Sprungziele finden, also geben wir einfach die leere Menge zurück.

In allen anderen Fällen betrachten wir den Rest des Codes, indem wir zum nächsten Opcode gehen und die Menge ab diesem abrufen. c[i] ist der aktuelle Opcode, also ist N(i,c[i]) die Position des nächsten Opcodes. DJ(c,N(i,c[i])) ist daher die Menge der gültigen Sprungziele, die beim nächsten Opcode beginnt. Wenn der aktuelle Opcode kein JUMPDEST ist, geben Sie einfach diese Menge zurück. Wenn es ein JUMPDEST ist, fügen Sie es in die Ergebnismenge ein und geben Sie diese zurück.

9.4.4 Normales Anhalten

Die Haltefunktion H kann drei Arten von Werten zurückgeben.

  • Wenn wir uns nicht in einem Halte-Opcode befinden, wird , die leere Menge, zurückgegeben. Konventionsgemäß wird dieser Wert als Boolesches False (Falsch) interpretiert.
  • Wenn wir einen Halte-Opcode haben, der keine Ausgabe erzeugt (entweder STOP (öffnet in einem neuen Tab) oder SELFDESTRUCT (öffnet in einem neuen Tab)), wird eine Sequenz von null Bytes als Rückgabewert zurückgegeben. Beachten Sie, dass sich dies stark von der leeren Menge unterscheidet. Dieser Wert bedeutet, dass die EVM wirklich angehalten hat, es gibt nur keine Rückgabedaten zum Lesen.
  • Wenn wir einen Halte-Opcode haben, der eine Ausgabe erzeugt (entweder RETURN (öffnet in einem neuen Tab) oder REVERT (öffnet in einem neuen Tab)), wird die durch diesen Opcode angegebene Bytesequenz zurückgegeben. Diese Sequenz wird aus dem Memory-Speicher entnommen, der Wert oben auf dem Stack (μs[0]) ist das erste Byte, und der Wert danach (μs[1]) ist die Länge.

H.2 Befehlssatz (Instruction set)

Bevor wir zum letzten Unterabschnitt der EVM, 9.5, übergehen, schauen wir uns die Anweisungen selbst an. Sie sind in Anhang H.2 definiert, der auf S. 30 beginnt. Alles, was nicht als sich mit diesem spezifischen Opcode ändernd angegeben ist, bleibt voraussichtlich gleich. Variablen, die sich ändern, werden mit einem <etwas>′ angegeben.

Schauen wir uns zum Beispiel den Opcode ADD (öffnet in einem neuen Tab) an.

WertMnemonicδαBeschreibung
0x01ADD21Additionsoperation.
μ′s[0] ≡ μs[0] + μs[1]

δ ist die Anzahl der Werte, die wir vom Stack entnehmen (pop). In diesem Fall zwei, da wir die beiden obersten Werte addieren.

α ist die Anzahl der Werte, die wir zurücklegen (push). In diesem Fall einer, die Summe.

Die neue Stack-Spitze (μ′s[0]) ist also die Summe der alten Stack-Spitze (μs[0]) und des alten Wertes darunter (μs[1]).

Anstatt alle Opcodes mit einer ermüdenden Liste durchzugehen, erklärt dieser Artikel nur die Opcodes, die etwas Neues einführen.

WertMnemonicδαBeschreibung
0x20KECCAK25621Keccak-256-Hash berechnen.
μ′s[0] ≡ KEC(μms[0] . . . (μs[0] + μs[1] − 1)])
μ′i ≡ M(μis[0],μs[1])

Dies ist der erste Opcode, der auf den Memory-Speicher zugreift (in diesem Fall nur lesend). Er könnte jedoch über die aktuellen Grenzen des Speichers hinausgehen, daher müssen wir μi aktualisieren. Wir tun dies mit der Funktion M, die in Gleichung 330 auf S. 30 definiert ist.

WertMnemonicδαBeschreibung
0x31BALANCE11Kontostand des angegebenen Kontos abrufen.
...

Die Adresse, deren Kontostand wir ermitteln müssen, ist μs[0] mod 2160. Die Spitze des Stacks ist die Adresse, aber da Adressen nur 160 Bit lang sind, berechnen wir den Wert modulo (öffnet in einem neuen Tab) 2160.

Wenn σ[μs[0] mod 2160] ≠ ∅, bedeutet dies, dass Informationen zu dieser Adresse vorliegen. In diesem Fall ist σ[μs[0] mod 2160]b der Kontostand für diese Adresse. Wenn σ[μs[0] mod 2160] = ∅, bedeutet dies, dass diese Adresse nicht initialisiert ist und der Kontostand null ist. Sie können die Liste der Kontoinformationsfelder in Abschnitt 4.1 auf S. 4 einsehen.

Die zweite Gleichung, A'a ≡ Aa ∪ {μs[0] mod 2160}, bezieht sich auf den Kostenunterschied zwischen dem Zugriff auf Warm Storage (Speicher, auf den kürzlich zugegriffen wurde und der wahrscheinlich zwischengespeichert ist) und Cold Storage (Speicher, auf den nicht zugegriffen wurde und der sich wahrscheinlich in einem langsameren Speicher befindet, dessen Abruf teurer ist). Aa ist die Liste der Adressen, auf die die Transaktion zuvor zugegriffen hat und auf die daher kostengünstiger zugegriffen werden sollte, wie in Abschnitt 6.1 auf S. 9 definiert. Sie können mehr über dieses Thema in EIP-2929 (öffnet in einem neuen Tab) lesen.

WertMnemonicδαBeschreibung
0x8FDUP16161716. Stack-Element duplizieren.
μ′s[0] ≡ μs[15]

Beachten Sie, dass wir jedes Stack-Element, das wir verwenden möchten, entnehmen (pop) müssen, was bedeutet, dass wir auch alle darüber liegenden Stack-Elemente entnehmen müssen. Im Falle von DUP<n> (öffnet in einem neuen Tab) und SWAP<n> (öffnet in einem neuen Tab) bedeutet dies, dass bis zu sechzehn Werte entnommen und dann wieder zurückgelegt (push) werden müssen.

9.5 Der Ausführungszyklus

Nachdem wir nun alle Teile haben, können wir endlich verstehen, wie der Ausführungszyklus der EVM dokumentiert ist.

Gleichung (164) besagt, dass bei gegebenem Zustand:

  • σ (globaler Blockchain-Zustand)
  • μ (EVM-Zustand)
  • A (Teilzustand, Änderungen, die beim Ende der Transaktion eintreten sollen)
  • I (Ausführungsumgebung)

Der neue Zustand (σ', μ', A', I') ist.

Die Gleichungen (165)-(167) definieren den Stack und dessen Änderung aufgrund eines Opcodes (μs). Gleichung (168) ist die Änderung des Gases (μg). Gleichung (169) ist die Änderung des Programmzählers (μpc). Schließlich legen die Gleichungen (170)-(173) fest, dass die anderen Parameter gleich bleiben, sofern sie nicht explizit durch den Opcode geändert werden.

Damit ist die EVM vollständig definiert.

Fazit

Die mathematische Notation ist präzise und hat es dem Yellow Paper ermöglicht, jedes Detail von Ethereum zu spezifizieren. Sie hat jedoch einige Nachteile:

  • Sie kann nur von Menschen verstanden werden, was bedeutet, dass Konformitätstests (öffnet in einem neuen Tab) manuell geschrieben werden müssen.
  • Programmierer verstehen Computercode. Sie verstehen möglicherweise mathematische Notation, oder auch nicht.

Vielleicht aus diesen Gründen sind die neueren Spezifikationen der Konsensschicht (öffnet in einem neuen Tab) in Python geschrieben. Es gibt Spezifikationen der Ausführungsschicht in Python (öffnet in einem neuen Tab), aber sie sind nicht vollständig. Bis das gesamte Yellow Paper ebenfalls in Python oder eine ähnliche Sprache übersetzt wird, bleibt das Yellow Paper in Gebrauch, und es ist hilfreich, es lesen zu können.