So entschlüsseln Sie einen Smart Contract Method Call

Eintauchen in die Ethereum VM Teil 4

In früheren Artikeln dieser Reihe haben wir gesehen, wie Solidity komplexe Datenstrukturen im EVM-Speicher darstellt. Daten sind jedoch nutzlos, wenn keine Möglichkeit besteht, mit ihnen zu interagieren. Der Smart Contract ist der Vermittler zwischen Daten und der Außenwelt.

In diesem Artikel erfahren Sie, wie Solidity und EVM es externen Programmen ermöglichen, die Methoden eines Vertrags aufzurufen und dessen Status zu ändern.

Das „externe Programm“ ist nicht an DApp / JavaScript gebunden. Jedes Programm, das über HTTP RPC mit einem Ethereum-Knoten kommunizieren kann, kann mit jedem in der Blockchain bereitgestellten Vertrag interagieren, indem Transaktionen erstellt werden.

Das Erstellen einer Transaktion entspricht dem Erstellen einer HTTP-Anforderung. Ein Webserver würde Ihre HTTP-Anfrage akzeptieren und Änderungen an der Datenbank vornehmen. Eine Transaktion würde vom Netzwerk akzeptiert und die zugrunde liegende Blockchain um die Statusänderungen erweitert.

Transaktionen beziehen sich auf Smart Contracts wie HTTP-Anforderungen auf Webdienste.

Wenn die EVM-Baugruppe und die Darstellung der Solidity-Daten nicht bekannt sind, lesen Sie die vorherigen Artikel dieser Serie, um weitere Informationen zu erhalten:

Vertragstransaktion

Sehen wir uns eine Transaktion an, bei der eine Statusvariable auf 0x1 gesetzt wird. Der Vertrag, mit dem wir interagieren möchten, enthält einen Setter und einen Getter für die Variable a :

Dieser Vertrag wird im Testnetzwerk Rinkeby bereitgestellt. Sie können es gerne mit Etherscan unter der Adresse 0x62650ae5….

überprüfen

Ich habe eine Transaktion erstellt, mit der setA (1) aufgerufen wird. Überprüfen Sie diese Transaktion unter der Adresse 0x7db471e5 ….

Die Eingabedaten der Transaktion lauten:

Für das EVM sind dies nur 36 Byte Rohdaten. Es wird unverarbeitet als calldata an den Smart Contract übergeben. Wenn der Smart Contact ein Solidity-Programm ist, interpretiert er diese Eingabebytes als Methodenaufruf und führt den entsprechenden Assemblycode für setA (1) aus.

Die Eingabedaten können in zwei Unterteile unterteilt werden:

Die ersten vier Bytes sind die Methodenauswahl. Der Rest der Eingabedaten sind Methodenargumente in Blöcken von 32 Bytes. In diesem Fall gibt es nur 1 Argument, den Wert 0x1 .

Der Methodenselektor ist der kecccak256-Hash der Methodensignatur. In diesem Fall lautet die Methodensignatur setA (uint256) . Dies ist der Name der Methode und der Typ ihrer Argumente.

Berechnen wir den Methodenselektor in Python. Hash die Methodensignatur:

Nehmen Sie dann die ersten 4 Bytes des Hash:

Hinweis: Jedes Byte wird durch 2 Zeichen in einer Python-Hex-Zeichenfolge

dargestellt

Die Anwendungsbinärschnittstelle (ABI)

Für das EVM sind die Eingabedaten der Transaktion ( Anrufdaten ) nur eine Folge von Bytes. Das EVM bietet keine integrierte Unterstützung für das Aufrufen von Methoden.

Ein intelligenter Vertrag kann einen Methodenaufruf simulieren, indem er die Eingabedaten strukturiert verarbeitet, wie im vorherigen Abschnitt gezeigt.

Wenn sich alle Sprachen im EVM darüber einig sind, wie Eingabedaten interpretiert werden sollen, können sie problemlos miteinander interagieren. Die binäre Schnittstelle für Vertragsanwendungen (ABI) gibt ein gemeinsames Codierungsschema an.

Wir haben gesehen, wie der ABI einen einfachen Methodenaufruf wie setA (1) codiert. In späteren Abschnitten werden wir sehen, wie Methodenaufrufe mit komplexeren Argumenten codiert werden.

Einen Getter anrufen

Wenn die von Ihnen aufgerufene Methode den Status ändert, muss das gesamte Netzwerk zustimmen. Dies würde eine Transaktion erfordern und kostet Sie Benzin.

Eine Getter-Methode wie getA () ändert nichts. Anstatt das gesamte Netzwerk zu bitten, die Berechnung durchzuführen, können wir den Methodenaufruf an einen lokalen Ethereum-Knoten senden. Mit einer RPC-Anforderung eth_call können Sie eine Transaktion lokal simulieren. Dies ist nützlich für die Nur-Lese-Methode oder die Schätzung des Gasverbrauchs.

Ein eth_call ähnelt einer zwischengespeicherten HTTP-GET-Anforderung.

Lassen Sie uns einen eth_call ausführen, um die getA -Methode aufzurufen und den Status a zurückzugeben. Berechnen Sie zunächst den Methodenselektor:

Da es kein Argument gibt, sind die Eingabedaten nur der Methodenselektor für sich. Wir können eine eth_call -Anforderung an jeden Ethereum-Knoten senden. In diesem Beispiel senden wir die Anforderung an einen öffentlichen Ethereum-Knoten, der von infura.io gehostet wird:

Das EVM führt die Berechnung durch und gibt als Ergebnis Rohbytes zurück:

Laut ABI sollten die Bytes als Wert 0x1 interpretiert werden.

Assembly für externen Methodenaufruf

Nun wollen wir sehen, wie der kompilierte Vertrag die rohen Eingabedaten verarbeitet, um einen Methodenaufruf durchzuführen. Stellen Sie sich einen Vertrag vor, der setA (uint256) definiert:

Kompilieren:

Der Assemblycode für die aufgerufenen Methoden befindet sich im Hauptteil des Vertrags und ist unter sub_0 :

organisiert

Es gibt zwei Teile des Boilerplate-Codes, die für diese Diskussion nicht relevant sind, aber zu Ihrer Information:

Teilen wir den verbleibenden Baugruppencode zur einfacheren Analyse in zwei Teile auf:

Zuerst die mit Anmerkungen versehene Baugruppe zum Abgleichen des Selektors:

Es ist unkompliziert, bis auf das Bit-Shuffling zu Beginn 4 Bytes aus den Anrufdaten zu laden. Aus Gründen der Klarheit sieht die Assemblierungslogik im Pseudocode auf niedriger Ebene wie folgt aus:

Die mit Anmerkungen versehene Assembly für den eigentlichen Methodenaufruf:

Vor dem Aufrufen des Methodenkörpers führt die Assembly zwei Aktionen aus:

Im Low-Level-Pseudocode:

Kombinieren Sie die beiden Teile miteinander:

Wissenswertes: Der Opcode für das Zurücksetzen lautet . Sie finden jedoch keine Spezifikation dafür im Gelben Papier oder keine Implementierung im Code. Tatsächlich existiert code> nicht wirklich! Es ist eine ungültige Operation. Wenn das EVM auf eine ungültige Operation stößt, gibt es den Status auf und stellt ihn als Nebeneffekt wieder her.

Umgang mit mehreren Methoden

Wie generiert der Solidity-Compiler eine Assembly für einen Vertrag mit mehreren Methoden?

Einfach. Nur mehr if-else verzweigt nacheinander:

Im Pseudocode:

ABI-Codierung für komplexe Methodenaufrufe

Bei einem Methodenaufruf sind die ersten vier Bytes der Transaktionseingabedaten immer der Methodenselektor. Dann folgen die Methodenargumente in Blöcken von 32 Bytes. In der ABI-Codierungsspezifikation wird detailliert beschrieben, wie die komplexeren Arten von Argumenten codiert werden. Das Lesen kann jedoch äußerst schmerzhaft sein.

Eine weitere Strategie zum Erlernen der ABI-Codierung besteht darin, mithilfe der ABI-Codierungsfunktion des Pyethereums zu untersuchen, wie verschiedene Datentypen codiert werden. Wir gehen von einfachen Fällen aus und bauen auf komplexere Typen auf.

Importieren Sie zunächst die Funktion encode_abi :

Bei einer Methode mit drei uint256-Argumenten (z. B. foo (uint256a, uint256b, uint256c) ) sind die codierten Argumente einfach nacheinander uint256-Nummern:

Typen, die kleiner als 32 Byte sind, werden auf 32 Byte aufgefüllt:

Bei Arrays mit fester Größe sind die Elemente wiederum 32-Byte-Blöcke (bei Bedarf mit Nullen aufgefüllt), die nacheinander angeordnet sind:

ABI-Codierung für dynamische Arrays

Der ABI führt eine Indirektionsebene ein, um dynamische Arrays nach einem Schema zu codieren, das als Head-Tail-Codierung bezeichnet wird.

Die Idee ist, dass die Elemente der dynamischen Arrays am Ende der Anrufdaten der Transaktion gepackt werden. Die Argumente (der „Kopf“) sind Verweise auf die Aufrufdaten, in denen sich die Array-Elemente befinden.

Wenn wir eine Methode mit 3 dynamischen Arrays aufrufen, werden die Argumente folgendermaßen codiert (Kommentare und Zeilenumbrüche wurden aus Gründen der Übersichtlichkeit hinzugefügt):

Der Abschnitt head enthält also drei 32-Byte-Argumente, die auf Positionen im Abschnitt tail zeigen, der die tatsächlichen Daten für die drei dynamischen Arrays enthält.

Das erste Argument ist beispielsweise 0x60 und zeigt auf das 96. Byte ( 0x60 ) der Aufrufdaten. Wenn Sie sich das 96. Byte ansehen, ist dies der Anfang eines Arrays. Die ersten 32 Bytes sind die Länge, gefolgt von drei Elementen.

Es ist möglich, dynamische und statische Argumente zu mischen. Hier ist ein Beispiel mit (statisch, dynamisch, statisch) Argumenten. Die statischen Argumente werden unverändert codiert, während die Daten für das zweite dynamische Array im Endabschnitt platziert werden:

Viele Nullen, aber es ist in Ordnung.

Codieren von Bytes

Strings und Byte Arrays sind ebenfalls Head-Tail-codiert. Der einzige Unterschied besteht darin, dass die Bytes dicht in Blöcken von 32 Bytes gepackt sind, wie folgt:

Für jeden String / Bytearray codieren die ersten 32 Bytes die Länge, gefolgt von den Bytes.

Wenn die Zeichenfolge größer als 32 Byte ist, werden mehrere 32-Byte-Blöcke verwendet:

Verschachtelte Arrays

Verschachtelte Arrays haben eine Indirektion pro Verschachtelung.

Ja, viele Nullen.

Gaskosten & amp; ABI-Codierungsdesign

Warum schneidet der ABI den Methodenselektor auf nur 4 Byte ab? Könnte es zu unglücklichen Kollisionen für verschiedene Methoden kommen, wenn wir nicht die vollen 32 Bytes von sha256 verwenden? Wenn durch das Abschneiden Kosten gespart werden sollen, warum sollten Sie sich dann die Mühe machen, nur 28 Bytes in der Methodenauswahl zu speichern, wenn viel mehr Bytes mit Null-Padding verschwendet werden?

Diese beiden Designentscheidungen scheinen widersprüchlich zu sein ... bis wir die Gaskosten für eine Transaktion berücksichtigen.

Ah ha! Nullen sind 17-mal billiger, daher ist die Null-Polsterung nicht so schlecht, wie es scheint.

Der Methodenselektor ist ein kryptografischer Hash, der pseudozufällig ist. Eine zufällige Zeichenfolge hat meistens Bytes ungleich Null, da jedes Byte nur eine Wahrscheinlichkeit von 0,3% (1/255) hat, 0 zu sein.

4 * 31 (Null-Bytes) + 68 (1 Nicht-Null-Byte)

32 * 68

32 * 4

Der ABI zeigt ein weiteres Beispiel für ein eigenartiges Low-Level-Design, das durch die Gaskostenstruktur angeregt wird.

Negative Ganzzahlen…

Negative Ganzzahlen werden normalerweise mit einem Schema namens Two's Complement dargestellt. Der Wert -1 des vom Typ int8 codierten Typs wäre alle 1s 1111 1111 .

Der ABI füllt negative Ganzzahlen mit 1s auf, sodass -1 wie folgt aufgefüllt wird:

Kleine negative Zahlen sind meistens 1s, was Sie ziemlich viel Benzin kostet.

¯ \ _ (ツ) _ / ¯

Schlussfolgerung

Um mit einem Smart Contract zu interagieren, senden Sie ihm Rohbytes. Es führt einige Berechnungen durch, ändert möglicherweise seinen eigenen Status und sendet Ihnen dann Rohbytes zurück. Methodenaufruf existiert eigentlich nicht. Es ist eine kollektive Illusion, die vom ABI geschaffen wurde.

Der ABI wird wie ein Low-Level-Format angegeben, in seiner Funktion ähnelt er jedoch eher einem Serialisierungsformat für ein sprachübergreifendes RPC-Framework.

Wir könnten Analogien zwischen den Architekturebenen von DApp und Web App ziehen:

Wenn Ihnen dieser Artikel gefallen hat, sollten Sie mir auf Twitter @hayeah.

folgen

In dieser Artikelserie über das EVM schreibe ich über:

Um mehr über Solidity und EVM zu erfahren, abonnieren Sie mein wöchentliches Tutorial: