Corda ist ein flexibles Framework, das es ermöglicht, Verträge elektronisch abzubilden. Dabei wurde Wert darauf gelegt, den Entwickler bestmöglich zu unterstützen. Ein weiteres wichtiges Merkmal ist die Privatsphäre. Nur ein bestimmter Teilnehmerkreis kann Transaktionen validieren und somit ist Corda eine „Permissioned Blockchain“. Der Kern von Corda wurde in der Programmiersprache Kotlin geschrieben. Fachliche Lösungen können dadurch in einer JVM-kompatiblen Technologie erstellt werden. In diesem Artikel wird dafür Java verwendet.

Parteien

Damit geteilte Fakten entstehen können, braucht es mindestens zwei Parteien. Parteien in Corda sind mit einem offiziellen Namen identifizierbar, d. h., der öffentliche Schlüssel (Public Key) jeder Partei ist an eine juristische Person gebunden. Corda hält sich dabei an den X.509-Standard [1]. Auch geht man bei Corda davon aus, dass man seinen Vertragspartnern grundsätzlich vertrauen kann. Bei einer Streitigkeit muss aber auch hier ein Gericht entscheiden. Eine Partei wird in Corda oft auch als Node bezeichnet.

Geteilte Fakten

Wünscht man sich in einem B2B-Umfeld, dass die Verträge eines Unternehmens von jedem eingesehen werden können? Nein, bestimmt nicht. Bei einem Netzwerk, wie beispielsweise Bitcoin ist aber genau das der Fall. Die Regeln sind einsehbar, und jede Transaktion kann von allen Beteiligten des Netzwerkes angeschaut werden. Die Transaktionen bei Bitcoin sind nicht verschlüsselt und z. B. mit dem Blockchain Explorer einsehbar [2]. Bei Corda werden die geteilten Fakten nicht über die ganze Welt verteilt, sondern nur an einen vordefinierten Kreis von Teilnehmern. Das geschieht mit privaten Transaktionen. Am besten lässt sich das an einem Venn-Diagramm veranschaulichen (Abb. 1).

Abb. 1: Geteilte Fakten

Die kleinen weißen Kreise stellen geteilte Fakten dar. Von Fakt A wissen nur Roger und Jürg, von Fakt B nur Jürg und André und von Fakt C nur André und Roger. Von Fakt D wissen Roger, Jürg und André. Die Fakten werden also nur mit denjenigen geteilt, die daran wirklich ein Interesse haben. In der Terminologie von Corda implementiert ein Fakt das Interface ContractState, weil es einen bestimmten Zustand eines Vertrags widerspiegelt.

Evolution eines Fakts

Ein geteilter Fakt kann im Lauf der Zeit ungültig werden. Hat sich Roger beispielsweise 100 Euro von Jürg geliehen, mag das heute ein gültiger geteilter Fakt A sein. Zum Beispiel könnte Fakt A einen Zustand eines Schuldscheins repräsentieren (Abb. 2). Morgen aber könnte Roger bereits die Hälfte der Schulden begleichen und somit nur noch 50 Euro Schulden haben. Was passiert nun mit dem bereits existierenden geteilten Fakt A? Da es sich bei Corda gemäß R3 um eine Blockchain handelt und die Unveränderbarkeit ein wesentliches Merkmal von Blockchain ist, darf dieser bereits geteilte Fakt nicht verändert werden. Stattdessen wird ein neuer Fakt A' in einer neuen gültigen Transaktion erzeugt, indem der Zustand des Schuldscheines nur noch einen Betrag von 50 Euro aufweist. Der alte, früher gültige Fakt wird dabei historisiert (Abb. 2). Somit entsteht mit der Zeit eine Kette von geteilten Fakten.

Abb. 2: Evolution eines geteilten Fakts

Signer

Damit ein neuer Vertragszustand entstehen kann, müssen die Vertragsparteien dem neuen Zustand zustimmen - genauso, wie das auch in der analogen Welt der Fall ist. Will jemand einen Vertrag abändern, müssen alle Beteiligten erneut unterschreiben. Mit digitalem Signieren wird also zugestimmt, dass ein neuer Fakt entstehen kann. Mit Signer oder Unterzeichner sind alle diejenigen Parteien gemeint, die hier ein Mitspracherecht haben.

Transaktion

Eine Transaktion ändert den Zustand eines bestimmten Sachverhalts. Roger hat nun nur noch 50 Euro Schulden und nicht mehr 100 Euro. Damit eine solche Transaktion Gültigkeit hat, muss sie einem bestimmten Vertrag folgen und alle Regeln einhalten, die im Vertrag festgelegt wurden. Eine Transaktion in Corda kann Inputs und Outputs haben. Als Input wird ein gültiger, noch nicht historisierter, geteilter Fakt genommen. Stimmen alle als Signer definierten Beteiligten einer Transaktion zu, entsteht als Output ein neuer gültiger Fakt, sofern der Notary Service kein Veto einlegt. Weitere Informationen zum Notary Service sind im Kasten „Double Spending Problem” zu finden. Der neue Fakt ist sofort gültig, im Gegensatz zu Bitcoin, wo die Transaktionen in Blöcken gesammelt werden. Bei Bitcoin hat eine Transaktion nur Gültigkeit, wenn der Block in der längsten Kette ist, in der sich die Transaktion befindet.

Wie oben beschrieben, darf ein Transaktionsinput, der einen früheren Transaktionsoutput referenziert, nicht schon historisiert sein. Jeder Input darf nur einmal verwendet werden. Als Beispiel darf eine virtuelle Münze vom gleichen Besitzer nur einmal ausgegeben werden. Ansonsten könnten aus einem Euro plötzlich zwei Euro entstehen, was im Blockchain-Umfeld „Double Spending Problem“ genannt wird.

Double Spending Problem

Eine Transaktion kann neue Outputs erzeugen, die „Unspent Transaction Outputs” (UTXO) genannt werden. Diese Outputs können in einer neuen Transaktion wieder als Input dienen. Ein bestimmter UTXO darf aber immer nur in einer einzigen Transaktion als Input verwendet werden, ansonsten haben wir ein Double Spending Problem (1). Es geht also darum, dies zu verhindern. Dafür gibt es zwei Lösungsansätze: zentralisiert und dezentralisiert.

Bei Bitcoin wurde der dezentralisierte Ansatz gewählt, weil man in diesem Netzwerk grundsätzlich keiner zentralen Stelle vertrauen und auch keinen „Single Point of Failure“ haben möchte. Um das zu erreichen, wird der Proof-of-Work-Consensus-Algorithmus verwendet. Man nennt ein solches System irrtümlicherweise auch Trustless Transactional System (2), was natürlich nicht ganz stimmt, denn wir vertrauen hier den Minern, die einen Anreiz haben, das System nicht zu betrügen.

Auch bei Corda muss sichergestellt werden, dass ein verwendeter UTXO nicht in mehreren Transaktionen als Input verwendet wird. Dies geschieht bei Corda mit einem Notary Service, der bei einer vorliegenden Transaktion prüft, ob der Input nicht schon in einer früheren Transaktion verwendet wurde und deshalb schon historisiert ist. Erst wenn der Notary Service eine vorliegende Transaktion auch signiert hat, gilt sie als abgeschlossen, und der neue geteilte Fakt wird bei den Teilnehmern gespeichert. Meist besteht der Notary Service nicht nur aus einem einzigen Node, sondern es wird ein ganzer Pool von Notary Nodes erstellt, die von unterschiedlichen Teilnehmern zur Verfügung gestellt werden. Trotzdem handelt es sich bei Corda um einen zentralisierten Ansatz und man muss dem Notary Pool vertrauen können.

Verträge und Regeln

Damit eine Transaktion gültig ist, muss sie sich an einen vordefinierten Vertrag halten. In diesem Vertrag wird beispielsweise definiert, wie viele Inputs und Outputs eine Transaktion haben und von welchem Typ sie sein kann, oder von wem die Transaktion signiert werden muss. Ein Vertrag wird in Corda „Contract“ genannt und muss zwischen den Vertragsparteien im Vorhinein definiert werden. Dieser Contract ist Zustandslos und beschreibt nur die Regeln, wie aus Fakten neue Fakten entstehen können. Im vorigen Beispiel (100 Euro Schulden) müsste Jürg also zustimmen, dass Roger nach der Transaktion nur noch 50 Euro Schulden hat. Der Gläubiger wird daher im Vertrag als erforderlicher Unterzeichner definiert. Erfüllt eine Transaktion alle Regeln im Vertrag und stimmen alle Signierenden der Transaktion zu, entsteht ein neuer geteilter Fakt, auch Output genannt. Dieser Output wird nun bei allen Vertragsparteien in einer Datenbank gespeichert. Corda nennt diesen Speicherort für die aktuellen und historisierten Fakten auch Vault. In einer späteren Transaktion könnte dieser Output wieder als Input dienen.

Kommando einer Transaktion

Ein bestimmter Zustand (geteilter Fakt) eines Vertrags kann meist durch verschiedene Einflüsse geändert werden. Stellen Sie sich eine digitale Münze vor. Diese Münze muss von jemandem erzeugt werden und erhält einen Besitzer. Später will man diese Münze jemandem übertragen und erhält im Gegenzug ein physisches Produkt. Daraus kann man bereits zwei Transaktionstypen ableiten. Ein erster Transaktionstyp hätte den Zweck Geldschöpfung und ein zweiter den Geldtransfer. In Corda heißt dieser Zweck „CommandData“ und davon leiten wir eigene Kommandos ab. Somit hat eine Transaktion also ein Kommando oder sogar mehrere, und die zu verifizierenden Regeln im Vertrag können abhängig vom Kommando unterschiedlich sein. Nur mittels Inputs und Outputs einer Transaktion wird es schwierig zu ermitteln, was die Transaktion macht. Dank eines Kommandos wird die Absicht aber klar.

Abbildung 3 zeigt eine Transaktion mit dem Kommando Geldtransfer. Abhängig vom Kommando werden nun Regeln überprüft. Beispielsweise muss eine solche Transaktion mindestens einen Input haben. Hier im Beispiel sind es zwei, was auch möglich ist, weil zweimal 50 Euro auch wieder 100 Euro ergeben. Beim Validieren der Transaktion muss sichergestellt werden, dass Roger der Besitzer der Inputs ist und diese noch nie verwendet wurden. Der Output muss einen anderen Besitzer haben als die Inputs. Im Vertrag sollte zusätzlich auch definiert sein, dass der Betrag des Outputs gleich ist wie die Summe der Beträge der Inputs.

Abb. 3: Kommando einer Transaktion

Flows

Das Herzstück von Corda sind die Flows, die alle nötigen Schritte zusammenfassen, bis ein neuer geteilter Fakt in die Vaults der Netzwerk-Nodes geschrieben werden kann. Eine Transaktion muss von einem Vertragspartner erstellt, validiert und signiert werden. Danach ist es nötig, diese Transaktion den anderen Vertragspartnern via Netzwerk zur Verfügung zu stellen, damit sie auch dort validiert und signiert werden kann. Sobald alle der Transaktion zugestimmt haben, wird die Transaktion auch noch dem Notary Service zugestellt, der überprüft, ob es keine Double Spendings gibt. Ging alles mit rechten Dingen zu, wird der neue geteilte Fakt bei allen Vertragspartnern gespeichert. Flows können je nach Anwendungsfall völlig frei definiert werden, damit alle nötigen Geschäftsbeziehungen in Corda abgebildet werden können. Beim Schreiben eines Flows muss sich der Entwickler nicht mit Dingen wie Nebenläufigkeit beschäftigen. Ein Flow wird sequenziell geschrieben und hat zum Beispiel keine Callback-Funktionen oder dergleichen. Um das zu erreichen, nutzt Corda die Library Quasar [3], die leichtgewichtige Threads (sogenannte Fibers [4]) zur Verfügung stellt. Alle Netzwerkherausforderungen werden für den Entwickler von Corda abstrahiert, und der Entwickler kann sich auf die Geschäftslogik konzentrieren. Um einen Flow schreiben zu können, wird die Klasse FlowLogic erweitert.

Corda „ContractState“, „Contract“, „CommandData“ and „FlowLogic“ in Aktion

Es soll nun an einem kleinen Beispiel gezeigt werden, wie alle diese Teile zu einem Ganzen verschmelzen. Dazu werden Gutscheine kreiert und verschenkt. Die Gutscheine müssen aber nicht zwingend vom Empfänger akzeptiert werden. Vielleicht hat der Beschenkte überhaupt kein Interesse daran, einen Gutschein für eine CryptoKitty [5] zu erhalten. Einen Gutschein für eine richtige Katze würde er allerdings gerne annehmen.

Zuerst wird ein CouponState erstellt, der ContractState implementiert (Listing 1). Zur Erinnerung: Ein ContractState ist ein Fakt, der geteilt und dann zu einem „Shared Fact“ werden kann.

public class CouponState implements ContractState {

    private CouponSubject couponSubject;
    private final Party issuer;
    private final Party owner;
    private final int value;

    public CouponState(CouponSubject couponSubject, Party issuer, Party owner, int value) {
        this.couponSubject = couponSubject;
        this.issuer = issuer;
        this.owner = owner;
        this.value = value;
    }

    @NotNull
    @Override
    public List<AbstractParty> getParticipants() {
        return ImmutableList.of(issuer, owner);
    }

    public enum CouponSubject {
        REAL_KITTY,
        CRYPTO_KITTY;
    }

}
Listing 1: Implementierung des Fakts CouponState

CouponState hat vier Felder. Das Feld couponSubject beschreibt, wofür dieser Gutschein eingelöst werden kann, couponSubject kann die Werte REAL_KITTY oder CRYPTO_KITTY annehmen. Das Feld issuer verweist auf die Partei, von der der Coupon erstellt wurde. Mit dem Feld owner wird der Besitzer des Gutscheins referenziert, und value bildet den Wert des Gutscheins ab. Die Methode getParticipants wird vom Interface vorgeschrieben und muss alle Parteien zurückgeben, die ein Interesse an diesem CouponState haben. Diese Parteien speichern schlussendlich den CouponState. Nun wird der Vertrag CouponContract, in Corda Contract genannt, erstellt (Listing 2).

public class CouponContract implements Contract {

    public static String ID = "example.CouponContract";

    @Override
    public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException {
        List<CommandWithParties<CommandData>> commands = tx.getCommands();

        if(commands.size() != 1) {
            throw new IllegalArgumentException("must have one command");
        }

        if(commands.get(0).getValue() instanceof Commands.Issue) {
            validateRulesForIssueCommand(tx);
        } else {
            throw new IllegalArgumentException("only issue command is supported");
        }
    }

    private static void validateRulesForIssueCommand(LedgerTransaction tx) {
        List<ContractState> inputs = tx.getInputStates();
        List<TransactionState<ContractState>> outputs = tx.getOutputs();
        List<CommandWithParties<CommandData>> commands = tx.getCommands();

        if(inputs.size() != 0) {
            throw new IllegalArgumentException("must not have input states, the coupon will be issued here");
        }

        if(outputs.size() != 1) {
            throw new IllegalArgumentException("must have one output state");
        }

        if(tx.outputsOfType(CouponState.class).size() != 1) {
            throw new IllegalArgumentException("transaction output must be a coupon state");
        }

        CouponState couponState = (CouponState) tx.getOutput(0);

        if(couponState.getValue() < 1)  {
            throw new IllegalArgumentException("coupon state value must have a positive value");
        }

        Party issuer = couponState.getIssuer();
        Party owner = couponState.getOwner();
        List<PublicKey> signers = commands.get(0).getSigners();

        if(!signers.contains(issuer.getOwningKey())) {
            throw new IllegalArgumentException("issuer must be a required signer");
        }

        if(!signers.contains(owner.getOwningKey())) {
            throw new IllegalArgumentException("owner must be required signer");
        }
    }

    public interface Commands extends CommandData {
        class Issue implements Commands { }
    }

}
Listing 2: Implementierung des Vetrages CouponContract

CouponContract muss die Methode verify implementieren. Ist etwas nicht regelkonform, wird eine Exception geworfen. Das bedeutet, die Transaktion ist ungültig und hält sich nicht an den Vertrag, der mit allen Parteien zu Beginn geteilt wurde. Jede Partei validiert eine eintreffende Transaktion gegen die eigene Kopie des CouponContracts. Wird keine Exception geworfen, ist die Transaktion gültig, sofern sie auch vom Notary Service als gültig akzeptiert wird. Wie weiter oben beschrieben, können je nach Kommando eigene Regeln gelten, und die Transaktion muss von anderen Parteien signiert werden.

Zu guter Letzt muss die Geschäftslogik implementiert werden. Es stellen sich Fragen wie: Wer erstellt eine Transaktion? An wen muss sie geschickt werden, um signiert zu werden? Hier im Gutscheinbeispiel ist es einfach. Es braucht nur zwei Beteiligte, um den Gutschein zu erstellen: den Ersteller und den ersten Besitzer des Gutscheins. Listing 3 zeigt, wie der Gutschein durch eine Transaktion vom Ersteller initiiert wird.

@InitiatingFlow
@StartableByRPC
public class CouponIssueFlow extends FlowLogic<SignedTransaction> {

    private CouponState.CouponSubject subject;
    private final Party owner;
    private final int value;

    public CouponIssueFlow(CouponState.CouponSubject subject, Party owner, int value) {
        this.subject = subject;
        this.owner = owner;
        this.value = value;
    }

    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
        Party issuer = getOurIdentity();

        // neuen Transaktionsoutput erstellen, ein Input
        // ist bei der Erzeugung nicht nötig
        CouponState couponState = new CouponState(
            this.subject,
            issuer,
            this.owner,
            this.value
        );

        // neue Transaktion erstellen
        TransactionBuilder transactionBuilder = new TransactionBuilder(notary);
        transactionBuilder.addOutputState(couponState, CouponContract.ID);
        transactionBuilder.addCommand(
            new CouponContract.Commands.Issue(),
            issuer.getOwningKey(),
            owner.getOwningKey()
        );

        // überprüfen, ob eigene erstellte Transaktion die Regeln
        // des Vertrages erfüllt
        transactionBuilder.verify(getServiceHub());

        // signieren der Transaktion mit dem privaten Schlüssel des Herausgebers,
        // die Transaktion wird dabei unveränderlich
        SignedTransaction partlySignedTx = getServiceHub()
            .signInitialTransaction(transactionBuilder);

        // Transaktion vom Besitzer signieren lassen
        FlowSession ownerSession = initiateFlow(owner);
        SignedTransaction fullySignedTx = subFlow(
            new CollectSignaturesFlow(partlySignedTx, ImmutableSet.of(ownerSession))
        );

        // Transaktion vom Notary Service beglaubigen lassen und neuen geteilten Fakt
        // bei allen Beteiligten speichern
        return subFlow(new FinalityFlow(fullySignedTx));
    }

}
Listing 3: Implementierung des Flows CouponIssueFlow

Die Annotation @InitiatingFlow besagt, dass die Klasse CouponIssueFlow eine Kommunikation mit einem Geschäftspartner starten kann. @StartableByRPC macht diesen Flow über RPC startbar. @Suspendable bei der Methode call macht es möglich, dass die Ausführung in den Ruhezustand gelegt werden kann, etwa wenn der Geschäftspartner gerade nicht antworten kann. Innerhalb der Methode wird ein neuer CouponState erzeugt, der einer neuen Transaktion als Output übergeben wird. Dieser Output kann nur erzeugt werden, wenn sich die erstellte Transaktion an die Regeln des Vertrages hält, der via CouponContract.ID übergeben wird. Es wird hier keine Klasse selbst übergeben, sondern nur eine ID. Denn jeder Geschäftspartner soll das mit seiner eigenen Kopie der Klasse überprüfen. Nach dem Erstellen einer Transaktion wird sie vom Initianten selbst validiert und dann signiert. Danach muss die Transaktion dem Geschäftspartner – hier dem Besitzer des Gutscheins – zum Signieren vorgelegt werden. Dieser könnte der Transaktion auch nicht zustimmen. Wenn er es tut, dann wird die Transaktion vom Notary Service beglaubigt und der neue geteilte Fakt bei allen Geschäftspartnern gespeichert.

Nachdem der Initiant implementiert wurde, muss noch der Flow des Geschäftspartners, hier des neuen Besitzers des Gutscheins, implementiert werden (Listing 4).

@InitiatedBy(CouponIssueFlow.class)
public class CouponOwnerFlow extends FlowLogic<Void> {

    private final FlowSession counterpartySession;

    public CouponOwnerFlow(FlowSession counterpartySession) {
        this.counterpartySession = counterpartySession;
    }

    @Suspendable
    @Override
    public Void call() throws FlowException {

        // Wenn der Initiant die Signatur des Besitzers für die Transaktion anfragt,
        // dann muss darauf mit einem SignTransactionFlow geantwortet werden.
        class SignTxFlow extends SignTransactionFlow {
            private SignTxFlow(FlowSession otherSession, ProgressTracker progressTracker) {
                super(otherSession, progressTracker);
            }

            // SignTransactionFlow überprüft automatisch die Regeln des Vertrages,
            // innerhalb von checkTransaction können zusätzliche Dinge überprüft werden
            @Override
            protected void checkTransaction(SignedTransaction stx) throws FlowException {
                Party identity = getOurIdentity();
                CouponState couponState = (CouponState) stx.getTx().getOutput(0);
                Party ouputOwner = couponState.getOwner();
                if (!identity.equals(ouputOwner)) {
                    throw new IllegalArgumentException("we must be owner of the coupon");
                }

                if (couponState.getCouponSubject().equals(
                    CouponState.CouponSubject.CRYPTO_KITTY)) {
                    throw new IllegalArgumentException("we don't like crypto kitty coupons");
                }
            }
        }

        subFlow(new SignTxFlow(this.counterpartySession, SignTransactionFlow.tracker()));
        return null;
    }

}
Listing 4: Implementierung des Flows CouponOwnerFlow

Die Methode call der Klasse CouponOwnerFlow wird aufgerufen, wenn CouponIssueFlow die Signaturen einfordert. Beachten Sie dazu die Annotation @InitiatedBy(CouponIssueFlow.class). Darauf wird mit der Klasse SignTxFlow geantwortet, die von SignTransactionFlow abgeleitet ist. Die überschriebene Methode checkTransaction wird dazu genutzt, die Transaktion genauer zu untersuchen und abzulehnen, wenn etwas nicht den Vorstellungen entspricht. Im Beispiel oben sollen nur Gutscheine angenommen werden, die auch tatsächlich für diesen Vertragspartner bestimmt sind. Die anderen Transaktionen werden nicht signiert. Auch werden keine Gutscheine für den Kauf eines CryptoKitty angenommen. Nachdem wir signiert haben, läuft der Flow vom Initianten weiter.

Fazit

Man fragt sich zu Recht: Wo zum Geier sind die Blöcke? Tatsache ist: Corda sammelt seine Transaktionen nicht in Blöcken und verwendet auch keinen Proof-of-Work-Consensus-Algorithmus. Da es keine Blöcke gibt, gibt es auch keine Referenzen auf einen früheren Block. Somit ist Corda im eigentlichen Sinne auch keine richtige Blockchain. Verkettet sind die geteilten Fakten aber. Die Evolution, wie es von einem Fakt zum nächsten gekommen ist, ist klar nachvollziehbar.

Corda hat großes Potenzial, weil es Verträge, die früher auf Papier waren und von den Vertragsparteien mühsam einzeln von Hand unterschrieben werden mussten, einfach elektronisch abbilden kann. Das ganze Hin- und Hersenden von Papier kann eingespart werden. Somit dürften sich viele Prozesse drastisch beschleunigen. Corda stellt für den Entwickler alle nötigen Funktionen bereit, das einfach zu erreichen und vereinfacht zugleich die Netzwerkkommunikation.

Nach einer erfolgreichen Transaktion haben alle nötigen Parteien einem Fakt zugestimmt. Dieser Zustand des Vertrages wird im Vault, das ist der eigene dafür zuständige Tresor jeder Partei, abgelegt.

Links & Literatur

  1. https://tools.ietf.org/html/rfc5280  ↩

  2. https://www.blockchain.com/explorer  ↩

  3. http://docs.paralleluniverse.co/quasar  ↩

  4. https://zeroturnaround.com/rebellabs/what-are-fibers-and-why-you-should-care  ↩

  5. https://www.cryptokitties.co  ↩

TAGS