Einstieg in EVM: So funktioniert Ethereum Backstage


Dieser Beitrag ist eine Fortsetzung von meinem Tief in die Serie eintauchen begann mit dem Bestreben, ein tieferes Verständnis der internen Abläufe und anderer cooler Dinge über Ethereum und Blockchain im Allgemeinen zu vermitteln, die Sie im Web nicht leicht finden. Hier sind die vorherigen Teile der Serie, falls Sie sie verpasst haben:

In diesem Teil werden wir das Kernverhalten des EVM näher erläutern und beschreiben. Wir werden sehen, wie Verträge erstellt werden, wie Nachrichtenanrufe funktionieren und wir werden uns alles ansehen, was mit Datenverwaltung zusammenhängt, wie z , , und der Stapel.

Um diesen Artikel besser zu verstehen, sollten Sie mit den Grundlagen des Ethereums vertraut sein. Wenn nicht, empfehle ich dringend, diese Beiträge zuerst zu lesen.

In diesem Beitrag werden wir einige Beispiele und Demonstrationen anhand von Beispielverträgen veranschaulichen, die Sie in diesem Repository finden. Bitte klone es, lauf , und probieren Sie es aus, bevor Sie beginnen.

Viel Spaß und bitte zögern Sie nicht, mit Fragen, Vorschlägen oder Rückmeldungen Kontakt aufzunehmen.

Bevor wir uns mit der Funktionsweise von EVM befassen und sie anhand von Codebeispielen betrachten, wollen wir uns ansehen, wo EVM in das Ethereum passt und welche Komponenten es enthält. Diese Diagramme machen Ihnen keine Angst, denn sobald Sie mit dem Lesen dieses Artikels fertig sind, werden Sie in der Lage sein, diese Diagramme sinnvoll zu gestalten.

Das folgende Diagramm zeigt, wo EVM in Ethereum passt.

Das folgende Diagramm zeigt die grundlegende Architektur von EVM.

Das folgende Diagramm zeigt, wie verschiedene Teile von EVM miteinander interagieren, damit Ethereum seine Magie entfaltet.

Wir haben gesehen, wie EVM aussieht. Jetzt ist es an der Zeit zu verstehen, wie diese Teile eine wichtige Rolle für die Funktionsweise von Ethereum spielen.

Grundlagen

Intelligente Verträge sind nur Computerprogramme, und wir können sagen, dass Ethereum-Verträge intelligente Verträge sind, die auf der virtuellen Ethereum-Maschine ausgeführt werden. Das EVM ist die Sandbox-Laufzeitumgebung und eine vollständig isolierte Umgebung für intelligente Verträge in Ethereum. Dies bedeutet, dass jeder Smart-Vertrag, der im EVM ausgeführt wird, keinen Zugriff auf das Netzwerk, das Dateisystem oder andere Prozesse hat, die auf dem Computer ausgeführt werden, auf dem sich die VM befindet.

Wie wir bereits wissen, gibt es zwei Arten von Konten: Verträge und externe Konten. Jedes Konto wird durch eine Adresse identifiziert und alle Konten teilen sich den gleichen Adressraum. Das EVM verarbeitet Adressen mit einer Länge von 160 Bit.

Jeder Account besteht aus einem Balance, ein nonce, Bytecode, und gespeicherte Daten (Lager). Es gibt jedoch einige Unterschiede zwischen diesen beiden Arten von Konten. Zum Beispiel die Code und Lager von Externe Accounts sind leerwährend Vertragskonten speichern ihren Bytecode und den Merkle-Root-Hash des gesamten Statusbaums. Darüber hinaus verfügen externe Adressen über einen entsprechenden privaten Schlüssel, Vertragskonten jedoch nicht. Die Aktionen von Vertragskonten werden zusätzlich zur regelmäßigen kryptografischen Signatur jeder Ethereum-Transaktion durch den von ihnen gehosteten Code gesteuert.

Schaffung

Das Anlegen eines Vertrages ist lediglich eine Transaktion, bei der die Empfängeradresse leer ist und deren Datenfeld den zusammengestellten Bytecode des anzulegenden Vertrages enthält (dies ist sinnvoll – Verträge können auch Verträge anlegen). Schauen wir uns ein kurzes Beispiel an. Bitte öffnen Sie das Verzeichnis der Übung 1; Darin finden Sie einen Vertrag namens mit folgendem Code:

Öffnen Sie eine Trüffelkonsole im Entwicklungsmodus, in der die Trüffelentwicklung ausgeführt wird. Befolgen Sie die folgenden Befehle, um eine Instanz von bereitzustellen :

trüffel (entwickeln)> kompilieren
trüffel (entwickeln)> sender = web3.eth.accounts[0]
Trüffel (entwickeln)> opts = {von: Absender, bis: Null, Daten: MyContract.bytecode, Gas: 4600000}
Trüffel (entwickeln)> txHash = web3.eth.sendTransaction (opts)

Wir können überprüfen, ob unser Vertrag erfolgreich bereitgestellt wurde, indem wir den folgenden Code ausführen:

trüffel (entwickeln)> quittung = web3.eth.getTransactionReceipt (txHash)
trüffel (entwickeln)> meinVertrag = neuer MeinVertrag (quittung.vertragAdresse)
trüffel (entwickeln)> myContract.add (10, 2)
{ [String: ‘12’] s: 1, e: 1, c: [ 12 ] }

Lassen Sie uns genauer analysieren, was wir gerade getan haben. Das erste, was passiert, wenn ein neuer Vertrag in der Ethereum-Blockchain bereitgestellt wird, ist, dass sein Konto erstellt wird. Wie Sie sehen, haben wir im obigen Beispiel die Adresse des Vertrags im Konstruktor protokolliert. Sie können dies bestätigen, indem Sie dies überprüfen ist die Adresse des vertraglich aufgefüllten und das ist der Hash der Zeichenfolge .

Im nächsten Schritt werden die mit der Transaktion gesendeten Daten als Bytecode ausgeführt. Dadurch werden die gespeicherten Statusvariablen initialisiert und der Hauptteil des zu erstellenden Vertrags festgelegt. Dieser Prozess wird nur einmal während des Lebenszyklus eines Vertrags ausgeführt. Der Initialisierungscode ist nicht das, was im Vertrag gespeichert ist. es erzeugt tatsächlich als Rückgabewert den zu speichernden Bytecode. Beachten Sie, dass nach dem Erstellen eines Vertragskontos der Code nicht mehr geändert werden kann.²

Angesichts der Tatsache, dass der Initialisierungsprozess den Code des zu speichernden Vertragsrumpfs zurückgibt, ist es sinnvoll, dass dieser Code nicht über die Konstruktorlogik erreichbar ist. Schauen wir uns zum Beispiel das an Übungsvertrag 1:

Wenn Sie versuchen, diesen Vertrag zu kompilieren, wird eine Warnung angezeigt, die besagt, dass Sie in der Konstruktorfunktion darauf verweisen. Diese wird jedoch kompiliert. Wenn Sie jedoch versuchen, eine neue Instanz bereitzustellen, wird diese zurückgesetzt. Dies liegt daran, dass es keinen Sinn macht, Code auszuführen, der noch nicht gespeichert ist.³ Auf der anderen Seite konnten wir auf die Adresse des Vertrags zugreifen: Das Konto ist vorhanden, es ist jedoch noch kein Code vorhanden.

Die Codeausführung kann jedoch andere Ereignisse hervorrufen, z. B. das Ändern des Speichers, das Erstellen weiterer Konten oder das Tätigen weiterer Nachrichtenaufrufe. Schauen wir uns zum Beispiel das an Code:

Sehen wir uns an, wie die folgenden Befehle in einer Trüffelkonsole ausgeführt werden:

trüffel (entwickeln)> kompilieren
trüffel (entwickeln)> sender = web3.eth.accounts[0]
Trüffel (entwickeln)> opts = {von: Absender, bis: Null, Daten: AnotherContract.bytecode, Gas: 4600000}
Trüffel (entwickeln)> txHash = web3.eth.sendTransaction (opts)
trüffel (entwickeln)> quittung = web3.eth.getTransactionReceipt (txHash)
Trüffel (entwickeln)> anotherContract = AnotherContract.at (receipt.contractAddress)
trüffel (entwickeln)> anotherContract.myContract (). then (a => myContractAddress = a)
trüffel (entwickeln)> myContract = MyContract.at (myContractAddress)
trüffel (entwickeln)> myContract.add (10, 2)
{ [String: ‘12’] s: 1, e: 1, c: [ 12 ] }

Darüber hinaus können Verträge mit dem erstellt werden Opcode, auf den sich das neue Solidity-Konstrukt kompiliert. Beide Alternativen funktionieren auf die gleiche Weise. Lassen Sie uns weiter untersuchen, wie Nachrichtenanrufe funktionieren.

Nachrichtenanrufe

Verträge können andere Verträge durch Nachrichtenaufrufe aufrufen. Jedes Mal, wenn ein Solidity-Vertrag eine Funktion eines anderen Vertrags aufruft, wird ein Nachrichtenaufruf erstellt. Jeder Anruf hat einen Absender, einen Empfänger, eine Nutzlast, einen Wert und eine Gasmenge. Die Tiefe des Nachrichtenanrufs ist auf weniger als 1024 Stufen begrenzt.

Solidity bietet eine native Aufrufmethode für den Adresstyp, die wie folgt funktioniert:

Adresse.call.gas (Gas).Wert(Wert) (Daten)

Gas ist die Menge des weiterzuleitenden Gases, die Adresse ist die anzurufende Adresse, ist die Menge an Äther, die in Wei übertragen werden soll, und ist die zu sendende Nutzlast. Bedenke, dass und sind hier optionale parameter, aber sei vorsichtig da fast alle übrig bleiben des Absenders wird standardmäßig in einem Low-Level-Anruf gesendet.

Wie Sie sehen, kann jeder Vertrag die Menge des in einem Anruf weiterzuleitenden Gases festlegen. Da jeder Anruf mit einer OOG-Ausnahme (Out-of-Gas) enden kann, wird zur Vermeidung von Sicherheitsrisiken mindestens 1/64 des verbleibenden Gases des Absenders gespart. Auf diese Weise können die Absender die Fehler der inneren Anrufe beim Auslassen des Gases behandeln, sodass sie die Ausführung beenden können, ohne dass ihnen das Gas ausgeht, und so die Ausnahme in die Luft sprudeln lassen.

Werfen wir einen Blick auf die Übungsvertrag 2:

Das contract verfügt nur über eine Fallback-Funktion, die jeden empfangenen Aufruf an eine Implementierungsinstanz umleitet. Diese Instanz wirft einfach durch ein Bei jedem eingehenden Anruf wird das gesamte gegebene Gas verbraucht. Dann ist die Idee hier, die Menge des Gases einzuloggen vor und unmittelbar nach der Weiterleitung eines Anrufs an . Lassen Sie uns eine Trüffelkonsole öffnen und sehen, was passiert:

trüffel (entwickeln)> kompilieren
trüffel (entwickeln)> caller.new (). dann (i => caller = i)
Trüffel (entwickeln)> opts = {gas: 4600000}
Trüffel (entwickeln)> caller.sendTransaction (opts) .then (r => result = r)
Trüffel (entwickeln)> logs = result.receipt.logs
Trüffel (entwickeln)> parseInt (Protokolle[0].data) // 4578955
Trüffel (entwickeln)> parseInt (Protokolle[1].data) // 71495

Wie du siehst, 71495 ist ungefähr der 64. Teil von 4578955. Dieses Beispiel zeigt deutlich, dass wir eine OOG-Ausnahme von einem inneren Aufruf behandeln können.

Solidity bietet außerdem den folgenden Opcode, mit dem wir Anrufe mit der Inline-Assembly verwalten können:

Woher ist die Menge des weiterzuleitenden Gases, ist die anzurufende Adresse, ist die Menge an Ether, in die übertragen werden soll , gibt die Speicherposition von an Bytes, in denen die Anrufdaten gespeichert sind, und und Zustand, in dem die Rückgabedaten gespeichert werden. Der einzige Unterschied besteht darin, dass wir mit einem Assembly-Aufruf Rückgabedaten verarbeiten können, während die Funktion nur 1 oder 0 zurückgibt, unabhängig davon, ob dies fehlgeschlagen ist oder nicht.

Das EVM unterstützt eine spezielle Variante eines angerufenen Nachrichtenaufrufs . Solidity bietet zusätzlich zu einer Inline-Assembly-Version eine integrierte Adressmethode. Der Unterschied zu einem Low-Level-Aufruf besteht darin, dass der Zielcode im Kontext des aufrufenden Vertrags ausgeführt wird und Ändere dich nicht.

Analysieren wir das folgende Beispiel, um besser zu verstehen, wie a funktioniert. Beginnen wir mit dem Vertrag:

Wie Sie sehen können, die Vertrag erklärt einfach a Funktion, die eine tragen die und Daten. Wir können diese Methode ausprobieren, indem wir die folgenden Zeilen in einer Trüffelkonsole ausführen:

trüffel (entwickeln)> kompilieren
trüffel (entwickeln)> someone = web3.eth.accounts[0]
trüffel (entwickeln)> ETH_2 = neue web3.BigNumber (‘2e18’)
Trüffel (entwickeln)> Begrüßer.neu (). dann (i => Begrüßer = i)
Trüffel (entwickeln)> opts = {von: jemandem, Wert: ETH_2}
trüffel (entwickeln)> grüße. danke (opts) .dann (tx => log = tx.logs[0])
Trüffel (entwickeln)> log.event // Danke
Trüffel (entwickeln)> log.args.sender === Jemand // wahr
Trüffel (entwickeln)> log.args.value.eq (ETH_2) // true

Nachdem wir die Funktionalität bestätigt haben, konzentrieren wir uns auf die Vertrag:

Dieser Vertrag definiert nur a Funktion, die die ausführt Vertrag Methode durch a . Mal sehen, was passiert, wenn wir anrufen Vertrag durch die Vertrag:

trüffel (entwickeln)> Wallet.new (). then (i => wallet = i)
trüffel (entwickeln)> wallet.sendTransaction (opts) .then (r => tx = r)
trüffel (entwickeln)> logs = tx.receipt.logs
trüffel (entwickeln)> SolidityEvent = erfordern (‘web3 / lib / web3 / event.js’)
Trüffel (entwickeln)> Danke = Objekt.Werte (Greeter.Veranstaltungen)[0]
trüffel (entwickeln)> event = new SolidityEvent (null, danke, 0)
Trüffel (entwickeln)> log = event.decode (logs[0])
Trüffel (entwickeln)> log.event // Danke
Trüffel (entwickeln)> log.args.sender === Jemand // wahr
Trüffel (entwickeln)> log.args.value.eq (ETH_2) // true

Wie Sie vielleicht bemerkt haben, haben wir gerade bestätigt, dass die Funktion bewahrt die und .

Dies bedeutet, dass ein Vertrag zur Laufzeit dynamisch Code von einer anderen Adresse laden kann. Speicherung, aktuelle Adresse und Guthaben beziehen sich weiterhin auf den anrufenden Vertrag, lediglich der Code wird von der angerufenen Adresse übernommen. Dadurch ist es möglich, die Funktion "Bibliothek" in Solidity zu implementieren. "

Es gibt noch eine Sache, die wir untersuchen sollten Anrufe. Wie oben erwähnt, ist der Speicher des aufrufenden Vertrags derjenige, auf den der ausgeführte Code zugreift. Sehen wir uns das an Vertrag:

Das Vertrag hat nur zwei Funktionen: und . Der Calculator-Vertrag kann nicht addieren oder multiplizieren. es delegiert diese Anrufe an die und Verträge jeweils statt. Alle diese Verträge haben jedoch dasselbe Statusvariablenergebnis, um das Ergebnis jeder Berechnung zu speichern. Mal sehen, wie das funktioniert:

trüffel (entwickeln)> Rechner.neu (). dann (i => rechner = i)
truffle (entwickeln)> calculator.addition (). then (a => additionAddress = a)
Trüffel (entwickeln)> addition = Addition.at (additionAddress)
truffle (entwickeln)> calculator.product (). then (a => productAddress = a)
Trüffel (entwickeln)> product = Product.at (productAddress)
trüffel (entwickeln)> calculator.add (5)
trüffel (entwickeln)> rechner.result (). dann (r => r.toString ()) // 5
trüffeln (entwickeln)> addieren.resultieren (). dann (r => r.toString ()) // 0
Trüffel (entwickeln)> Produkt.Ergebnis (). Dann (r => r.toString ()) // 0
trüffel (entwickeln)> calculator.mul (2)
trüffel (entwickeln)> rechner.result (). dann (r => r.toString ()) // 10
trüffeln (entwickeln)> addieren.resultieren (). dann (r => r.toString ()) // 0
Trüffel (entwickeln)> Produkt.Ergebnis (). Dann (r => r.toString ()) // 0

Wir haben soeben bestätigt, dass wir die Speicherung des Vertrag. Außerdem wird der ausgeführte Code in der Datei gespeichert und in der Verträge.

Darüber hinaus gibt es für die Anruffunktion eine Solidity Assembly-Opcode-Version . Werfen wir einen Blick auf die Vertrag, um zu sehen, wie wir es nutzen können:

Dieses Mal verwenden wir die Inline-Assemblierung, um das auszuführen . Wie Sie vielleicht bemerkt haben, gibt es hier kein Wertargument, da wird sich nicht ändern. Sie fragen sich vielleicht, warum wir das laden Adresse, oder was und sind. Keine Panik – wir werden sie im nächsten Beitrag der Serie beschreiben. In der Zwischenzeit können Sie dieselben Befehle über eine Trüffelkonsole ausführen, um deren Verhalten zu überprüfen.

Es ist erneut wichtig zu verstehen, wie ein funktioniert. Jeder ausgelöste Anruf wird vom aktuellen Vertrag und nicht vom vom Stellvertreter angerufenen Vertrag gesendet. Zusätzlich kann der ausgeführte Code in den Speicher des Anrufervertrages lesen und schreiben. Bei unsachgemäßer Umsetzung können selbst kleinste Fehler zu Verlusten in Millionenhöhe führen. Hier ist eine Liste der teuersten Fehler in der Geschichte von Ethereum.

HackPedia: 16 Solidity Hacks / Vulnerabilities, ihre Fixes und Beispiele aus der Praxis

Das EVM verwaltet je nach Kontext unterschiedliche Arten von Daten, und das auf unterschiedliche Weise. Wir können mindestens vier Haupttypen von Daten unterscheiden: , , , und , neben dem Vertragscode. Analysieren wir jeden dieser Punkte:

Stapel

Das EVM ist eine Stapelmaschine, dh es wird nicht mit Registern, sondern mit einem virtuellen Stapel gearbeitet. Der Stapel hat eine maximale Größe von 1024. Stapelelemente haben eine Größe von 256 Bit. in der Tat ist das EVM eine 256-Bit-Wortmaschine (dies erleichtert Hash-Schema und elliptische Kurvenberechnungen). Hier verbrauchen die meisten Opcodes ihre Parameter.

Das EVM bietet viele Opcodes, um den Stack direkt zu ändern. Einige davon sind:

  • Entfernt den Gegenstand vom Stapel.
  • setzt die folgenden n Bytes Element im Stapel, mit n von 1 bis 32.
  • dupliziert die nth stack item, with n von 1 bis 32.
  • tauscht die 1. und nth stack item, with n von 1 bis 32.

Daten anrufen

Das ist ein schreibgeschützter byteadressierbarer Bereich, in dem der Datenparameter einer Transaktion oder eines Aufrufs gespeichert ist. Im Gegensatz zum Stack müssen Sie zur Verwendung dieser Daten einen genauen Byte-Versatz und die Anzahl der zu lesenden Bytes angeben.

Die vom EVM zur Verfügung gestellten Opcodes für den Betrieb mit dem umfassen:

  • gibt die Größe der Transaktionsdaten an.
  • Lädt 32 Bytes der Transaktionsdaten in den Stapel.
  • kopiert eine Anzahl von Bytes der Transaktionsdaten in den Speicher.

Solidity bietet auch eine Inline-Assembly-Version dieser Opcodes. Diese sind , und beziehungsweise. Der letzte erwartet drei Argumente (, , ): es wird kopiert Bytes von an der Position in den Speicher an der Position . Außerdem können Sie mit Solidity auf die zugreifen durch .

Wie Sie vielleicht bemerkt haben, haben wir einige dieser Opcodes in einigen Beispielen des vorherigen Beitrags verwendet. Werfen wir einen Blick auf den Inline-Assembler-Codeblock von a nochmal:

Montage {
let ptr: = mload (0x40)
calldatacopy (ptr, 0, calldatasize)
let result: = delegatecall (gas, _impl, ptr, calldatasize, 0, 0)
}

So delegieren Sie den Anruf an die adresse müssen wir weiterleiten . Vorausgesetzt, dass die opcode arbeitet mit Daten im Speicher, die wir kopieren müssen zuerst in den Speicher. Deshalb verwenden wir um alle zu kopieren auf einen Speicherzeiger (beachten Sie, dass wir verwenden ).

Analysieren wir ein anderes Beispiel mit . Sie finden a Vertrag im Ordner Übung 3 mit folgendem Code:

Die Idee hier ist, die Addition von zwei Zahlen zurückzugeben, die von Argumenten übergeben werden. Wie Sie sehen, laden wir erneut einen Speicherzeiger, aus dem gelesen wird , aber bitte ignorieren Sie das jetzt; wir werden es gleich nach diesem Beispiel erklären. Wir speichern diesen Speicherzeiger in der Variablen und speichern in die folgende Position, die 32-Byte direkt danach ist . Dann benutzen wir um den ersten Parameter in zu speichern . Möglicherweise haben Sie bemerkt, dass wir es von der 4. Position der kopieren statt seines Anfangs. Dies liegt daran, dass die ersten 4 Bytes der Halten Sie die Signatur der aufgerufenen Funktion, in diesem Fall Mit dieser Funktion identifiziert das EVM, welche Funktion bei einem Aufruf ausgeführt werden muss. Dann speichern wir den zweiten Parameter in b und kopieren die folgenden 32 Bytes der . Schließlich müssen wir nur die Addition beider Werte berechnen, die sie aus dem Speicher laden.

Sie können dies selbst mit einer Trüffelkonsole testen, auf der die folgenden Befehle ausgeführt werden:

trüffel (entwickeln)> kompilieren
Trüffel (entwickeln)> Calldata.new (). dann (i => calldata = i)
Trüffel (entwickeln)> calldata.add (1, 6) .then (r => r.toString ()) // 7

Erinnerung

Der Speicher ist ein flüchtiger byteadressierbarer Lese- / Schreibbereich. Es wird hauptsächlich zum Speichern von Daten während der Ausführung verwendet, hauptsächlich zum Übergeben von Argumenten an interne Funktionen. Da dies ein flüchtiger Bereich ist, beginnt jeder Nachrichtenaufruf mit einem gelöschten Speicher. Alle Standorte sind anfänglich als Null definiert. Als Aufrufdaten kann der Speicher auf Byte-Ebene adressiert werden, es können jedoch nur 32-Byte-Wörter gleichzeitig gelesen werden.

Das Gedächtnis wird "erweitert", wenn wir ein Wort darin schreiben, das zuvor nicht verwendet wurde. Zusätzlich zu den Kosten für das Schreiben selbst entstehen Kosten für diese Erweiterung, die für die ersten 724 Bytes linear und danach quadratisch ansteigen.

Das EVM bietet drei Opcodes zur Interaktion mit dem Speicherbereich:

  • Lädt ein Wort aus dem Speicher in den Stapel.
  • speichert ein Wort im Speicher.
  • Speichert ein Byte im Speicher.

Solidity bietet auch eine Inline-Assembly-Version dieser Opcodes.

Es gibt noch eine andere wichtige Sache, die wir über das Gedächtnis wissen müssen. Solidity speichert immer einen freien Speicherzeiger an der Position eine Referenz auf das erste unbenutzte Wort im Speicher. Aus diesem Grund laden wir dieses Wort, um mit Inline-Assemblierung zu arbeiten. Da die ersten 64 Byte des Speichers für das EVM reserviert sind, können wir auf diese Weise sicherstellen, dass der von Solidity intern verwendete Speicher nicht überschrieben wird. Zum Beispiel in der In dem oben gezeigten Beispiel haben wir diesen Zeiger geladen, um den angegebenen zu speichern um es weiterzuleiten. Dies liegt am Inline-Assembly-Opcode muss seine Nutzlast aus dem Speicher holen.

Wenn Sie außerdem auf den vom Solidity-Compiler ausgegebenen Bytecode achten, werden Sie feststellen, dass alle mit 0x6060604052 … beginnen. Dies bedeutet:

PUSH1: EVM-Opcode ist 0x60
0x60: Der Zeiger für den freien Speicher
PUSH1: EVM-Opcode ist 0x60
0x40: Speicherposition für den freien Speicherzeiger
MSTORE: EVM-Opcode ist 0x52

Sie müssen sehr vorsichtig sein, wenn Sie mit Speicher auf Assembly-Ebene arbeiten. Andernfalls könnten Sie einen reservierten Platz überschreiben.

Lager

Der Speicher ist ein persistenter, wortadressierbarer Lese- / Schreibbereich. Hier speichert jeder Vertrag seine persistenten Informationen. Im Gegensatz zum Speicher ist der Speicher ein beständiger Bereich und kann nur mit Worten angesprochen werden. Es handelt sich um eine Schlüsselwertzuordnung von 2²⁵⁶ Slots mit jeweils 32 Bytes. Ein Vertrag kann keinen anderen Speicher als den eigenen lesen oder beschreiben. Alle Standorte sind anfänglich als Null definiert.

Die zum Speichern von Daten erforderliche Gasmenge ist eine der höchsten im EVM. Diese Kosten sind nicht immer gleich. Das Ändern eines Speichersteckplatzes von einem Wert von Null auf einen Wert ungleich Null kostet 20.000. Das Speichern desselben Werts ungleich Null oder das Einstellen eines Werts ungleich Null auf Null kostet 5.000. Im letzten Szenario wird jedoch eine Rückerstattung von 15.000 gewährt, wenn ein Wert ungleich Null auf Null gesetzt wird.

Das EVM bietet zwei Operationscodes zum Betreiben des Speichers:

  • Lädt ein Wort aus dem Speicher in den Stapel.
  • Speichert ein Wort im Speicher.

Diese Opcodes werden auch von der Inline-Assembly von Solidity unterstützt.

Solidity ordnet automatisch jede definierte Statusvariable Ihres Vertrags einem Speicherplatz zu. Die Strategie ist recht einfach – statisch große Variablen (alles außer Mappings und dynamischen Arrays) werden ab Position 0 zusammenhängend im Speicher abgelegt.

Bei dynamischen Arrays kann dieser Steckplatz () speichert die Länge des Arrays und seine Daten werden an der Slot-Nummer abgelegt, die sich aus dem Hashing von p ergibt (). Bei Zuordnungen wird dieser Steckplatz nicht verwendet und der Wert entspricht einem Schlüssel wird sich in befinden . Beachten Sie, dass die Parameter von keccak256 ( und ) werden immer auf 32 Bytes aufgefüllt.

Schauen wir uns ein Codebeispiel an, um zu verstehen, wie dies funktioniert. Im Vertragsordner von Übung 3 finden Sie einen Speichervertrag mit folgendem Code:

Öffnen wir nun eine Trüffelkonsole, um die Speicherstruktur zu testen. Zunächst kompilieren und erstellen wir eine neue Vertragsinstanz:

trüffel (entwickeln)> kompilieren
trüffel (entwickeln)> lagerung.neu (). dann (i => lagerung = i)

Dann können wir sicherstellen, dass die Adresse 0 eine Nummer 2 und die Adresse 1 die Adresse des Vertrags enthält:

Trüffel (entwickeln)> web3.eth.getStorageAt (Speicheradresse, 0) // 0x02
Trüffel (entwickeln)> web3.eth.getStorageAt (Speicheradresse, 1) // 0x ..

Wir können überprüfen, ob die Speicherposition 2 die Länge des Arrays wie folgt hält:

Trüffel (entwickeln)> web3.eth.getStorageAt (Speicheradresse, 2) // 0x02

Schließlich können wir überprüfen, ob die Speicherposition 3 nicht verwendet wird, und die Zuordnungswerte werden wie oben beschrieben gespeichert:

Trüffel (entwickeln)> web3.eth.getStorageAt (Speicheradresse, 3) 
// 0x00
Trüffel (entwickeln)> mapIndex = "0000000000000000000000000000000000000000000000000000000000000003"Trüffel (entwickeln)> firstKey = "0000000000000000000000000000000000000000000000000000000000000001"Trüffel (entwickeln)> firstPosition = web3.sha3 (firstKey + mapIndex, {Kodierung: ‘hex’})Trüffel (entwickeln)> web3.eth.getStorageAt (Speicheradresse, firstPosition)
// 0x09
Trüffel (entwickeln)> secondKey = "0000000000000000000000000000000000000000000000000000000000000002"Trüffel (entwickeln)> secondPosition = web3.sha3 (secondKey + mapIndex, {Kodierung: 'hex'})Trüffel (entwickeln)> web3.eth.getStorageAt (Speicheradresse, zweite Position)
// 0x0A

Groß! Wir haben gezeigt, dass die Solidity-Speicherstrategie so funktioniert, wie wir sie verstehen! In der offiziellen Dokumentation erfahren Sie, wie Solidity Statusvariablen in den Speicher abbildet.

Das ist es. Ich hoffe, dass Sie jetzt besser verstehen, welche Rolle EVM in der Infrastruktur von Ethereum spielt.

"Die Adresse des neuen Kontos wird als die am weitesten rechts stehenden 160 Bits des Keccak-Hash der RLP-Codierung der Struktur definiert, die nur den Absender und das Konto nonce enthält." (Ethereum Yellow Paper)

² Eine der Säulen von zeppelin_os ist die Aufrüstbarkeit von Verträgen. Bei Zeppelin haben wir verschiedene Strategien untersucht, um dies umzusetzen. Mehr dazu können Sie hier lesen.

³ Solidity prüft vor dem Aufrufen einer externen Funktion, ob die Adresse einen Bytecode enthält, und stellt sie ansonsten wieder her.

Coins Kaufen: Bitcoin.deAnycoinDirektCoinbaseCoinMama (mit Kreditkarte)Paxfull

Handelsplätze / Börsen: Bitcoin.de | KuCoinBinanceBitMexBitpandaeToro

Lending / Zinsen erhalten: Celsius NetworkCoinlend (Bot)

Cloud Mining: HashflareGenesis MiningIQ Mining

Werbung: Immobilienmakler HeidelbergMakler Heidelberg

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close