Die Ethereum Virtual Machine

Um das Prinzip der Smart Contracts in Ethereum zu verstehen, ist es notwendig, zunächst einen kurzen Exkurs in die Welt der Programmiersprachen zu unternehmen.

Die gängigen Programmiersprachen zerfallen grundsätzlich in zwei Gruppen: interpretierte und kompilierte Sprachen. Bei der ersten Gruppe schreibt man Programme in einer Hochsprache, die dann von einem Interpreter ausgeführt werden. Zwei populäre Beispiele dafür sind Python und JavaScript. Interpretierte Sprachen sind in vielen Anwendungsdomänen (wie zum Beispiel im Web) verbreitet, denn man kann sofort loslegen. Außerdem sind sie sehr universell und auf verschiedenen Plattformen einsetzbar, ohne dass man weiteres Tooling braucht.

Demgegenüber stehen die kompilierten Sprachen, bei der zunächst ein Compiler den Programmtext in eine andere Sprache – oft binärer Maschinencode – überführt. Dieser Binärcode ist plattformabhängig und wird direkt auf einem Prozessor ausgeführt. Der Compiler kann (und muss) maßgeschneiderten Code für den Befehlssatz des Prozessors ausgeben, zum Beispiel für ARM- oder Intel-kompatible CPUs. Bekannte Vertreterinnen dieses Typus sind C und Rust.

Die Realität ist jedoch wie immer komplexer als diese einfachen Kategorien suggerieren. Seit längerer Zeit gibt es Mischformen, wie zum Beispiel Java. Der Java-Compiler übersetzt nämlich Java-Code nicht direkt in „echten“ Maschinencode, sondern in ein spezielles Zwischenformat. Dieses Zwischenformat wiederum wird dann von einem Interpreter – der Java Virtual Machine – auf der konkreten Prozessorarchitektur ausgeführt.

Auf ähnliche Weise funktionieren auch die Smart Contracts in Ethereum. Sämtliche Knoten, die in Ethereum Transaktionen validieren und neue Währung schürfen, beinhalten eine Instanz der Ethereum Virtual Machine (EVM). Im „Yellow Paper“, der technischen Spezifikation von Ethereum, ist haarklein definiert, welche Instruktionen die EVM unterstützt und wie diese auszuführen sind. Es handelt sich dabei um eine Eigenentwicklung, die zahlreiche Besonderheiten aufweist:

  • Interaktion mit der äußeren Welt ist nicht möglich: sämtliche algorithmische Entscheidungen müssen sich aus der Blockchain und ihren Transaktionen ergeben.
  • Arithmetik basiert auf 256-Bit-Werten, um das Hantieren mit Adressen und größeren Geldbeträgen zu erleichtern.
  • Spezialoperationen wie Hashfunktionen sind zur Performancesteigerung direkt eingebaut.
  • Sämtlichen Instruktionen ist eine Kostenfunktion (engl. „fuel“) zugeordnet, die grob der nötigen Ausführungszeit und dem Speicheraufwand entspricht (dazu später mehr). Im Englischen ist der Begriff Metering üblich.

Programmieren auf der EVM

Ähnlich wie im Java-Ökosystem gibt es mehrere Programmiersprachen, für die EVM-Compiler zur Verfügung stehen. Die verbreitetste Sprache ist Solidity, die oberflächlich (syntaktisch) an JavaScript erinnert. Stand 2020 listet die Ethereum-Dokumentation noch zwei weitere Sprachen; Vyper, welche sich an Python orientiert und Yul+, eine komplett eigenständige Entwicklung.

Gemein ist allen diesen Sprachen, dass sie Domänen-spezifisch sind, denn im Gegensatz zu den General-purpose languages besetzen sie eine Nische mit speziellen Features und insbesondere einer speziellen Runtime: der EVM.

Natürlich sind solche Domänen-spezifische Sprachen (DSLs) grundsätzlich eine gute Idee, um die Komplexität von Anwendungen zu reduzieren. Doch im Falle der EVM erscheint das im Nachhinein wenig sinnvoll, denn – ungeachtet der fehlenden Interaktionsmöglichkeiten mit der Blockchain-externen Welt – sie kann beliebige Algorithmen ausführen; ist also (vereinfacht ausgedrückt) Turing-vollständig.

Warum bedient man sich nicht also einer existierenden Sprache und Laufzeitumgebung? Gegebenfalls müsste man dann zwar Features entfernen, könnte aber auf eine längere Erfahrung, stabileres Tooling und – viel wichtiger – eine breitere Basis von Programmierer:innen zurückgreifen. Denn es ist ein alter Hut, dass heutzutage für die Popularität einer Programmiersprache nicht nur ausschlaggebend ist, ob man in ihr besonders konzis, typsicher, dynamisch oder sonstwie programmieren kann, sondern auch, wie einfach man auf wie viele existierende Bibliotheken und Pakete zugreifen kann. Besonders wird dieser Zeitenwandel von JavaScript illustriert, welche oft für seine krude Semantik kritisiert wird, aber spätestens seit dem millionsten npm-Paket als populärste Programmiersprache gelten muss.

WebAssembly, eine universelle Zwischensprache

Die Menschen hinter der Ethereum-Spezifikation sind zu dem Schluss gekommen, dass sich einige Probleme durch eine Migration weg von einer Eigenentwicklung hin zu einer Universalsprache lösen lassen. Wie praktisch, dass im Web gerade die Entwicklung im Gange ist, eine Alternative zum Platzhirsch JavaScript zu etablieren: WebAssembly (WASM). Dabei handelt es sich zugleich um eine universelle Zwischensprache und ein Binärformat, gepaart mit einer Spezifikation für Interpreter. Der offene Standard war – wie der Name bereits nahe legt – ursprünglich für das Web ersonnen worden; mittlerweile werden aber auch weitere Anwendungsgebiete (z.B. Smartphone-Apps) diskutiert.

Vorangetrieben wird die Entwicklung von WASM von Branchengrößen wie Microsoft, Google und Apple.

Die Sprache zeichnet aus, dass sie von Anfang an für Portabilität ausgelegt ist. Erkennbar ist das daran, das zahlreiche existierende Programmiersprachen wie Rust, C++ oder Go bereits für WebAssembly kompilieren können.

Eine perfekte Grundlage also auch für Smart Contracts? Tatsächlich scheint WebAssembly wie geschaffen für diesen Einsatzzweck. Auch im Browser – die ursprüngliche Domäne für WASM – ist es nötig, den laufenden Code stark zu reglementieren, um Sicherheitslücken zu verhindern. Ferner sorgt die Tatsache, dass WASM keine Garbage Collection aufweist, man sich also als Programmierer:in selbst um die Speicherverwaltung kümmern muss, für die deterministische Ausführung von Algorithmen. Als Dreingabe gibt es solide und effiziente Interpreter, die für verschiedene Plattformen verfügbar sind.

Alles das sind auch Kriterien, die für die Gestaltung einer Blockchain äußerst wichtig sind. Daher wird die Verwendung von Ethereum flavored WebAssembly (EWASM) folgerichtig in der Langzeitplanung von Ethereum 2 angestrebt. EWASM ist vollständig kompatibel zu WASM; führt allerdings eine zusätzliche Schnittstelle zu der Blockchain ein, um typische Ethereum-Operationen steuern zu können (z.B. den Transfer von Tokens). Für Ethereum-Nutzer:innen ist die Interaktion mit EWASM-Smart-Contracts transparent und funktioniert genau wie EVM-Verträgen.

Smart Contracts in WebAssembly

Wie aber geht man vor, wenn man einen Smart Contract in EWASM programmieren will?

Zum einen benötigt man ein passendes Ethereum-Netzwerk. Denn so schön die Vision von Ethereum 2 ist, so ist sie eben nur eine Vision.

Unglücklicherweise existiert kein offizielles Testnetzwerk, aber man kann auf Oasis Ethereum ausweichen, welches mit Ethereum kompatibel ist und gewissermaßen eine Vorschau auf das zukünftige Ethereum 2 bietet. Das genaue Vorgehen, um den lokalen Browser hierfür zu konfigurieren, habe ich hier beschrieben.

Außerdem braucht man noch eine Programmiersprache, um keinen Assembly-Code von Hand schreiben? Martin Helmich schrieb in Ausgabe 60 bereits von den Vorzügen von Rust. Dank Unterstützung von WASM im Rust-Compiler lassen sich direkt kompatible Smart Contracts aus einem Rust-Programm erzeugen. Allerdings ist Rust derzeit keine vernünftige Wahl für Ethereum, denn die Toolchain ist noch zu fragil. Die meisten Beispiele, die man dafür im Netz findet, sind kurz nach Veröffentlichung schon wieder veraltet.

Deswegen lohnt sich hier stattdessen ein Blick auf Solidity. Das klassiche Beispiel für einen Smart Contract in Solidity ist ein ERC-20-Token, eine standardisierte Schnittstelle, um beliebige Token auf der Ethereum-Chain zu verwalten. Standardmäßig verwendet Ethereum die Währung Ether. Mit ERC-20-Token kann man weitere Währungen definieren, beispielsweise ein Token, der 1:1 gegen Euro eingetauscht werden kann.

Ein solcher Vertrag hat eine feste Struktur. Für die Oasis-Chain existiert bereits ein Beispiel. Grundsätzlich gibt es in dem Vertrag Speicherfelder für:

  • Umlaufmenge der Token (d.h. es kann eine Obergrenze festgelegt werden)
  • derzeitige Kontostände

In Solidity lässt sich das wie folgt definieren:

uint256 private totalSupply;
mapping(address => uint256) private balances;

Schließlich gibt es noch Methoden, um Token von einem Konto zu einem anderen zu transferieren. Dazu muss man zunächst sicherstellen, dass kein Under- oder Overflow passiert:

function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a, "SafeMath: addition overflow");

    return c;
}

function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b <= a, "SafeMath: subtraction overflow");
    uint256 c = a - b;

    return c;
}

Diese beiden Methoden dienen der sicheren Addition bzw. Subtraktion von zwei Zahlen. Wenn z.B. auf dem Quellkonto nicht genügend Token vorhanden sind, dann wird die Transaktion mit einer Fehlermeldung („subtraction overflow“) abgebrochen.

Die eigentliche Transferfunktion ist dann schnell implementiert:

function transfer(
    address recipient,
    uint256 amount
) {
    balances[msg.sender] = sub(balances[msg.sender], amount);
    balances[recipient] = add(balances[recipient], amount);
}

Zunächst wird der Kontostand beim Sender verringert, anschließend bei der Empfängerin erhöht. Ein Aufruf dieser Methode ist immer transaktional; d.h. wenn entweder der Empfänger bereits zuviel oder die Absenderin nicht genug Token hat, dann wird keine der beiden Operationen durchgeführt.

Mittels des SOLL-Compilers lässt sich der obige Solidity-Code nach WebAssembly übersetzen.

Deployment und was man sonst noch so tun kann

Diese fertige WASM-Datei kann man wie üblich bei Ethereum mit web3.js auf einer Blockchain deployen. Möchte man wie weiter oben beschrieben Oasis benutzen, lässt sich ein entsprechender Provider konfigurieren. Auch das habe ich in der Anleitung beschrieben. Man kann dabei entweder im Browser oder in Node.js arbeiten.

Um den Vertrag zu deployen, muss man eine neue Transaktion erstellen, die keine Empfängeradresse angegeben hat. Als Daten übergibt man den Hex-kodierten String des WebAssembly-Codes:

const receipt = await web3.eth.sendTransaction({ from: 0, data: "0061736d...", gas: 10000000 });
console.log(receipt.contractAddress);

Wenn alles richtig gelaufen ist, gibt die letzte Zeile die Adresse des neu erstellten Vertrags aus:

'0xad1c3896b09F86906030654F6e92D61bf0D24293'

Neben der Verwendung in einer Blockchain kann der vom Compiler erzeugten WASM-Code theoretisch auch unverändert im Browser ausführen, sofern man die nötigen Ethereum-Funktionen bereitstellt.

Die WebAssembly-Tools bieten auch noch eine Reihe andere Möglichkeiten der Verarbeitung, z.B. lässt sich der Binärcode nach C übersetzen. Es wäre also durchaus möglich, den gleichen Code sowohl in Smart Contracts als auch in klassischen Applikationen zu benutzen.

Einen fundierten Einstieg in das Thema Blockchain-Technologien bietet unsere iSAQB-zertifizierte Online-Schulung „Blockchain – Konsens in dezentralisierten Applikationen” mit Dr. Lars Hupel.

Conclusion

Mit EWASM hat die Ethereum-Community die Weichen gestellt, um Smart Contracts zukünftig auf eine solide Basis zu stellen. Im Gegensatz zur selbstgestrickten EVM bietet WebAssembly handfeste Vorteile, zum Beispiel die breite Anwendbarkeit und höhere Sicherheit dank zahlreicher Analysemöglichkeiten. Verschiedene Ethereum-Clients implementieren diese alternative VM bereits, sind aber teilweise noch zueinander inkompatibel. Schade ist nur, dass die Rust-Toolchain noch nicht sauber in Ethereum integriert ist. Da die Entwicklung von Ethereum 2.0 aber laufend voranschreitet und ein hohes Interesse an WebAssembly herrscht, gibt es hoffentlich bald robusteres Tooling.

TAGS

Comments