Kapitel 4. Entwicklung von Smart Contracts

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

In diesem Kapitel lernst du die Entwicklung von Fabric-Smart Contracts kennen, indem du einen einfachen Smart Contract und die Fabric-APIs untersuchst, die zur Implementierung von Fabric-Smart Contracts verwendet werden. Sobald du die Grundlagen der Programmierung eines Smart Contracts und der APIs verstehst, können wir mit Kapitel 5 fortfahren, in dem wir den Inhalt dieses Kapitels auf das Aufrufen von Smart Contracts anwenden werden. Um loszulegen, müssen wir zunächst die Hyperledger Fabric Entwicklungstools herunterladen. Sie ermöglichen einen schnellen Einstieg in die Entwicklung von Fabric-Smart Contracts, indem sie eine komplette Fabric-Laufzeit für zwei Organisationen mit Skripten zum Hoch- und Herunterfahren kapseln.

Wir werden die von Hyperledger zur Verfügung gestellten Binärdateien und Beispielprojekte aus dem Fabric-Projekt verwenden. Diese Binärdateien und Beispielprojekte werden uns helfen, ein Fabric-Testnetzwerk zu starten, und die Beispielprojekte bieten mehrere Beispiel-Smart Contracts, anhand derer du lernen kannst, wie du deine eigenen entwickeln kannst. In diesem Kapitel wird ein Beispiel für einen Smart Contract aus dem Beispielprojekt Fabcar untersucht. Die von uns verwendeten Binärdateien haben auf allen unterstützten Betriebssystemen den gleichen Namen.

Dieses Kapitel wird dir helfen, die folgenden praktischen Ziele zu erreichen:

  • Einen Fabric Smart Contract mit der Programmiersprache JavaScript schreiben

  • Einen Fabric Smart Contract installieren und instanziieren

  • Validierung und Bereinigung von Eingaben und Argumenten in einem Smart Contract

  • Einfache oder komplexe Abfragen erstellen und ausführen

  • Mit einer privaten Datensammlung in Fabric arbeiten

Installation der Voraussetzungen und Einrichtung von Hyperledger Fabric

Bevor wir Fabric-Smart Contracts entwickeln können, müssen wir die für den Download von Hyperledger Fabric erforderliche Software herunterladen und installieren. Um Hyperledger Fabric für die Entwicklung von Fabric-Smart Contracts herunterzuladen und einzurichten, führen wir ein Skript aus, das bestimmte Software auf der Plattform benötigt, auf der du entwickelst - Windows, Linux oder Mac. Wir müssen Git, cURL, Node.js, npm, Docker und Docker Compose sowie das Fabric-Installationsskript installieren.

Git

Git wird verwendet, um das fabric-samples Repository von GitHub auf deinen lokalen Rechner zu klonen. Wenn du Git nicht installiert hast, kannst du es von https://git-scm.com/downloads herunterladen . Sobald du es heruntergeladen und installiert hast, überprüfe die Git-Installation mit dem folgenden Befehl:

$ git --version
git version 2.26.2

cURL

Wir verwenden cURL, um die Fabric-Binärdateien von aus dem Internet herunterzuladen. Du kannst cURL von https://curl.haxx.se/download.html herunterladen . Wenn du es heruntergeladen und installiert hast, überprüfe die Installation, indem du den folgenden Befehl ausführst:

$ curl -V
curl 7.54.0 (x86_64-apple-darwin18.0) libcurl/7.54.0 LibreSSL/2.6.5 zlib/1.2.11 nghttp2/1.24.1
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps
telnet tftp
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz HTTP2 UnixSockets HTTPS-
proxy

Node.js und npm

Wir werden JavaScript für verwenden, um unsere Fabric Smart Contracts zu entwickeln. Fabric verwendet Node.js und npm für die Verarbeitung von Smart Contracts. Die unterstützten Versionen von Node.js sind 10.15.3 und höher, und 12.13.1 und höher. Die unterstützten Versionen von npm sind 6 und höher. Node.js beinhaltet npm bei der Installation. Du kannst Node.js von https://nodejs.org/en/download herunterladen . Du kannst die Installation von Node.js und npm überprüfen, indem du die folgenden Befehle ausführst:

$ node -v
v10.15.3

$ npm -v
6.11.2

Docker und Docker Compose

Hyperledger Fabric besteht aus mehreren Komponenten, von denen jede als separater ausführbarer Dienst betrieben wird, so dass Fabric Docker-Images jeder Komponente unterhält. Die Images werden auf der offiziellen Docker Hub Website gehostet. Du brauchst mindestens die Docker-Version 17.06.2-ce. Die neueste Version von Docker findest du unter https://www.docker.com/get-started. Wenn Docker installiert wird, wird auch Docker Compose installiert. Du kannst die Docker-Version überprüfen, indem du den folgenden Befehl ausführst:

$ docker -v
Docker version 19.03.13, build 4484c46d9d

Überprüfe dann deine Docker Compose Version, indem du dies ausführst:

$ docker-compose --version
docker-compose version 1.27.4, build 40524192

Bevor du fortfährst, starte Docker, denn Docker muss laufen, damit die Installation des Fabric-Installationsskripts abgeschlossen werden kann.

Skript zur Installation von Stoffen

Erstelle das Verzeichnis , das du für die Installation der Fabric-Binärdateien und Beispielprojekte verwenden wirst, und wechsle dorthin. Docker muss laufen, denn das Skript benötigt Docker, um die Fabric-Images herunterzuladen.

Das Skript wird Folgendes tun:

  1. Lade die Fabric-Binärdateien herunter

  2. Klone fabric-samples aus dem GitHub Repo

  3. Lade die Hyperledger Fabric Docker-Images herunter

Hier ist der Befehl zum Ausführen des Skripts:

curl -sSL https://bit.ly/2ysbOFE | bash -s

Aber wir werden diesen Befehl noch nicht ausführen. Zuerst speichern wir die Befehlsausgabe, damit wir sie untersuchen können. Vergewissere dich, dass du dich in dem Verzeichnis befindest, das du für die Installation der Fabric-Binärdateien und der Beispielprojekte erstellt hast, und führe dann den folgenden Befehl aus:

curl -sSL https://bit.ly/2ysbOFE > FabricDevInstall.sh

Jetzt kannst du FabricDevInstall.sh in deinem Lieblingseditor öffnen und das Skript untersuchen, um zu sehen, wie es die Fabric-Samples von GitHub klont, die Fabric-Binärdateien herunterlädt und die Docker-Images herunterlädt. Wenn du die Funktionsweise dieses Skripts verstehst, kann dir das später helfen, wenn du deine Fabric-Entwicklungsumgebung anpassen oder sie für deinen Arbeitsablauf optimieren willst.

Nachdem du das Skript untersucht hast, öffne eine Shell und verwandle FabricDevInstall.sh in eine ausführbare Datei, indem du den folgenden Befehl ausführst:

$ chmod +x FabricDevInstall.sh

Führen wir nun FabricDevInstall.sh mit dem folgenden Befehl aus:

FabricDevInstall.sh

Sobald das Skript ausgeführt wurde, sollte alles fertig sein. Das Verzeichnis fabric-samples, die Docker-Images und Fabric-Binärdateien werden im Verzeichnis fabric-samples/bin installiert. In dem Verzeichnis, in dem du den Befehl ausgeführt hast, sollte es jetzt ein Verzeichnis namens fabric-samples geben. Alles, was wir brauchen, befindet sich in fabric-samples. Zuerst werden wir uns den Fabcar-Beispiel-Smart-Contract ansehen, den du im Verzeichnis fabric-samples findest.

Grundlegende Anforderungen an einen Smart Contract

Der Fabcar-Beispiel-Smart-Contract ist ein gültiges, funktionierendes Beispiel für einen grundlegenden Smart Contract. Wir müssen noch viel mehr hinzufügen, bevor er produktionsreif ist, einschließlich Sicherheit, Fehlermanagement, Berichterstattung, Überwachung und Tests. Wir wollen uns an einige wichtige Punkte aus diesem Beispiel-Smart-Contract erinnern. Gehen wir sie durch:

Contract Klasse
Smart Contracts erweitern die Klasse Contract. Dies ist eine einfache Klasse mit wenigen Funktionen. Wir werden uns diese Klasse später in diesem Kapitel ansehen.
Transaktionskontext
Alle Funktionen der Smart-Contract-Transaktion übergeben ein Transaktionskontextobjekt als erstes Argument. Dieses Transaktionskontextobjekt ist eine Context Klasse. Wenn wir uns die Klasse Contract ansehen, werden wir uns auch diese Klasse ansehen.
Konstrukteur
Alle Smart Contracts müssen einen Konstruktor haben. Das Konstruktor-Argument ist optional und steht für den Namen des Vertrags. Wenn es nicht angegeben wird, wird der Klassenname verwendet. Wir empfehlen dir, einen eindeutigen Namen zu übergeben und dir das wie einen Namensraum vorzustellen, wie eine umgekehrte Domainnamenstruktur.
Transaktionsfunktion
Es kann eine Transaktionsfunktion erstellt werden, um einen Smart Contract zu initialisieren und vor Kundenanfragen aufzurufen. Damit kannst du deinen Smart Contract mit allen erforderlichen Ressourcen einrichten und ausführen. Bei diesen Ressourcen kann es sich um Tabellen oder Karten mit Daten handeln, die für Lookups, Übersetzungen, Dekodierung, Validierung, Anreicherung, Sicherheit und so weiter verwendet werden.
Weltstaat
Wir können den Weltstatus auf auf verschiedene Arten abfragen. Die einfache Abfrage ist eine Schlüsselabfrage, und eine Bereichsabfrage liefert eine Menge. Es gibt noch eine weitere Möglichkeit, die Rich Query. Wir sehen uns den Weltstatus in Kapitel 5 an.
putState
Um Daten in das Hauptbuch zu schreiben, verwenden wir die Funktion putState. Sie nimmt als Argumente ein key und value. Das value ist ein Byte-Array, so dass das Ledger beliebige Daten speichern kann. In der Regel werden wir das Äquivalent von Geschäftsobjekten speichern, die in Byte-Arrays umgewandelt werden, bevor sie als value Argument übergeben werden.
ChaincodeStub
Die Klasse ChaincodeStub enthält mehrere Funktionen, die zur Interaktion mit dem Ledger und dem Weltzustand verwendet werden. Alle Smart Contracts erhalten eine Implementierung dieser Klasse als stub Objekt, das in der Context Klassenimplementierung ctx enthalten ist und die alle Transaktionsfunktionen als ihr erstes Argument erhalten.
Lese-/Schreibvorgänge
Eine Aktualisierung in einem Smart Contract wird in drei Schritten ausgeführt: eine Lesetransaktion, eine Aktualisierung der von der Lesetransaktion zurückgegebenen In-Memory-Daten und eine anschließende Schreibtransaktion. Auf diese Weise wird ein neuer Weltzustand für den Schlüssel erzeugt, während die Historie des Schlüssels im unveränderlichen dateibasierten Ledger erhalten bleibt.

Es ist wichtig, sich an diesen Punkt zu erinnern: Du kannst das Hauptbuch nicht aktualisieren (oder in das Hauptbuch schreiben) und das, was du geschrieben hast, in derselben Transaktion zurücklesen. Es spielt keine Rolle, wie viele andere Transaktionsfunktionen du von einer Transaktionsfunktion aus aufrufst. Du musst über den Datenfluss einer Transaktionsanfrage nachdenken. Kunden reichen Transaktionsanfragen bei Peers ein, die die angefragte Transaktion bestätigen (hier wird der Smart Contract ausgeführt); die Bestätigungen mit Lese- und Schreibsätzen werden an die Kunden zurückgeschickt; und die bestätigten Anfragen werden an einen Orderer geschickt, der die Transaktionen ordnet und Blöcke erstellt. Der Auftraggeber sendet die geordneten Anfragen an die Commit-Peers, die die Lese- und Schreibsets validieren, bevor sie die Schreibvorgänge im Ledger festschreiben und den Weltstatus aktualisieren.

In der einfachsten Form ist ein Smart Contract ein Wrapper um ChaincodeStub, denn Smart Contracts müssen die Schnittstelle nutzen, die durch diese Klasse offengelegt wird, um mit dem Ledger und dem Weltzustand zu interagieren. Das ist ein wichtiger Punkt, an den du dich erinnern solltest. Du solltest die Geschäftslogik in einem modularen Design implementieren und deine Contract Unterklasse wie eine Datenquelle behandeln. So kannst du deinen Code im Laufe der Zeit weiterentwickeln und die Logik in funktionale Komponenten aufteilen, die gemeinsam genutzt und wiederverwendet werden können. In Kapitel 5 befassen wir uns mit dem Design im Zusammenhang mit der Paketierung und Bereitstellung und in Kapitel 6 mit dem modularen Design und der Implementierung, um Wartung und Tests zu erleichtern.

Mehrere Peers, die Endorsing Peers, werden deine Smart Contracts ausführen. In der heutigen Architektur werden die Smart Contracts hinter einem Gateway platziert, das eine Middleware im Smart Contract SDK ist. Das Gateway empfängt Smart-Contract-Anfragen, verarbeitet sie und sendet sie an einen oder mehrere Peers. Die Peers instanziieren den Kettencode zur Ausführung.

SDK

Fabric bietet ein in Go, Java und Node.js (JavaScript) implementiertes SDK für die Entwicklung von Smart Contracts. Wir interessieren uns für das Hyperledger Fabric Smart Contract Entwicklungs-SDK für Node.js, das fabric-chaincode-node genannt wird. Unter musst du es für die Entwicklung von Smart Contracts nicht herunterladen. Du kannst fabric-chaincode-node von GitHub herunterladen oder klonen.

Das fabric-chaincode-node SDK hat eine Menge zu bieten. Wir interessieren uns für ein paar Komponenten, die für die Entwicklung von Smart Contracts zentral sind. Bei den übrigen Dateien handelt es sich um Low-Level-Schnittstellen, Support-Artefakte, Tools und mehr, die benötigt werden, um die Schnittstelle Contract mit Node.js zu implementieren. Dieses SDK hilft Entwicklern wie uns, indem es eine High-Level-API bereitstellt, so dass wir schnell lernen und uns auf unsere Smart-Contract-Geschäftslogik und das Design konzentrieren können.

Die erste API, an der wir interessiert sind , ist fabric-contract-api. Sie befindet sich im Unterverzeichnis apis von fabric-chaincode-node. Die andere API, die du siehst, fabric-shim-api, ist die Typdefinition und reine Schnittstelle für die fabric-shim-Bibliothek, die wir uns später in diesem Kapitel ansehen.

Wenn wir unser Smart-Contract-Projekt starten und npm install ausführen, was wir in Kapitel 5 tun werden, lädt npm fabric-contract-api als Modul aus dem npm Public Repository herunter, ebenso wie fabric-shim. Dieser Download erfolgt, weil wir für die Entwicklung von Hyperledger Fabric Smart Contracts zwei explizite Abhängigkeiten haben. Diese werden in diesem Auszug aus der package.json-Datei des Fabcar-Smart Contracts angezeigt:

"dependencies": {
    "fabric-contract-api": "^2.0.0",
    "fabric-shim": "^2.0.0"
},

fabric-contract-api und fabric-shim sind die einzigen Module, die wir für die Entwicklung unserer Smart Contracts benötigen. fabric-contract-api enthält die Dateien contract.js und context.js, die die Klassen Contract und Context implementieren.

Vertragsklasse

Contract ist eine einfache Klasse. Neben dem Konstruktor gibt es Hilfsfunktionen, die du überschreiben kannst, um Logik vor und nach Transaktionen zu implementieren:

constructor(name) {
    this.__isContract = true;
    if (typeof name === 'undefined' || name === null) {
        this.name = this.constructor.name;
    } else {
        this.name = name.trim();
    }
    logger.info('Creating new Contract', name);
}

Die Funktion beforeTransaction wird aufgerufen, bevor die Funktionen der Vertragstransaktion aufgerufen werden. Du kannst diese Methode außer Kraft setzen, um deine eigene Logik zu implementieren:

async beforeTransaction(ctx) {
// default implementation is do nothing
}

Die Funktion afterTransaction wird aufgerufen, nachdem alle Funktionen der Vertragstransaktion aufgerufen wurden. Du kannst diese Methode außer Kraft setzen, um deine eigene Logik zu implementieren:

async afterTransaction(ctx, result) {
    // default implementation is do nothing
}

Die Funktion getName ist ein Getter , der den Vertragsnamen zurückgibt:

getName() {
    return this.name;
}

Und die Funktion createContext erstellt einen benutzerdefinierten Transaktionskontext:

createContext() {
    return new Context();
}

Transaktionskontext

Du kannst einen eigenen Transaktionskontext erstellen, um deine eigenen Objekte zu speichern, auf die deine Funktionen über das ctx Objekt zugreifen können, das alle Transaktionsfunktionen als erstes Argument erhalten. Hier ist ein Beispiel für die Erstellung eines benutzerdefinierten Kontexts:

const AssetList = require('./assetlist.js');

class MyContext extends Context {
    constructor() {
        super();
        this.assetList = new AssetList(this);
    }

}

class AssetContract extends Contract {
    constructor() {
        super('org.my.asset');
    }

    createContext() {
        return new MyContext();
    }
}

Mit dem benutzerdefinierten Kontext MyContext können Transaktionen auf assetList als ctx.assetList zugreifen.

Wie du siehst, ist es ganz einfach, einen einfachen, intelligenten Vertrag zu erstellen. Du importierst Contract aus fabric-contract-api und erweiterst es. Dann erstellst du einen Konstruktor ohne Argumente und exportierst unseren Vertrag. Das war's.

Kontext Klasse

OK, das ist die Klasse Contract, aber was über die Klasse Context? Du hast gerade gelernt, wie man einen benutzerdefinierten Transaktionskontext erstellt, aber was enthält die Klasse Context? Wie du gelernt hast, erhält jede Transaktionsfunktion ein Context Objekt namens ctx als erstes Argument. Das ist der Transaktionskontext. Er enthält zwei wichtige Objekte: stub und clientIdentity. Das stub ist eine Implementierung der Klasse ChaincodeStub und das clientIdentity ist eine Implementierung der Klasse ClientIdentity. Wir besprechen diese Klassen als nächstes.

ChaincodeStub hat alle Funktionen, die wir brauchen, um mit dem Ledger und dem Weltzustand zu interagieren. Es ist unsere API für den Ledger und den Weltzustand. Sie ist im Modul fabric-shim im Verzeichnis lib enthalten und in der Datei stub.js implementiert. Die beiden wichtigsten Funktionen sind getState und putState. Mehrere zusätzliche Funktionen sind verfügbar. Die meisten können in die folgenden Kategorien eingeteilt werden:

  • Bundeslandbezogen

  • Abfrage bezogen

  • Transaktionsbezogen

  • Private Daten im Zusammenhang mit

Diese vier Gruppen stellen die meisten der Funktionen dar. Die zustandsbezogenen Funktionen werden verwendet, um aus dem Hauptbuch zu lesen und in das Hauptbuch zu schreiben. Sie verwenden oder beinhalten die Verwendung eines Schlüssels.

Die abfragebezogenen Funktionen sind zwei Rich Query Funktionen, eine davon mit Paginierung. Rich-Queries sind String-Queries die in der Datenbank enthalten sind. Um Rich Queries zu nutzen, musst du CouchDB als Datenbank verwenden. Das werden wir in Kapitel 5 tun, wo du lernst, wie du smarte Verträge aufrufst. Eine weitere einzigartige Abfragefunktion ist getHistoryForKey, die einen Schlüssel nimmt und die Historie für diesen Schlüssel zurückgibt. Damit kannst du Änderungen überprüfen und Transaktionen finden, die fehlgeschlagen sind.

Hyperledger hat fünf transaktionsbezogene Funktionen:

  • getTxID(): string;

  • getChannelID(): string;

  • getCreator(): SerializedIdentity;

  • getMspID(): string;

  • getTransient(): Map<string, Uint8Array>;

Verwende getTxID, um die Transaktions-ID abzurufen, getChannelID für die ID des Kanals, getCreator für den Kunden, getMspID für die Organisation, zu der der Kunde gehört, und getTransient für private Daten. In den nächsten beiden Kapiteln werden wir jeden dieser Schritte ausführen.

Hyperledger hat neun private datenbezogene Funktionen:

  • getPrivateData(collection: string, key: string): Promise​<Uint8Array>;

  • getPrivateDataHash(collection: string, key: string): Promise​<Uint8Array>;

  • putPrivateData(collection: string, key: string, value: Uint8Array): Promise<void>;

  • deletePrivateData(collection: string, key: string): Promise<void>;

  • setPrivateDataValidationParameter(collection: string, key: string, ep: Uint8Array): Promise<void>;

  • getPrivateDataValidationParameter(collection: string, key: string): Promise<Uint8Array>;

  • getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getPrivateDataByPartialCompositeKey(collection: string, objectType: string, attributes: string[]): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getPrivateDataQueryResult(collection: string, query: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

Diese neun Funktionen für private Daten ermöglichen das Lesen und Schreiben in eine private Datensammlung, das Abrufen eines privaten Datenhashs, das Löschen von privaten Daten, das Festlegen und Abrufen einer Bestätigungsrichtlinie für die Validierung privater Daten und das Abrufen privater Daten über einen Bereich, einen partiellen zusammengesetzten Schlüssel oder eine umfangreiche Abfrage. Wir werden einige dieser Funktionen in den Kapiteln 5 und 6 ausführen, wenn wir private Daten in unseren Smart Contract-Transaktionen verwenden.

Nachdem du nun eine gute Vorstellung davon hast, welche Funktionen über das Objekt stub zur Verfügung stehen, das wir von dem Objekt ctx erhalten, schauen wir uns ClientIdentity an, das andere Objekt im Transaktionskontext.

Das Objekt clientIdentity enthielt im Transaktionskontext, da ctx.clientIdentity die Implementierung der Klasse ClientIdentity ist, die eine kleine Klasse mit nur fünf Funktionen ist:

  • assertAttributeValue(attrName: string, attrValue: string): boolean;

  • getAttributeValue(attrName: string): string | null;

  • getID(): string;

  • getIDBytes(): Uint8Array;

  • getMSPID(): string;

Die Funktionen assertAttributeValue und getAttributeValue arbeiten mit dem Client-Zertifikat. Mit diesen Funktionen kann eine granulare Sicherheit implementiert werden, indem die Attributwerte des Zertifikats verwendet werden. Die Funktionen getID und getIDBytes rufen die Identität des Kunden ab, und getMSPID wird verwendet, um die Organisation zu ermitteln, der der Kunde angehört. Mit diesen Funktionen kannst du eine Vielzahl von Authentifizierungs- und Autorisierungsmustern umsetzen.

Transaktionsfunktionen

Transaktionsfunktionen sind die Smart-Contract-Funktionen, die Kunden aufrufen. Das sind die Geschäftsfunktionen, die du in deinen Smart Contracts entwirfst und implementierst. Hier ist ein Beispiel für drei Transaktionsfunktionen aus dem Fabcar Smart Contract. Wir werden sie im Abschnitt "Definieren eines Smart Contracts" definieren :

async queryCar(ctx, carNumber) 
async createCar(ctx, carNumber, make, model, color, owner)
async queryAllCars(ctx)

Alle Transaktionsfunktionen erhalten als erstes Argument den Transaktionskontext, das ctx Objekt. Die Transaktionsfunktionen verwenden dieses Objekt, um auf die Objekte stub und clientIdentity zu verweisen - zum Beispiel ctx.stub und ctx.clientIdentity. stub ist eine Instanz von ChaincodeStub. clientIdentity ist eine Implementierung von C⁠l⁠i⁠e⁠n⁠t​I⁠d⁠e⁠n⁠t⁠i⁠t⁠y und stellt Funktionen zur Verfügung, um die Transaktions-ID, die Client-ID, alle Client-Attribute und die Organisations-ID zu erhalten. Diese Funktionen können für die anwendungs- und transaktionsspezifische Authentifizierung und Autorisierung verwendet werden.

Es ist üblich, dass die meisten Transaktionsfunktionen einen Aufruf des Ledgers oder des Weltzustands enthalten. Die stub stellt die Funktionen zum Lesen aus dem Weltzustand und zum Schreiben in den Ledger bereit, der den Weltzustand aktualisiert.

Die Art und Weise, wie du deine Transaktionsfunktionen gestaltest, liegt ganz in deiner Hand. Du kannst sie gruppieren, mischen, Bibliotheken erstellen und vieles mehr. Erinnere dich daran, dass bei Transaktionen, die du schreibst, alle vorgesehenen Endorser deine Smart Contracts ausführen müssen.

Befürwortungsrichtlinien entscheiden darüber, ob ein Vorgang bestätigt wird. Eine Bestätigungsrichtlinie kann zum Beispiel festlegen, dass drei von vier Peers die Transaktion bestätigen müssen. Wenn aus irgendeinem Grund weniger als drei Peers die Transaktion bestätigen können, wird die Transaktion nicht bestätigt, d.h. die Daten werden nicht im Ledger verfügbar sein.

Du solltest dich daran erinnern, dass ein Schreibvorgang, auch wenn er fehlschlägt, als ungültig gekennzeichnet und in das Hauptbuch geschrieben wird. Eine ungültige Transaktion ist nicht Teil des Weltzustands.

Als bewährte Methode erfordern Fabric Smart Contracts deterministischen Code. Viele Peers müssen den Code ausführen und alle müssen zum gleichen Ergebnis kommen. Daher müssen die Eingaben immer das gleiche Ergebnis liefern. Es darf keine Rolle spielen, welcher Peer den Code ausführt. Bei gleichen Eingaben sollte der Peer auch die gleichen Ergebnisse liefern. Das sollte unabhängig davon sein, wie oft der Code ausgeführt wird.

Der Code sollte einen Anfang und ein Ende haben. Er sollte nie von dynamischen Daten oder langen zufälligen Ausführungen abhängen. Der Code sollte schnell und effizient sein, mit klaren logischen Abläufen, die nicht zirkulär sind. Zum Beispiel:

async CreateAsset(ctx, id, amount, owner) {
        const asset = {
            ID: id,
            Amount: amount,
            Owner: owner
        };
        return ctx.stub.putState(id,Buffer.from(
           JSON.stringify(asset)));
}

Wenn du einen Vermögenswert anlegst, erwarten wir die ID des Vermögenswerts, den Betrag und den Eigentümer als Eingaben.

Argumente validieren und bereinigen

Transaktionen müssen validieren und ihre Argumente bereinigen. Das gilt nicht nur für Smart Contracts. Es ist eine sinnvolle Standardpraxis, wenn du die Integrität und Verfügbarkeit deines Smart Contracts sicherstellen willst.

Nutze bekannte Techniken, um deine Argumente zu überprüfen und Daten zu vermeiden, die deinem Smart Contract schaden könnten. Außerdem musst du deine Argumente bereinigen und die Qualität der Daten, die du erwartest, sicherstellen. Du willst die unnötige Verarbeitung von Daten begrenzen, die später in deiner Logik einen Fehler oder einen nicht abgedeckten Kantenfall verursachen. Hier ist ein Beispiel, das überprüft, ob die Funktion Process ist; wenn nicht, wird eine Ausnahme geworfen:

func (c *Asset) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    function, args := stub.GetFunctionAndParameters()
    if function == "Process" {
        return c.Process(stub, args)
    } 
    return shim.Error("Invalid function name")
}

Einfache Zustandsinteraktion (get, put, delete)

Die Smart Contracts der Fabric, die den Kern von bilden, sind Zustandsmaschinen, die sich im Laufe der Zeit weiterentwickeln und eine unveränderliche Historie aller vorherigen Zustände speichern. Die drei wichtigsten stub Funktionen, die du verwenden wirst, sind die folgenden:

getState(key: string): Promise<Uint8Array>;
putState(key: string, value: Uint8Array): Promise<void>;
deleteState(key: string): Promise<void>;

Die Funktionen von stub bieten deinen Smart Contracts die Möglichkeit, aus dem World State zu lesen, in den Ledger zu schreiben und aus dem World State zu löschen.

Das Hauptbuch und der Weltzustand sind Key-Value-Datenspeicher. Das hält die Sache einfach und übersichtlich, ermöglicht es aber, umfangreiche Daten im Ledger zu speichern, abzufragen und über den World State, ein Dokument oder eine NoSQL-Datenbank einzusehen. Derzeit werden LevelDB und CouchDB unterstützt. LevelDB ist ein einfacher Key-Value-Datenspeicher , während CouchDB eine umfangreiche und robuste NoSQL-Dokumentendatenbank ist. Das bedeutet, dass du einfache bis komplexe JSON-Datenstrukturen lesen und in das Hauptbuch schreiben und die World State Database nach umfangreichen Daten abfragen kannst. Für Abfragen stehen mehrere Funktionen zur Verfügung.

Erstellen und Ausführen von Abfragen

Smart Contracts müssen oft nachschlagen oder Daten aus dem Weltzustand abfragen, während sie eine Transaktion verarbeiten. Erinnerst du dich an die Aktualisierung unseres Fabcar-Muster-Smart Contracts? Um eine Aktualisierung durchzuführen, muss ein Smart Contract normalerweise das zu aktualisierende Objekt finden und laden. Dann aktualisiert er die In-Memory-Daten und schreibt die aktualisierten Daten in den Ledger - denk an putState zum Schreiben und getState zum Lesen.

Im Gegensatz zu einer relationalen Datenbank, in der wir ein Feld aktualisieren können, ohne vorher die Zeile auszuwählen, müssen wir bei Fabric Smart Contracts den Wert eines Schlüssels laden und den Wert aktualisieren. Das kann ein einzelnes Feld in einer sehr großen und komplexen JSON-Datenstruktur sein, oder ein einfaches Objekt mit einem Feld, dem Feld, das wir aktualisieren. Warum ist das so? Weil alle Daten an einen eindeutigen Schlüssel gebunden sind. Wenn der zugehörige Wert eines Schlüssels ein Objekt mit vier Feldern ist, betrachten wir jedes Feld als einen Wert - aber für das Hauptbuch ist das alles nur ein einziges Wertobjekt, das durch einen einzigen eindeutigen Schlüssel identifiziert wird.

Hier sind die verfügbaren stub Funktionen, die du zum Suchen von Daten verwenden kannst:

  • getState(key: string): Promise<Uint8Array>;

  • getStateByRange(startKey: string, endKey: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getStateByRangeWithPagination(startKey: string, endKey: string, pageSize: number, bookmark?: string): Promise<StateQueryResponse<Iterators.StateQueryIterator>> & AsyncIterable<Iterators.KV>;

  • getQueryResult(query: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV>;

  • getQueryResultWithPagination(query: string, pageSize: number, bookmark?: string): Promise<StateQueryResponse<Iterators.StateQueryIterator>> & AsyncIterable<Iterators.KV>;

  • getHistoryForKey(key: string): Promise<Iterators.HistoryQueryIterator> & AsyncIterable<Iterators.KeyModification>;

Wir besprechen diese in Kapitel 5, wenn wir sie in einem Smart Contract verwenden und sie von einem Client aus aufrufen.

Einen Smart Contract definieren

Beginnen wir mit dem einfachen Fabcar Smart Contract. Im Verzeichnis fabric-samples suchst du das Verzeichnis chaincode. Suche im chaincode-Verzeichnis das fabcar-Verzeichnis. Suche im fabcar-Verzeichnis das javascript-Verzeichnis. Wechsle in deiner Shell in dieses Verzeichnis und führe den folgenden Befehl aus:

$ npm install

Dadurch wird das Verzeichnis node_modules erstellt und die abhängigen Module, die in package.json definiert sind, installiert. Wir haben das getan, weil wir auf die Module fabric-contract-api und fabric-shim angewiesen sind. Das sind die beiden Module, die wir bei der Entwicklung von Fabric-Smart Contracts in JavaScript verwenden. Wir werden uns diese Module ansehen, nachdem wir den Fabcar-Smart-Contract untersucht haben.

Schauen wir uns nun den Fabcar-Smart-Contract an. Dieser einfache Smart Contract ist ein großartiges Beispiel, um die Entwicklung von Fabric-Smart Contracts zu lernen, denn er enthält die notwendigen Details, um eine Grundlage zu schaffen, auf der wir zu fortgeschritteneren Smart Contracts übergehen können. Er befindet sich im lib-Verzeichnis im aktuellen Verzeichnis, das das fabric-samples/chaincode/fabcar/javascript-Verzeichnis sein sollte. Öffne fabcar.js in deinem Lieblingseditor; Beispiel 4-1 zeigt den Quellcode.

Beispiel 4-1. fabcar.js
/*
 * Copyright IBM Corp. All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
'use strict';
const { Contract } = require('fabric-contract-api'); 1
class FabCar extends Contract { 2
    async initLedger(ctx) { 3
        console.info('============= START : Initialize Ledger ===========');

        const cars = [ 4
            {
                color: 'blue',
                make: 'Toyota',
                model: 'Prius',
                owner: 'Tomoko',
            },
...
        ];
        for (let i = 0; i < cars.length; i++) { 5
            cars[i].docType = 'car';
            await ctx.stub.putState('CAR' + i, 
            Buffer.from(JSON.stringify(cars[i]))); 6
        }
    }
    async queryCar(ctx, carNumber) { 7
        const carAsBytes = await ctx.stub.getState(
        carNumber); // get the car from chaincode state 8
        if (!carAsBytes || carAsBytes.length === 0) {
            throw new Error(`${carNumber} does not exist`);
        return carAsBytes.toString();
    }
    async createCar(ctx, carNumber, make, model, color, owner) { 9
        console.info('============= START : Create Car ===========');
        const car = {color, docType: 'car',make, model, owner}

        await ctx.stub.putState(carNumber, Buffer.from(
        JSON.stringify(car))); 10
    }
    async queryAllCars(ctx) { 11
        const startKey = '';
        const endKey = '';
        const allResults = [];
        for await (const {key, value} of ctx.stub.getStateByRange(
            startKey, endKey)) { 12
            const strValue = Buffer.from(value).toString('utf8');

            let record;
            try {
                record = JSON.parse(strValue);
            } catch (err) {
                record = strValue;
            }
            allResults.push({ Key: key, Record: record });
        }
        return JSON.stringify(allResults);
    }
    async changeCarOwner(ctx, carNumber, newOwner) { 13
        

const carAsBytes = await ctx.stub.getState(carNumber); 14
if (!carAsBytes || carAsBytes.length === 0) {
            throw new Error(`${carNumber} does not exist`);
        }
        const car = JSON.parse(carAsBytes.toString());
        car.owner = newOwner;
        await ctx.stub.putState(carNumber, Buffer.from(
JSON.stringify(car))); 15
    }
}
module.exports = FabCar; 16
1

Wir beginnen damit, das Modul fabric-contract-api zu importieren.

2

Alle Fabric Smart Contracts erweitern die Klasse Contract. Wir erhalten die Klasse Contract aus dem fabric-contract-api Modul, das wir in Zeile 1 importiert haben.

3

Smart Contracts können Transaktionen verwenden, um sie zu initialisieren, bevor sie Anfragen von Client-Anwendungen bearbeiten. Diese Zeile ist der Anfang der Funktion, die den Smart Contract initialisiert. Alle Smart-Contract-Funktionen erhalten ein Transaktionskontextobjekt als Argument, das per Konvention ctx genannt wird.

4

In diesem Beispiel erstellt die Funktion initLedger ein Array von Objekten namens cars. Jedes Array-Objekt enthält Schlüssel-Werte-Paare. Du kannst dir das Array mit den Objekten als Datensätze von Vermögenswerten und die Objekt-Schlüsselwertpaare als Felder vorstellen. Mit dieser Funktion wird ein Array von car Objekten vorgeladen, um die Transaktionsfunktionen des Smart Contracts auszuführen.

5

Als Nächstes durchläuft die Funktion initLedger das Array von car Vermögensobjekten und fügt jedem Objekt ein Feld namens docType hinzu und weist jedem Objekt den Stringwert car zu.

6

Diese Zeile ist die erste Verwendung des ctx Objekts (KlasseContext ), das als erstes Funktionsargument an alle Transaktionsfunktionen der Klasse Contract übergeben wird. Das ctx Objekt enthält das stub Objekt, , das eine ChaincodeStub Klasse ist. Die Klasse ChaincodeStub implementiert eine API für den Zugriff auf das Hauptbuch. Diese Zeile ruft die Funktion putState auf, die den Schlüssel und den Wert in das Ledger und den Weltzustand schreibt.

Hinweis

Hyperledger Fabric implementiert den Blockchain-Ledger in zwei Komponenten: eine dateibasierte Komponente und eine Datenbankkomponente. Die dateibasierte Komponente ist die unveränderliche Datenstruktur auf niedriger Ebene , die den Ledger implementiert, und die Datenbankkomponente zeigt den aktuellen Zustand des dateibasierten Ledgers an. Die Datenbankkomponente wird Weltzustand genannt, weil sie den aktuellen Zustand des Ledgers darstellt. Die dateibasierte Komponente verwaltet das ewige unveränderliche Hauptbuch. Die fabric-contact-api greift auf den Weltstatus zu. Untergeordnete APIs greifen auf das dateibasierte Hauptbuch zu.

7

Die erste Transaktionsfunktion kommt als nächstes. Wie bereits erwähnt, ist das erste Argument aller Smart-Contract-Transaktionsfunktionen das ctx Objekt, das den Transaktionskontext darstellt und eine Context Klasse ist. Alle anderen Argumente sind optional.

8

Die Funktion queryCars ist eine Lesetransaktion. Über das Objekt ctx ruft sie die Funktion getState von stubauf, die aus dem Weltzustand - der Datenbank - liest. Die Funktion stub ist eine Implementierung der Klasse ChaincodeStub, die wir später behandeln werden. Bei dieser Funktion ist das Argument, das carNumber genannt wird, das key, das an die Funktion getState übergeben wird, die die Weltzustandsdatenbank nach key durchsucht und den dazugehörigen Wert zurückgibt. Der Rest der Funktion prüft, ob Daten zurückgegeben wurden. Wenn ja, wandelt sie das zurückgegebene Byte-Array in eine Zeichenkette um und gibt die Zeichenkette zurück, die den Wert des im Weltzustand gespeicherten Schlüssels darstellt. Erinnere dich daran, dass der Weltzustand den aktuellen Zustand des unveränderlichen, dateibasierten Ledgers für jedes im Ledger gespeicherte Schlüssel-Wert-Paar darstellt. Während die Datenbank veränderbar sein kann, ist das dateibasierte Hauptbuch nicht veränderbar. Selbst wenn also ein Schlüssel-Wert-Paar aus der Datenbank oder dem Weltzustand gelöscht wird, befindet sich das Schlüssel-Wert-Paar immer noch im dateibasierten Ledger, wo die gesamte Historie im immerwährenden, unveränderlichen Ledger gespeichert ist.

9

Dann haben wir die zweite Transaktionsfunktion. Sie übergibt die Werte, die zur Erstellung eines neuen car Datensatzobjekts erforderlich sind, das wir dem Ledger hinzufügen werden.

10

Mit dem aus den Funktionsargumenten erstellten car Datensatz-Objekt rufen wir die ChaincodeStub API-Funktion auf, die von stub implementiert wird und putState heißt. Diese schreibt die key und value in das Hauptbuch und aktualisiert den aktuellen Weltzustand. Die ersten beiden Argumente, die an die Funktion putState übergeben werden, sind ein Schlüssel bzw. ein Wert. Wir müssen das value, das car Datensatzobjekt, in ein Byte-Array umwandeln, das die ChaincodeStub APIs benötigen.

11

Die nächste Transaktionsfunktion namens queryAllCars ist eine Lesetransaktion und demonstriert eine Bereichsabfrage. Eine Bereichsabfrage wird, wie alle Abfragen, von dem Peer ausgeführt, der die Anfrage erhält. Eine Bereichsabfrage benötigt zwei Argumente: den Anfangsschlüssel und den Endschlüssel. Diese beiden Schlüssel stellen den Anfang und das Ende des Bereichs dar. Alle Schlüssel, die in den Bereich fallen, werden zusammen mit den zugehörigen Werten zurückgegeben. Du kannst für beide Schlüssel eine leere Zeichenkette angeben, um alle Schlüssel und Werte abzufragen.

12

Es wird eine for Schleife ausgeführt, die alle Schlüssel und zugehörigen Werte speichert, die von der ChaincodeStub API-Funktion getStateByRange zurückgegeben werden.

13

Die letzte Transaktionsfunktion, changeCarOwner, kombiniert sowohl Lese- als auch Schreibaufgaben, um den Zustand der Welt zu ändern. Die Geschäftslogik ist hier eine Übertragung des Eigentums. Zusätzlich zum Argument ctx werden zwei Argumente übergeben: ein key namens carNumber und ein value Objekt namens newOwner.

14

Als Nächstes müssen wir das Datensatzobjekt aus dem Weltzustand abrufen, das den aktuellen Schlüssel und Wert für diesen Datensatz darstellt. Die key ist carNumber. Wir verwenden es, um die ChaincodeStub API getState auszuführen. Nachdem wir das aktuelle car Datensatzobjekt für carNumber abgerufen haben, ändern wir das Feld owner in newOwner.

15

Nachdem wir die Ledger-Daten, die den Weltzustand repräsentieren, abgerufen und die abgerufenen Daten aktualisiert haben, aktualisieren wir das Ledger für dieses car record object, indem wir die ChaincodeStub API putState ausführen. Dadurch wird ein neues key und value in das Ledger geschrieben, das den Weltzustand darstellt. Wenn das Datensatzobjekt car jetzt abgerufen wird, zeigt das Ledger den neuen Eigentümer erst an, wenn das Datensatzobjekt in das Ledger übertragen wird. Es ist wichtig zu verstehen, dass das Hauptbuch angehängt wird, sobald die Übergabe erfolgt ist, und dass der Datenbankstatus geändert wird (der Weltstatus wird aktualisiert). Du kannst dir das Hauptbuch als einen ständig wachsenden Stapel von Objekten vorstellen, von denen jedes einen eindeutigen Bezeichner, den Schlüssel, hat. Es kann viele Schlüssel mit demselben Wert geben, aber nur einer repräsentiert den aktuellen oder den Weltzustand. Auf diese Weise implementiert die Datenbank die Ansicht des Weltzustands, während das dateibasierte Hauptbuch die unveränderliche Historie aller Schlüssel in Zeitstempelreihenfolge implementiert.

Hinweis

Das dateibasierte Hauptbuch speichert alle Schreibvorgänge. Sowohl erfolgreiche als auch fehlgeschlagene Schreibvorgänge sind Teil des dateibasierten unveränderlichen Ledgers. Flags kontrollieren die Gültigkeit von Transaktionen, die im unveränderlichen dateibasierten Ledger gespeichert sind. Dies erleichtert die Prüfung aller eingereichten Schreibvorgänge.

16

Diese Zeile ist spezifisch für Node.js. Wir besprechen den Export von Smart-Contract-Modulen in Kapitel 5, wenn wir die Ausführung von Smart Contracts, einschließlich Projektstruktur, Paketierung und Bereitstellung, behandeln.

Damit ist dieser einfache Smart Contract fertig. Wir werden ihn nun zusammenfassend besprechen, um die grundlegenden Anforderungen für die Entwicklung eines Smart Contracts aufzuzeigen. Ausgehend von diesem grundlegenden Smart Contract können komplexe Smart-Contract-Anwendungen entworfen und entwickelt werden.

Assets mit Schlüssel-Werte-Paaren definieren

Wenn du Smart Contracts entwirfst ( ), musst du vielleicht in Form von Vermögenswerten denken. Vermögenswerte sind allgemeiner Natur und können viele Dinge darstellen, darunter materielle und immaterielle Objekte. Das können Maschinenteile, Hundefutter, Währungen oder grüne Derivate sein. Wir verwenden Name-Wert-Paare oder Schlüssel-Wert-Paare, je nachdem, wie du darüber denken willst, um unsere Datenstrukturen zu erstellen. Hier ist ein Beispiel, das wir besprechen können:

const assets = [
    {
        color: 'blue',
        make: 'Honda',
        model: 'Accord',
        owner: 'Jones',
    },  
    {
        color: 'red',
        make: 'Ford',
        model: 'Mustang',
        owner: 'Smith',
    },
];
for (let i = 0; i < assets.length; i++) { 
    assets[i].docType = 'asset';
    await ctx.stub.putState('ASSET' + i,
      Buffer.from(JSON.stringify(assets[i])));
}

Das haben wir schon im Fabcar-Beispiel gesehen. Es ist ein gutes Beispiel für eine grundlegende Verarbeitung, die du je nach Anwendungsfall ausbauen kannst. In diesem Beispiel wird ein Array von Objekten erstellt, die Assets darstellen. Die Schlüssel-Wert-Paare definieren die Attribute oder Merkmale jedes Vermögenswertes. Das Array fungiert als eine einfache Datenbank für Vermögenswerte. Jeder Vermögenswert wird in das Hauptbuch geschrieben, indem ctx.stub.putState aufgerufen wird, das einen Schlüssel und einen Wert benötigt. Der Wert muss ein Byte-Array sein, also wandeln wir das JSON-Objekt in einen String um und konvertieren dann den String in das Byte-Array. Du wirst dies oft tun und möchtest es vielleicht vereinfachen und ein Dienstprogramm oder eine Bibliothek erstellen. Dieser spezielle Code wurde zur Initialisierung des Smart Contracts verwendet.

Wir können Vermögenswerte auch mithilfe einer Smart-Contract-Transaktion definieren. Die nachfolgend gezeigte Transaktionsfunktion createAsset veranschaulicht, wie einfach es ist, einen Vermögenswert zu erstellen und ihn in das Hauptbuch zu schreiben. Diese Funktion wird von einem Client aufgerufen. Der Client kann ein Benutzer oder ein Prozess sein. Wichtig ist, dass du dich daran erinnerst, dass der Vermögenswert erst dann verfügbar ist, wenn die Transaktion in das Hauptbuch übertragen wurde. Du kannst also nicht eine Reihe von Vermögenswerten in den Ledger schreiben und später in deinem Smart Contract erwarten, dass du ihre Daten lesen und verwenden kannst, um die Verarbeitung fortzusetzen. Über diesen unverbundenen Zustand solltest du nachdenken, wenn du mit dem Design und dem Brainstorming beginnst. Hier ist die createAsset Transaktionsfunktion:

async createAsset(ctx, assetNumber, make, model, color, owner) {
        const asset = {
        color,
        docType: 'asset',
        make,
        model,
        owner,
        };
    await  ctx.stub.putState(assetNumber, Buffer.from(JSON.stringify(asset)));
}

Private Daten sammeln

Die private Datensammlung(PDC) ist eine Partition der Ledger-Daten, die einer Organisation gehört, die private Daten speichert und die Daten vor anderen Organisationen in diesem Kanal geheim hält. Dazu gehören private Daten und der Hash-Wert der privaten Daten. Kapitel 9 enthält weitere Einzelheiten. Die Notwendigkeit, bestimmte Daten privat zu halten, ist wichtig für die Entwicklung von Smart Contracts. Viele Smart Contracts müssen den Anforderungen an Datenschutz und Sicherheit entsprechen. Fabric unterstützt private Daten für Transaktionsfunktionen und Smart Contracts.

Die privaten Daten können geteilt oder isoliert und sicher aufbewahrt werden. Wir können die privaten Daten ablaufen lassen, nachdem eine bestimmte Anzahl von Blöcken erstellt wurde oder bei Bedarf. Die Daten in den PDCs bleiben von den öffentlichen Daten getrennt, und die PDCs sind lokal und geschützt. Der Weltzustand kann in Verbindung mit PDCs durch die Verwendung von Hashes und öffentlichen Schlüsseln genutzt werden.

In Tabelle 4-1 sind verschiedene Funktionen aufgeführt, die unter stub zur Verfügung stehen.

Tabelle 4-1. Befehle für die Arbeit mit privaten Daten
API Hinweis
getPrivateData(collection: string, key: string): Promise<Uint8Array> Gibt die Endorsement-Policy aus dem Sammlungsnamen und dem angegebenen Schlüssel zurück.
putPrivateData(collection: string, key: string, value: Uint8Array): Promise<void> Legt den Sammlungsnamen sowie den angegebenen Schlüssel und Wert in die private writeSet der Transaktion.
deletePrivateData(collection: string, key: string): Promise<void> Löscht die Endorsement-Policy durch Angabe des Sammlungsnamens und des Schlüssels der privaten Datenvariablen.
setPrivateDataValidationParameter(collection: string, key: string, ep: Uint8Array): Promise<void> Legt die Endorsement-Richtlinie fest, indem er den Namen der Sammlung und den Schlüssel der privaten Datenvariablen angibt.
getPrivateDataValidationParameter(collection: string, key: string): Promise<Uint8Array> Liefert die Endorsement-Policy durch Angabe des Sammlungsnamens und des Schlüssels der privaten Datenvariablen zurück.
getPrivateDataByRange(collection: string, startKey: string, endKey: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV> Gibt die Endorsement-Policy aus dem Sammlungsnamen und dem Schlüssel der privaten Datenvariablen zurück.
getPrivateDataByPartialCompositeKey(collection: string, objectType: string, attributes: string[]): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV> Fragt die Endorsement-Policy in einem bestimmten Sammlungsnamen und einem bestimmten zusammengesetzten Teilschlüssel ab.
getPrivateDataQueryResult(collection: string, query: string): Promise<Iterators.StateQueryIterator> & AsyncIterable<Iterators.KV> Führt eine Rich Query gegen eine bestimmte private Sammlung durch. Sie wird für State-Datenbanken unterstützt, die eine Rich Query ausführen können (z. B. CouchDB).

Die Verwendung privater Daten kann knifflig sein, und es gibt Muster für die Verwendung unter verschiedenen Umständen. Wir werden die meisten dieser Funktionen in Kapitel 5 behandeln, wenn wir Funktionen für private Daten implementieren, um sie aufzurufen, und erneut in Kapitel 6, wenn wir sie bei der Wartung und beim Testen verwenden.

Zugriffskontrolle auf Basis von Attributen festlegen

Letztendlich brauchst du eine Möglichkeit, um eine granulare Authentifizierung und Autorisierung zu implementieren. Kunden haben eine Identität, die den Zugang kontrolliert. Die Identitäten müssen zu autorisierten Organisationen gehören, und Organisationen gehören zu Kanälen, die Chaincode hosten. Ein Zertifikat stellt die Identität des Kunden dar. Es unterstützt Attribute, die zur Umsetzung von Transaktionen und Authentifizierungs- und Autorisierungsrichtlinien auf Smart-Contract-Ebene verwendet werden können.

Du greifst auf diese Informationen über das Objekt clientIdentity zu, das im Transaktionskontext enthalten ist. Dieses Objekt hat zwei Funktionen, die sich auf die Attributwerte beziehen:

assertAttributeValue(attrName: string, attrValue: string): boolean;
getAttributeValue(attrName: string): string | null;

Verwende assertAttributeValue, um das Vorhandensein eines Attributs zu prüfen, und verwende getAttributeValue, um ein bestimmtes Attribut abzurufen. Es ist eine gute Praxis, das Attribut zu bestätigen, bevor du es abrufst. In den Kapiteln 5 und 6 werden wir Attribute für Sicherheits- und andere Zwecke verwenden.

Initialisiere den Ledger-Status

Die Initialisierung des Ledgers ist oft eine notwendige Aufgabe. Das folgende Beispiel aus dem Fabcar-Code, den wir uns bereits angesehen haben, zeigt, wie du den Zustand deines Smart Contracts initialisierst. Du initialisierst ihn direkt nach dem Commit. Danach kannst du mit dem Einreichen von Transaktionen und dem Abfragen von Ledger-Daten beginnen, indem du Smart-Contract-Methoden aufrufst.

Du erstellst eine Funktion, die du aufrufst, um deine Initialisierung durchzuführen; hier heißt sie initLedger. In der Funktion initLedger kannst du das tun, was du für die Initialisierung brauchst. In diesem Beispiel erstellen wir ein Array mit Geschäftsobjekten, durchlaufen das Array cars und fügen dann jedem car Objekt im Array cars ein zusätzliches Attribut docType hinzu. Hier ist die initLedger Logik:

async initLedger(ctx) {
    const cars = [
         {
              color: 'blue',
              make: 'Toyota',
              model: 'Prius',
              owner: 'Tomoko',
         },
         .
         .
         .
         {
              color: 'brown',
              make: 'Holden',
              model: 'Barina',
              owner: 'Shotaro',
              },
    ];
    for (let i = 0; i < cars.length; i++) {
              cars[i].docType = 'car';
              await ctx.stub.putState('CAR' + i,
              Buffer.from(JSON.stringify(cars[i])));
              console.info('Added <--> ', cars[i]);
    }
}

Die Funktion initLedger schreibt die Array-Objekte mit Hilfe der Funktion putState in den Ledger. Um die Funktion initLedger auszuführen, müssen wir den Smart Contract aufrufen. Wir können den Befehl peer CLI invoke verwenden. Schauen wir uns an, wie wir initLedger über den Befehl invoke aufrufen können.

Kettencode aufrufen init

Um eine Transaktionsfunktion auf unserem Smart Contract auszuführen, können wir den Befehl invoke verwenden, der von der Peer Binary bereitgestellt wird. Dieses Binary bietet viele Befehle, von denen du einige in den Kapiteln 5 und 6 kennenlernen wirst. Hier verwenden wir ihn, um unsere initLedger Funktion aufzurufen:

Hinweis

Wir haben den folgenden Befehl zur besseren Lesbarkeit auf mehrere Zeilen gedruckt. Wenn du den Befehl eingibst, muss er in einer Zeile stehen, sonst schlägt die Ausführung fehl.

peer chaincode invoke 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/tls/
ca.example.com-cert.pem 
-C mychannel 
-n fabcar 
--peerAddresses localhost:7051 
--tlsRootCertFiles /OReilly/fabric-samples/test-network/organizations
/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt 
--isInit 
-c '{"function":"initLedger","Args":[]}'

[chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

Der Befehl invoke führt das Befehlsobjekt aus, das dem Befehlsargument-Flag -c folgt. Das Befehlsobjekt gibt die auszuführende Funktion und alle Funktionsargumente an. Hier führen wir die Funktion initLedger aus, und es gibt keine Funktionsargumente.

Wir können die Ergebnisse der Funktion initLedger testen. Wir erwarten, dass sie den Inhalt des Arrays zurückgibt, das in das Hauptbuch geschrieben wurde. Wir werden den Befehl query verwenden; schauen wir uns an, wie wir Ledger-Daten abfragen können.

Kettencode-Abfrage

Mit dem Befehl query des Peers, können wir eine unserer Smart Contract Abfragefunktionen ausführen. In diesem Fall setzen wir das -c Befehlsflag, um queryAllCars auszuführen:

peer chaincode query 
-C mychannel 
-n fabcar 
-c '{"Args":["queryAllCars"]}'

Hier ist die Rückgabe:

[{"Key":"CAR0","Record":{"color":"blue","docType":"car",
"make":"Toyota","model":"Prius","owner":"Tomoko"}},
.
.
.
{"Key":"CAR9","Record":{"color":"brown","docType":"car","make":"Holden",
"model":"Barina","owner":"Shotaro"}}]

Das Ergebnis zeigt, dass die Funktion initLedger ausgeführt wurde und unser Smart Contract initialisiert ist.

Installieren und Instanziieren eines Smart Contracts

In Vorbereitung auf Kapitel 5 gehen wir auf durch, was wir tun müssen, wenn wir unseren Smart Contract fertig kodiert haben. In diesem Abschnitt werden mehrere Schritte erläutert, die wir durchführen müssen, um unseren Smart Contract entweder über die Kommandozeile oder über einen Smart Contract Client aufrufen zu können. Diese Schritte sind wie folgt:

  1. Verpacke den Kettencode.

  2. Installiere den Kettencode.

  3. Frag die Anlage ab.

  4. Genehmige das Paket.

  5. Überprüfe die Commit-Bereitschaft.

  6. Bestätige die Definition des Kettencodes.

  7. Frag ab, ob der Kettencode festgelegt ist.

  8. Initialisiere den Vertrag.

  9. Führe eine Abfrage aus.

In Kapitel 5 werden diese Schritte ausführlicher behandelt. Sie enthalten Beispiel-Befehlszeilencode, und einige haben eine Ausgabe. Die folgenden peer Befehle können in der Hyperledger Fabric Dokumentation nachgeschlagen werden .

Den Kettencode verpacken

Als Erstes müssen wir unseren Code verpacken. Wie du aus dem folgenden Befehl ersehen kannst, verwenden wir die peer CLI, um diesen Schritt und alle anderen Schritte durchzuführen.

Um unseren Smart Contract vorzubereiten, verwenden wir den folgenden peer Paketbefehl:

peer lifecycle chaincode package fabcar.tar.gz \ 
        --path ../chaincode/fabcar/javascript/ \
        --lang node \
        --label fabcar_1

Sobald dieser Befehl abgeschlossen ist, haben wir eine tar.gz-Datei mit unserem Smart Contract. Als Nächstes müssen wir dieses Paket installieren.

Installiere den Kettencode

Nachdem wir unseren intelligenten Vertrag verpackt haben, können wir ihn installieren. Wenn mehrere Organisationen zusammenarbeiten, müssen nicht alle Organisationen ihre Smart Contracts separat verpacken. Ein einziges Smart-Contract-Paket kann von allen Organisationen genutzt werden. Sobald eine Organisation das Paket erhalten hat, wird es bei den anderen Organisationen installiert, die den Vertrag unterstützen. In Kapitel 9 wird dies ausführlicher beschrieben.

Hier ist der Installationsbefehl, der eine erfolgreiche Ausgabemeldung anzeigt:

peer lifecycle chaincode install fabcar.tar.gz

[cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed

Sobald der Vertrag installiert ist, solltest du ihn überprüfen.

Abfrage der Installation

Du kannst den folgenden Befehl ausführen, um die Details des zuletzt installierten Kettencodes zu erhalten:

peer lifecycle chaincode queryinstalled

Installed chaincodes on peer:
Package ID: fabcar_1:5a00a40697…330bf5de39, 
Label: fabcar_1

Abhängig von der Anzahl der installierten Pakete erhältst du möglicherweise viele Paket-IDs. Du kannst ein Skript verwenden, um die Ausgabe zu filtern, wenn du eine Aufgabe automatisieren musst, die davon abhängt, ob ein bestimmtes Paket installiert oder nicht installiert ist. Sobald ein Chaincode-Paket installiert ist, muss es genehmigt werden.

Genehmige das Paket

Nach der Installation eines Pakets muss eine Organisation es genehmigen, bevor es übertragen und zugänglich gemacht werden kann. Dieser Befehl hat eine Menge Parameter. Der für unsere Zwecke interessanteste ist –package-id. Wir können ihn aus der Ausgabe des vorangegangenen queryinstalled Befehls entnehmen. package-id wird als Bezeichner für das Chaincode-Installationspaket verwendet:

peer lifecycle chaincode approveformyorg 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/
tlsca.example.com-cert.pem 
--channelID mychannel 
--name fabcar 
--version 1 
--init-required 
--package-id fabcar_1:5a00a406972168ac5856857b5867f51d5244208b876206b7e0e418330bf5de39 
--sequence 1

Sobald der Befehl approve abgeschlossen ist, können wir feststellen, ob wir die Daten übertragen können. Wir verwenden den Befehl checkcommitreadiness.

Commit-Bereitschaft prüfen

Eine bestimmte Anzahl von Organisationen muss zustimmen, bevor das Kettencode-Paket freigegeben werden kann. Die Anzahl hängt von der Richtlinie ab, die verlangen kann, dass alle Organisationen oder eine Teilmenge von Organisationen zustimmen. Im Idealfall sollen alle Organisationen zustimmen. Mit dem Befehl checkcommitreadiness können wir feststellen, ob wir das Paket übertragen können. In diesem Fall können wir es nicht, weil Org2 noch nicht genehmigt hat. Sobald dies der Fall ist, wird dieser Befehl true für Org1 und true für Org2 anzeigen:

peer lifecycle chaincode checkcommitreadiness 
--channelID mychannel 
--name fabcar 
--version 1 
--sequence 1 
--output json 
--init-required

{
    "approvals": {
        "Org1MSP": true,
        "Org2MSP": false
    }
}

In einer Hyperledger-Konfiguration können wir verschiedene Arten von Life-Cycle-Endorsement-Richtlinien definieren. Die Standardeinstellung ist MAJORITY Endorsement. Dies erfordert, dass eine Mehrheit der Peers eine Chaincode-Transaktion für die Validierung und Ausführung im Channel befürwortet und die Transaktion an das Ledger überträgt. In Kapitel 9 wird dies ausführlicher behandelt. Sobald alle Genehmigungen stimmen, können wir das Kettencode-Paket übertragen.

Die Kettencode-Definition festschreiben

Sobald alle Organisationen oder Untergruppen der Organisation die genannten Richtlinien erfüllen, kann der Kettencode in das Hauptbuch übertragen werden. Um den Kettencode zu übertragen, verwenden wir den hier gezeigten Befehl commit:

peer lifecycle chaincode commit 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/
tlsca.example.com-cert.pem 
--channelID mychannel 
--name fabcar 
--peerAddresses localhost:7051 
--tlsRootCertFiles /OReilly/fabric-samples/test-network/organizations
/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
--version 1 
--sequence 1
--init-required

Hier ist die Ausgabe, nachdem der Befehl ausgeführt wurde:

[chaincodeCmd] ClientWait -> INFO 001 txid
[ef59101c320469be3242daa9ebe262771fc8cc8bd5cd0854c6424e1d2a0c61c2] committed with status (VALID) at
localhost:9051

Als Nächstes können wir prüfen, ob der Kettencode mit querycommitted festgelegt ist.

Abfrage, ob der Kettencode festgeschrieben ist

Chaincode muss festgeschrieben sein, bevor er aufgerufen werden kann. Um herauszufinden, ob Chaincode aktiviert ist, verwende den Befehl querycommitted chaincode:

peer lifecycle chaincode querycommitted 
--channelID mychannel 
--name fabcar

Hier ist die Ausgabe:

Committed chaincode definition for chaincode 'fabcar' on channel 'mychannel':
Version: 1, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc, Approvals: [Org1MSP: true,
Org2MSP: true]

Die Ausführung des Befehls querycommitted zeigt uns, dass unser Chaincode fertig ist. Dieser Chaincode enthält einen Smart Contract, der initialisiert werden muss. Um ihn zu initialisieren, rufen wir ihn auf.

Den Vertrag initialisieren

Endlich können wir unseren Smart Contract initialisieren, denn der Chaincode ist genehmigt und bestätigt. Wir können den Befehl invoke verwenden, um Smart-Contract-Transaktionen auszuführen:

peer chaincode invoke 
-o localhost:7050 
--ordererTLSHostnameOverride orderer.example.com 
--tls true 
--cafile /OReilly/fabric-samples/test-network/organizations/ordererOrganizations
/example.com/orderers/orderer.example.com/msp/tlscacerts/
tlsca.example.com-cert.pem 
-C mychannel 
-n fabcar 
--peerAddresses localhost:7051 
--tlsRootCertFiles /OReilly/fabric-samples/test-
network/organizations/peerOrganizations/org1.example.com/peers
/peer0.org1.example.com/tls/ca.crt 
--peerAddresses localhost:9051 
--tlsRootCertFiles /OReilly/fabric-samples/test-network/organizations
/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt 
--isInit
-c '{"function":"initLedger","Args":[]}'

Nach der Ausführung des Befehls invoke sehen wir die folgende Ausgabe; sie gibt einen 200-Antwortstatus zurück, wenn der Aufruf von chaincode erfolgreich war:

[chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

Am Ende des invoke Befehls sehen wir das -c Befehlsflag und das Befehlsobjekt, das den Funktionsschlüssel und den Wert initLedger ohne Argumente enthält. Das bedeutet, dass die Smart-Contract-Transaktionsfunktion initLedger ausgeführt wird. Die Ausgabe zeigt ein erfolgreiches Ergebnis. Unser Smart Contract ist nun initialisiert und bereit für Kunden. Wir können unseren Smart Contract jetzt testen, indem wir eine Abfrage ausführen.

Ausführen einer Abfrage

Wir haben die Schritte durchlaufen, um dein Smart-Contract-Quellcode-Projekt zu übernehmen und es zu instanziieren. Wir können es testen, indem wir eine Abfrage wie diese ausführen:

peer chaincode query 
-C mychannel 
-n fabcar 
-c '{"Args":["queryAllCars"]}'

Hier ist die Ausgabe nach dem Ausführen des Befehls queryAllCars:

[{"Key":"CAR0","Record":{"color":"blue","docType":"car",
"make":"Toyota","model":"Prius","owner":"Tomoko"}},
.
.
.
{"Key":"CAR9","Record":{"color":"brown","docType":"car",
"make":"Holden","model":"Barina","owner":"Shotaro"}}]

Diese Abfrage führt die Smart-Contract-Transaktionsfunktion namens queryAllCars aus. Schreibvorgänge werden übertragen und erfordern daher eine Bestätigung, an der mehrere Peers beteiligt sind. Eine Abfrage sollte nicht mehr als einen Peer mit der Ausführung beauftragen müssen. Genau das erledigt der clientseitige Code für uns. Dieses Beispiel zeigt, wie eine Transaktionsfunktion eine ChaincodeStub Funktion umhüllt, in diesem Fall eine rangeQuery abstrahiert als queryAllCars.

Zusammenfassung

Als Vorbereitung auf die Kapitel 5 und 6 haben wir zunächst unsere Hyperledger Fabric-Entwicklungsumgebung eingerichtet und sie genutzt, um einen einfachen, aber vollständigen Smart Contract namens Fabcar zu erkunden und zu untersuchen. Der Code, den wir für Fabric-Smart Contracts schreiben, hängt von den APIs ab, die von den SDKs bereitgestellt werden. Wir haben den Fabcar-Code behandelt, weil er klein und einfach zu erlernen ist. So konnten wir uns auf den Code von Smart Contracts, die verwendeten Klassen und Schnittstellen und die Fabric Smart Contract APIs konzentrieren, auf die wir angewiesen sind.

Fabric-Smart-Contract-SDKs gibt es für JavaScript, Java und Go, und weitere werden folgen. Wir haben das JavaScript Fabric-Smart-Contract-SDK für Node.js verwendet. Damit konnten wir die fabric-contract-api und die wichtigsten Klassen und Objekte kennenlernen, die wir für die Entwicklung von Fabric-Smart Contracts brauchen.

Mit dem Wissen über die API haben wir uns damit beschäftigt, wie man einen Smart Contract erstellt und was Smart-Contract-Transaktionsfunktionen sind. Funktionen führen unsere Smart-Contract-Transaktionen aus. Deshalb war es wichtig, einige wichtige Themen wie die Validierung und Bereinigung von Funktionsargumenten, die Initialisierung von Smart Contracts und die Interaktion mit dem Ledger vorzustellen. Die Fabric Contract API bietet die Schnittstelle zum Ledger, auf den du in unseren Smart Contracts über den Transaktionskontext, den jede Transaktion erhält, zugreifen kannst. Es gab viel zu behandeln, aber wir haben versucht, es einfach zu halten und dir dennoch die fabric-contract-api näher zu bringen, die die Schnittstellen enthält, die du brauchst, um robuste Smart Contracts zu entwickeln und zu implementieren.

Sobald der Code für den Smart Contract geschrieben ist, müssen wir ihn verpacken und im Fabric-Netzwerk bereitstellen. Dazu sind mehrere Schritte erforderlich. Wir haben sie Schritt für Schritt durchlaufen. Es ist wichtig, diese Schritte zu kennen, um unsere Smart Contracts vom Quellcode zum instanziierten Kettencode zu bringen. Wir können nur instanziierten Code ausführen.

In den Kapiteln 5 und 6 verpacken und instanziieren wir die von uns erstellten Smart Contracts, indem wir das in diesem Kapitel gelernte Wissen anwenden. Jetzt können wir mit Kapitel 5 fortfahren und uns auf den Aufruf von Smart Contracts konzentrieren.

Get Hands-On Smart Contract Entwicklung mit Hyperledger Fabric V2 now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.