Adressen und Transaktionen in Kryptowährungen

Teil 2: Ethereum

Kryptowährungen sind in aller Munde. Tutorials und Erklärungen gibt es zuhauf. In dieser Artikelserie soll ein bestimmter Aspekt, nämlich der der Adressen und Transaktionen, detailliert für populäre Blockchains geklärt werden. Denn unter der Oberfläche verbirgt sich einiges an Komplexität.

Dieser Artikel setzt Grundkenntnisse in Ethereum voraus. Leser*innen sollten mit den Begrifflichkeiten Schlüssel(paar), Signatur, Ethereum, Mining, Blöcke und Blockchain vertraut sein. Dazu eignet sich der Blockchain-Podcast mit Stefan Tilkov und Lukas Dohmen hervorragend. Teil 3 dieser Serie dreht sich um Altcoins.

Im ersten Artikel dieser Serie ging es um die Kryptowährung Bitcoin. Nun möchte ich Ethereum näher beleuchten. Dabei handelt es sich um eine Blockchain-Technologie, die von Anfang an auf die Ausführung von Code ausgelegt ist.

Die Homepage verkündet:

Ethereum is a decentralized platform that runs smart contracts: applications that run exactly as programmed without any possibility of downtime, censorship, fraud or third-party interference.

Daher rührt auch die Bezeichnung The World Computer für Ethereum.

In diesem Abschnitt möchte ich Ethereum gegenüber Bitcoin abgrenzen und erläutern, wie Transaktionen in Ethereum funktionieren. Doch kurz vorab: bei Ethereum gibt es begrifflich einen Unterschied zwischen der Blockchain und der eigentlichen Kryptowährung, was am Ende des Artikels noch erläutert wird.

Konten

Wir erinnern uns: In Bitcoin können Transaktionen mehrere Quellen und Senken haben. Als Quellen sind bisher unverbrauchte Senken zulässig.

Ethereum hingegen funktioniert viel eher wie eine klassische Bank. Sogenannte Externally Owned Accounts (EOAs) sind von einem Schlüsselpaar gedeckt. Transaktionen haben genau ein Zielkonto und spezifizieren optional Betrag (Value) und Daten. Fürs erste können wir die Daten ignorieren. Der Betrag gibt die Menge an Ether an – die Währung von Ethereum –, die das Zielkonto erhalten soll. Das Quellkonto ergibt sich aus der Signatur der Transaktion.

Ethereum unterscheidet mehrere Arten von Transaktionen, je nachdem, ob Daten oder Betrag vorhanden sind:

Zahlung
Eine Transaktion, die einen Betrag enthält.
Aufruf
Eine Transaktion, die Daten enthält.

Beides lässt sich kombinieren. Transaktionen, denen beides fehlt, sind zwar möglich, aber nutzlos.

Übertragen in Java-Quelltext kann man sich eine Transaktion wie folgt vorstellen:

interface Transaction {
    // signature: contains public key of EOA
    Signature getSignature();

    // recipient address, can be any address type
    Address getRecipient();

    // amount of ether to be sent, can be 0
    Value getValue();

    // data for a call, can be empty
    String getData();
}

Der Begriff Aufruf deutet schon an, dass Ethereum in irgendeiner Form Routinen unterstützt.

Verträge

Neben EOAs gibt es in Ethereum auch Vertragskonten, für die sich der Begriff Smart Contract etabliert hat. Verträge werden im Gegensatz zu EOAs nicht von einem Schlüsselpaar repräsentiert; stattdessen werden sie von einem anderen Konto aus erzeugt.

Ähnlich wie ein EOA kann auch ein Vertrag über einen Kontostand (Balance) verfügen. Darüber hinaus enthält ein Vertrag auch Bytecode der Ethereum Virtual Machine (EVM) sowie quasi beliebigen Zustand.

Übertragen auf Java entspricht ein Contract einer Instanz einer Klasse mit öffentlichen und privaten Methoden. Von einer Klasse kann es mehrere Instanzen geben und auch ein Vererbungskonzept existiert. Üblicherweise schreibt man Klassen nicht direkt in JVM-Bytecode, sondern benutzt einen Compiler, der Hochsprache in Bytecode übersetzt. In Ethereum heißt die populärste Sprache Solidity.

Verträge werden niemals von selbst tätig, sondern können nur auf eingehende Aufrufe reagieren und dabei auch mit anderen Verträgen interagieren oder Beträge an EOAs versenden. Trotz Ethereums Natur als verteiltes System sind Verträge daher nie nebenläufig: Wenn eine Transaktion einen Vertrag erzeugt oder aufruft, werden alle Programmschritte sequenziell ausgeführt. Wichtig ist, dass alle diese Verarbeitungsschritte von einer einzigen Transaktion ausgehen: ein Vertragsaufruf, der zu weiteren Vertragsaufrufen führt, erzeugt dabei keine neuen Transaktionen. Eine Transaktion kann immer nur von einem EOA ausgehen.

Erzeugt wird ein Vertrag, in dem man eine Transaktion generiert, die in den Daten den Bytecode enthält und als Empfängeradresse die spezielle Adresse 0x0 definiert hat.

Ein einmal definierter Vertrag ist unveränderlich und kann prinzipiell für alle Ewigkeit von beliebigen Transaktionen aufgerufen werden. Ein möglicher Lebenszyklus lässt sich wie folgt darstellen:

Lebenszyklus eines Vertrags
Lebenszyklus eines Vertrags

Ein Vertrag ist insofern ähnlich zu einer Kapitalgesellschaft im Wirtschaftsrecht: Er existiert unabhängig von Personen (EOAs) und kann selbst Verträge aufrufen und aufgerufen werden sowie Geld versenden und empfangen. Wenn das Subjekt, das den Vertrag erzeugt hat, die Kontrolle über seinen privaten Schlüssel verloren hat, existiert der Vertrag trotzdem weiter. Die einzige Möglichkeit, einen Vertrag wieder aufzulösen, besteht darin, dass der Vertrag die SELFDESTRUCT-Operation ausführt. Diese Möglichkeit muss der Vertrag selbst vorgesehen haben; Konto A darf nicht einfach so Vertrag V auflösen.

Hochsprache Solidity

Verträge direkt in Bytecode zu implementieren, wäre so ähnlich wie Assembler-Code zu schreiben. Daher benutzt man eine Hochsprache wie Solidity, eine getypte, objektorientierte Sprache für Verträge. Das folgende Beispiel zeigt eine einfache Implementierung eines Vertrages, der im Auftrag eines anderen Kontos Währung verwaltet:

pragma solidity ^0.4.22;

contract Wallet {
    uint256 balance;
    address owner;
    constructor () public {
        balance = 0;
        owner = msg.sender;
    }
    function addfund() payable public returns (uint256) {
        require (msg.sender == owner);
        balance += msg.value;
        return balance;
    }
    function withdraw() public {
        require (msg.sender == owner);
        selfdestruct(owner);
    }
}

Ein Solidity-Programm beginnt mit einem pragma, welches die Sprachversion festlegt. Leider ist dies oft notwendig, denn Solidity verfügt über keine formale Spezifikation und weist daher fast ausschließlich – wie der C-Standard sagen würde – implementation defined behaviour auf. Dank Versionsnummer kleiner eins dürfen auch des öfteren inkompatible Änderungen in den Compiler Einzug halten.

Obiger Contract erinnert stark an eine Klasse. Er verfügt über zwei Instanzvariablen: balance und owner.

Der Konstruktor nutzt ein spezielles Schlüsselwort. Sobald ein Vertrag durch Transaktion an die Empfängeradresse 0x0 erzeugt wird, wird dieser Konstruktur ausgeführt. Hier wird die balance mit 0 initialisiert und anschließend owner auf die Absenderadresse gesetzt.

Die addfund-Methode ist öffentlich aufrufbar (public) und kann bei Transaktionen auch Ether entgegennehmen (payable). Sie führt folgende Schritte durch:

  1. Eine aufrufende Transaktion muss von derselben Adresse stammen, die auch den Vertrag angelegt hat.
  2. Die balance wird um den gesendeten Betrag erhöht.
  3. Die neue balance wird zurückgeliefert.

Eigentlich ist die balance-Variable redundant: Das Ethereum-Netzwerk weiß zu jedem Zeitpunkt, wie viele Ether in einem Vertrag hinterlegt sind. Dies passiert immer implizit, wenn eine payable-Methode aufgerufen wird.

Schlussendlich gibt es noch die withdraw-Methode. Auch hier wird die Aufruferadresse geprüft. Anschließend zerstört sich der Vertrag selbst und sendet die gesamten hinterlegten Ether an die owner-Adresse.

Kompiliert zu Bytecode, sieht der Vertrag so aus:

608060405234801561001057600080fd5b506000808190555033600160006101000a81548173ffff
ffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffff
ffffffff1602179055506101be806100686000396000f30060806040526004361061004c57600035
7c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063
3ccfd60b146100515780638f9595d414610068575b600080fd5b34801561005d57600080fd5b5061
0066610086565b005b61007061011d565b6040518082815260200191505060405180910390f35b60
0160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffff
ffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614
15156100e257600080fd5b600160009054906101000a900473ffffffffffffffffffffffffffffff
ffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b60006001600090549061
01000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffff
ffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017b5760
0080fd5b3460008082825401925050819055506000549050905600a165627a7a72305820b4d2d5a2
acfa0e197908c7c57826d35a987f64b29f8b97020a4ca96f63ce66630029

Gerade wegen der Unzulänglichkeiten in Solidity ist es bei wichtigen Verträgen durchaus üblich, dass Autor*innen sich den generierten Bytecode noch einmal ansehen, gegebenenfalls auch unter Zuhilfenahme von Tools.

Betrachten wir nun eine Beispielinteraktion mit dem Vertrag Wallet. Ein Aufruf muss als Daten zwingend erhalten, welche Methode mit welchen Parametern ausgeführt werden soll. (In unserem Fall gibt es keine Parameter.) Werden die Daten weggelassen, wird standardmäßig eine Ausweichmethode aufgerufen, die aber in unserem Vertrag nicht definiert ist. Eine Transaktion ohne Daten (d.h. eine einfache Zahlung) würde folglich scheitern.

Beispielinteraktion
Beispielinteraktion

Im obigen Diagramm ist explizit von Transfer die Rede, um eine Abgrenzung zu Zahlung zu illustrieren: Eine Zahlung steht für eine eigene Transaktion; ein Transfer heißt hier nur, dass innerhalb einer Transaktion eine Gutschrift für Konto A stattfindet.

Der Vertrag kann also als virtueller Geldbeutel bezeichnet werden. Striche man die require-Anweisung aus der addfund-Methode, würde aus dem Vertrag eine Spendenkasse, die Einzahlung durch beliebige Adressen erlaubt. Sinnvollerweise müsste man dann aber auch selfdestruct durch einen einfachen Transfer ersetzen (mittels owner.send(balance)).

Die wesentlichen Unterschiede zu Bitcoin-Skripten lassen sich daher so zusammenfassen, denn diese:

  1. können nur ein einziges Mal (erfolgreich) aufgerufen werden
  2. können nicht mit anderen Skripten interagieren.
  3. folgen einem primitiven Programmiermodell, das nur Stack-Manipulationen ohne Rekursion, Schleifen, und Schreiben von externem Zustand zulässt (Lesen von externem Zustand ist hingegen auch in Bitcoin erlaubt, z.B. die aktuelle Zeit)

Man nennt Sprachen wie den EVM-Bytecode auch Turing-vollständig, da sie in der Lage sind, beliebige berechenbare Funktionen auszuführen. Klammert man also Ein-/Ausgabeoperationen aus, lassen sich alle bekannten Algorithmen auch auf der EVM implementieren.

Weitere Beispiele für Smart Contracts haben Stefan Tilkov und Marc Jansing in ihrem Artikel herausgearbeitet.

Adresskompatibilität

Die starke Ähnlichkeit von Ethereum-Konten zu gewöhnlichen Bankkonten wird auch dadurch untermauert, dass es ein Adressformat gibt, welches an das IBAN-Format angelehnt ist. Dieses Inter exchange Client Address Protocol (ICAP) erzeugt fiktive IBAN-Adressen, z.B.

XE60HAMICDXSV5QXVJA7TJW47Q9CHWKJD

Fiktiv ist diese Adresse, weil XE als Länderkennung im IBAN-Standard nicht vorgesehen ist. Diesem Code folgt die Prüfziffer (60), die nach den üblichen IBAN-Regeln gebildet wird. Schlussendlich folgt die Repräsentation der Ethereum-Adresse; allerdings passen in die 30 Stellen nur 155 von 160 Bit. Man benötigt also eine Adresse, deren erstes Byte Null ist.

Dieses Adressformat ist bis auf Weiteres als Spielerei zu betrachten, denn eine SEPA-Überweisung an eine Ethereum-Adresse kann man auch mit dem ICAP-Format nicht durchführen. Desweiteren kann man nur mit einer ICAP-Adresse keine Daten an einen Vertrag übermitteln.

Ethereum vs. Bitcoin

Ein paar Unterschiede zwischen Bitcoin und Ethereum haben sich bereits herauskristallisiert, es gibt aber noch mehr Vergleichspunkte:

Wertschöpfung
Auch Ethereum kennt Wertschöpfung durch Mining. Im Gegensatz zu Bitcoin gibt es aber keine Coinbase-Transaktionen. Stattdessen enthält ein Block die Adresse des Miners, welcher implizit fünf Ether (+ X, nach einer komplizierten Formel) gutgeschrieben werden.
Mehrfachverwendung einer Adresse
In Ethereum ist es üblich, eine Adresse mehrfach zu nutzen. Insbesondere bei Verträgen bleibt die Adresse über die Lebenszeit konstant. Es ist nicht möglich, aus einem existierenden Vertrag bei jeder Transaktion einen neuen Vertrag zu konstruieren, es sei denn, dies wurde im Vertrag explizit so vorgegeben.
Wechselgeld
Da Ethereum-Transaktionen immer nur genau eine Empfängeradresse spezifizieren, ist es weder möglich noch notwendig, eine Wechselgeldadresse anzugeben. Insofern entspricht Ethereum viel eher einem klassischen Bank-ähnlichem Kontenmodell.
Adresserzeugung
Bei Bitcoin ergeben sich sämtliche Adressen aus dem Hash eines Keys. Bei P2PKH ist die Adresse komplett beliebig (außer man benutzt BIP 32/BIP 39). Bei P2SH ist die Adresse vom Skript abhängig. Bei Ethereum verhält es sich ähnlich, allerdings werden Vertragsadressen aus der Erzeugeradresse und einem Transaktionszähler (nonce) ermittelt.
Ausdrucksstärke
EVM-Verträge können im Gegensatz zu Bitcoin-Skripten beliebige Algorithmen abdecken. Sie können auch mehr Bedingungen stellen. So ist es beispielsweise für eine Bitcoin-Transaktion T zwar möglich, bestimmte Anforderungen an eine Transaktion U zu stellen, die T als Quelle angibt, aber die Senke von U kann nicht beeinflusst werden. Allerdings muss U nicht zwingend existieren. In Ethereum können dagegen beliebige Geldflüsse definiert werden, die aber dann auch komplett ablaufen.
Gebühren
Dieses Thema wurde im Artikel noch gar nicht angesprochen. Bei beiden Blockchain-Systemen gibt es Transaktionsgebühren, die Miner zusätzlich zur Block-Belohnung kassieren. Diese Gebühren können von jedem Subjekt, das eine Transaktion anstößt, frei festgelegt werden. Je höher die Gebühr, desto eher haben die Miner einen Anreiz, die Transaktion in den nächsten Block aufzunehmen.
In Bitcoin ist diese Gebühr fix und prinzipiell unabhängig vom Skript. Sie wird festgelegt, in dem eine Differenz zwischen Summe von Senken und Summe von Quellen übrig gelassen wird; d.h. die Transaktion verteilt nicht alle Senken vollständig. Je größer eine Transaktion ist (z.B. mehr Quellen), desto mehr Gebühren werden Miner verlangen.
In Ethereum ist diese Gebühr ein Multiplikator: jede Instruktion in der EVM, einschließlich reine Zahlungen oder Erzeugung von Verträgen, haben einen zugeordneten Spritverbrauch. Der Spritpreis multipliziert mit dem Gesamtverbrauch ergibt die Gesamtgebühr. Da sich zur Beginn einer Transaktion noch nicht exakt sagen lässt, wie viele Instruktionen tatsächlich ausgeführt werden müssen, gibt jede Transaktion zusätzlich zum Spritpreis noch die Tankgröße an. Wenn der Tank ausreicht, wird dem Miner der Gesamtpreis gutgeschrieben. Läuft der Tank leer, wird die Transaktion (fast) folgenlos abgebrochen, denn der Miner erhält trotzdem den verbrauchten Spritpreis.
Währung
Bei beiden Netzwerken gibt es eine eingebaute Währung: Bei Bitcoin Bitcoins und bei Ethereum Ether. Über diese Währung werden Transfers und Gebühren abgewickelt. Mittels spezieller Verträge können bei Ethereum aber auch virtuelle Währungen angelegt werden (sogenannte Tokens). Seit EIP/ERC 20 existiert dafür auch ein Standard. Stand Februar 2019 gibt es im öffentlichen Ethereum-Netzwerk bereits weit über 150.000 solcher Token-Verträge. Im Gegensatz zu Bitcoin ist Ethereum daher nicht nur eine Kryptowährung, sondern auch eine Plattform für Kryptowährungen.

Fazit

Ethereums Adress- und Transaktionssystem ist deutlich einfacher aufgebaut als bei Bitcoin. Allerdings können in einer Transaktion quasi beliebige Rechenoperationen ausgeführt werden, weshalb ihre Folgen – z.B. Überweisungen – schwieriger vorauszusagen sind. Im dritten und letzten Teil der Serie werde ich noch auf die restlichen PoW-Währungen in der Top-20-Liste eingehen.

TAGS

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen