Tutorial Hyperledger Fabric SDK Go: Comment construire votre première application ?

Ce tutoriel vous présentera le SDK Hyperledger Fabric Go et vous permettra de construire une application simple en utilisant le principe de la blockchain.

Ce tutoriel utilise Hyperledger Fabric version 1.0.5, sur le github de Heroes-Service utilisez la branche 1.0.5!

Edit : Les certificats utilisés dans ce tutoriel ne sont plus valides. La nouvelle version de ce tutoriel est finie et et disponible ici : chainhero.io/2018/06/tutorial-hyperledger-fabric-sdk-go-how-to-build-your-first-app-v1-0-5

Ceci est la première partie de ce tutoriel. Les fonctionnalités de base du SDK seront montrées, mais la deuxième partie est prévue pour démontrer une application plus complexe.

  1. Prérequis
  2. Introduction à Hyperledger Fabric
  3. Guide d'installation
    1. Docker
    2. Go
    3. Hyperledger Fabric & Certificate Authority (CA)
    4. Fabric SDK Go
  4. Création de votre premier réseau blockchain
    1. Préparation de l'environnement
    2. Build a Docker compose file
    3. Test
  5. Utilisation de Fabric SDK Go
    1. Configuration
    2. Initialisation
    3. Test
    4. Nettoyage et Makefile
    5. Installation et instanciation du chaincode
    6. Chaincode query
    7. Changement du ledger state
  6. Faire tout ceci dans une application web
  7. Références

1. Prérequis

Ce tutoriel ne va pas expliquer en détail comment fonctionne Hyperledger Fabric. Je vais juste donner quelques conseils pour comprendre le comportement général du framework. Si vous voulez obtenir une explication complète de l’outil, consultez la documentation officielle, il y a beaucoup de travail là-bas qui explique quel genre de blockchain Hyperledger Fabric est.

Ce tutoriel a été réalisé sur Ubuntu 16.04 mais le framework Hyperledger Fabric est compatible avec Mac OS X, Windows et d’autres distributions Linux.

Nous utiliserons le langage Go pour concevoir notre première application car Hyperledger Fabric a été codé dans ce langage, et car il est vraiment simple à utiliser. En outre, le chaincode (contrat intelligent) peut être écrit en Go également. Ainsi, le full-stack sera uniquement en Go! Il y a d’autres SDK si vous voulez, pour NodeJS, Java ou Python.

Hyperledger Fabric utilise Docker pour déployer facilement un réseau blockchain. En outre, certains composants (pairs) déploient également des conteneurs docker pour séparer les données (canal). Assurez-vous donc que votre plateforme supporte ce type de virtualisation.

2. Introduction to Hyperledger Fabric

Hyperledger Fabric est une plate-forme de solutions ledger distribuées reposant sur une architecture modulaire offrant des degrés élevés de confidentialité, de résilience, de flexibilité et d’évolutivité. Elle est conçue pour soutenir la mise en œuvre de différents composants et tenir compte de la complexité et des subtilités qui existent dans l’écosystème économique.

Voir l’explication complète de la documentation officielle, dans la partie introduction : Hyperledger Fabric Blockchain

3. Guide d'installation

Ce tutoriel a été fait sur Ubuntu 16.04, mais il y aussi des aides pour Windows, Mac OS X et d’autres utilisateurs de distributions Linux.

a. Docker

Linux (Ubuntu)

La version requise pour docker est 1.12 ou plus, cette version est déjà disponible dans le gestionnaire de packages sur Ubuntu. Il suffit de l’installer avec cette ligne de commande :

sudo apt install docker.io

En outre, nous avons besoin de docker-compose 1.8+ pour gérer plusieurs conteneurs à la fois. Vous pouvez également utiliser votre gestionnaire de packages qui détient la bonne version :

sudo apt install docker-compose

Maintenant, nous devons gérer l’utilisateur actuel pour éviter d’utiliser l’accès root lorsque nous utiliserons docker. Pour ce faire, il faut ajouter l’utilisateur courant au groupe docker :

sudo groupadd docker
sudo gpasswd -a ${USER} docker
sudo service docker restart
In order to apply these changes, you need to logout/login and then check versions with:
docker --version
docker-compose version

Mac OS X

Téléchargez et installez le dernier pack Docker.dmg pour Mac OS X disponible sur le site web de Docker. Cela installera docker-compose aussi.

Linux (pas Ubuntu)

Checkez les liens ci-dessous:

Windows

Lisez les instructions sur le site de Docker : docker.com/docker-for-windows

b. Go

Hyperledger Fabric nécessite une version Go 1.7.x ou plus et nous n’avons que la version Go 1.6.x dans le gestionnaire de packages. Donc cette fois, nous devons utiliser la méthode d’installation officielle. Vous pouvez suivre les instructions de golang.org ou utiliser ces commandes génériques qui vont installer Golang 1.8.3 et préparer votre environnement (générer votre GOPATH) pour Ubuntu :

wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz && \
sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz && \
rm go1.8.3.linux-amd64.tar.gz && \
echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee -a /etc/profile && \
echo 'export GOPATH=$HOME/go' | tee -a $HOME/.bashrc && \
echo 'export PATH=$PATH:$GOROOT/bin:$GOPATH/bin' | tee -a $HOME/.bashrc && \
mkdir -p $HOME/go/{src,pkg,bin}

Pour s’assurer que l’installation a fonctionné, vous pouvez vous déconnecter/reconnecter (à nouveau) et exécuter :

go version

c. Hyperledger Fabric & Certificate Authority (CA)

Maintenant, nous pouvons installer le framework principal : Fabric Hyperledger. Nous allons fixer le niveau de commit à v1.0.0-rc1 car le Fabric SDK Go est compatible avec celui-ci. Tout le code est disponible en mirror sur github, il suffit de vérifier (et éventuellement construire des binaires) :

mkdir -p $GOPATH/src/github.com/hyperledger && \
cd $GOPATH/src/github.com/hyperledger && \
git clone https://github.com/hyperledger/fabric.git && \
cd fabric && \
git checkout v1.0.0-rc1

Idem pour la partoe Hyperledger Fabric CA :

cd $GOPATH/src/github.com/hyperledger && \
git clone https://github.com/hyperledger/fabric-ca.git && \
cd fabric-ca && \
git checkout v1.0.0-rc1

Nous n’utiliserons pas directement le framework, mais ceci est utile pour avoir le framework localement dans votre GOPATH pour compiler votre application.

d. Fabric SDK Go

Enfin, nous installons le SDK Hyperledger Fabric Go qui nous permettra de communiquer facilement avec le framework Fabric. Pour éviter les problèmes de versions, nous vérifions directement un commit spécifique qui fonctionne avec le tutoriel suivant.

cd $GOPATH/src/github.com/hyperledger && \
git clone https://github.com/hyperledger/fabric-sdk-go.git && \
cd fabric-sdk-go && \
git checkout 85fa3101eb4694d464003c3a900672d632f17833

Ensuite, nous utiliserons les fonctions intégrées de golang pour installer les packages :

go get github.com/hyperledger/fabric-sdk-go/pkg/fabric-client && \
go get github.com/hyperledger/fabric-sdk-go/pkg/fabric-ca-client

Si vous obtenez l’erreur suivante :

../fabric-sdk-go/vendor/github.com/miekg/pkcs11/pkcs11.go:29:18: fatal error: ltdl.h: No such file or directory

Vous devez installer le package libltdl-dev et ré-exécuter la commande précédente (go get ...):

sudo apt install libltdl-dev

Ensuite, vous pouvez aller dans le nouveau répertoire fabric-sdk-go dans votre GOPATH et nous installerons les dépendances et vérifierons si tout va bien :

cd $GOPATH/src/github.com/hyperledger/fabric-sdk-go && make

L’installation peut prendre un certain temps (en fonction de votre connexion réseau), mais à la fin vous devriez voir Integration tests passed. Au cours de ce processus, un réseau virtuel est construit et certains tests sont effectués afin de vérifier si votre système est prêt. Maintenant nous pouvons travailler avec notre première application.

4. Création de votre premier réseau blockchain

a. Prepare environment

Afin de créer un réseau blockchain, nous utiliserons docker pour construire des ordinateurs virtuels qui géreront différents rôles. Dans ce tutoriel, nous allons rester aussi simple que possible. Hyperledger Fabric a besoin de beaucoup de certificats pour assurer le chiffrement tout au long du processus (SSL, TSL, authentification…). Heureusement, le SDK Fabric Go les fournit. Pour les utiliser, nous utilisons simplement le réseau déployé par la partie test du SDK.

Créez un nouveau répertoire dans le dossier src de votre GOPATH, nous le nommons heroes-service :

mkdir -p $GOPATH/src/github.com/chainHero/heroes-service && \
cd $GOPATH/src/github.com/chainHero/heroes-service

Maintenant, nous pouvons copier l’environnement du SDK Fabric Go placé dans le dossier de test :

cp -r $GOPATH/src/github.com/hyperledger/fabric-sdk-go/test/fixtures ./

Nous pouvons nettoyer un peu pour le rendre plus simple. Nous supprimons le chaincode par défaut, car nous ferons notre propre chaincode plus tard. Nous supprimons également certains fichiers utilisés par le script de test du SDK :

rm -rf fixtures/{config,src,.env,latest-env.sh}

b. Build a Docker compose file

Pour que cela fonctionne, nous devons éditer le fichier docker-compose.yaml, qui est le fichier de configuration de la commande docker-compose. Il indique quels conteneurs doivent être créés/lancés et avec la bonne configuration pour chacun. Prenez votre éditeur de texte favori et copiez/collez le contenu de ce dépôt :

cd $GOPATH/src/github.com/chainhero/heroes-service && \
vi fixtures/docker-compose.yaml

Checkez fixtures/docker-compose.yaml

Maintenant, si nous utilisons docker-compose, nous allons mettre en place deux autorités de certificat de tissu avec un pair pour chacun. Les pairs auront tous les rôles : ledger, endorser et commiter. De plus, un client est également créé avec l’algorithme de commande solo (pas de consensus).

c. Test

Afin de vérifier si le réseau fonctionne, nous utiliserons docker-compose pour démarrer ou arrêter tous les conteneurs en même temps. Allez dans le dossier fixtures et exécutez :

cd $GOPATH/src/github.com/chainHero/heroes-service/fixtures && \
docker-compose up

Vous verrez beaucoup de logs de différentes couleurs (pour information, le rouge ne signifie pas qu'il y a des erreurs).

Ouvrez un nouveau terminal et exécutez :

docker ps

Vous verrez : deux pairs, le client et un conteneur CA. Vous avez réussi à créer un nouveau réseau prêt à être utilisé avec le SDK. Pour arrêter le réseau, revenez au terminal précédent, appuyez sur Ctrl+C et attendez que tous les conteneurs soient arrêtés. Si vous voulez explorer plus en profondeur, consultez la documentation officielle à ce sujet : Building Your First Network

Astuce : lorsque le réseau est arrêté, tous les conteneurs utilisés restent accessibles. C’est très utile pour vérifier les logs par exemple. Vous pouvez les voir avec docker ps -a. Pour nettoyer ces conteneurs, vous devez les supprimer avec docker rm $(docker ps -aq) ou si vous avez utilisé un fichier docker-compose, allez où est ce fichier et lancez docker-compose down.

Astuce : vous pouvez lancer la commande docker-compose en arrière-plan pour garder l’invite. Pour ce faire, utilisez le paramètre -d, comme ceci : docker-compose up -d. Pour arrêter les conteneurs, exécutez dans le même dossier où se trouve docker-compose.yaml, la commande : docker-compose stop (ou docker-compose down pour nettoyer après que tous les conteneurs sont arrêtés).

5. Utilisation de Fabric SDK Go

a. Configuration

Comme nous avons supprimé le dossier de configuration, nous devons créer un nouveau fichier de configuration. Nous allons y mettre tout ce que le SDK Fabric Go et nos paramètres personnalisés pour notre application doit fonctionner. Le fichier de configuration contiendra tous nos paramètres personnalisés et tout le reste dont le SDK Fabric Go a besoin pour que notre application fonctionne. Pour le moment, nous allons seulement essayer de faire fonctionner le SDK Fabric Go avec le chaincode par défaut :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
vi config.yaml
client:
 peers:
  # peer0
  - host: "localhost"
    port: 7051
    eventHost: "localhost"
    eventPort: 7053
    primary: true
    tls:
      # Certificate location absolute path
      certificate: "$GOPATH/src/github.com/chainhero/heroes-service/fixtures/channel/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/cacerts/org1.example.com-cert.pem"
      serverHostOverride: "peer0.org1.example.com"

 tls:
  enabled: true

 security:
  enabled: true
  hashAlgorithm: "SHA2"
  level: 256

 tcert:
  batch:
    size: 200

 orderer:
  host: "localhost"
  port: 7050
  tls:
    # Certificate location absolute path
    certificate: "$GOPATH/src/github.com/chainhero/heroes-service/fixtures/channel/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/cacerts/example.com-cert.pem"
    serverHostOverride: "orderer.example.com"

 logging:
  level: info

 fabricCA:
  tlsEnabled: true
  id: "Org1MSP"
  name: "ca-org1"
  homeDir: "/tmp/"
  mspDir: "msp"
  serverURL: "https://localhost:7054"
  certfiles :
    - "$GOPATH/src/github.com/chainhero/heroes-service/fixtures/tls/fabricca/ca/ca_root.pem"
  client:
   keyfile: "$GOPATH/src/github.com/chainhero/heroes-service/fixtures/tls/fabricca/client/client_client1-key.pem"
   certfile: "$GOPATH/src/github.com/chainhero/heroes-service/fixtures/tls/fabricca/client/client_client1.pem"

 cryptoconfig:
  path: "$GOPATH/src/github.com/chainhero/heroes-service/fixtures/channel/crypto-config"

Le fichier de configuration en entier est disponible ici : config.yaml

b. Initialisation

Nous ajoutons un nouveau dossier nommé blockchain qui contiendra toute l’interface qui communique avec le réseau. Nous verrons le Fabric SDK Go uniquement

mkdir $GOPATH/src/github.com/chainHero/heroes-service/blockchain

Maintenant, nous ajoutons un nouveau fichier nommé setup.go:

vi $GOPATH/src/github.com/chainHero/heroes-service/blockchain/setup.go
package blockchain

import (
	api "github.com/hyperledger/fabric-sdk-go/api"
	fsgConfig "github.com/hyperledger/fabric-sdk-go/pkg/config"
	bccspFactory "github.com/hyperledger/fabric/bccsp/factory"
	fcutil "github.com/hyperledger/fabric-sdk-go/pkg/util"
	"github.com/hyperledger/fabric-sdk-go/pkg/fabric-client/events"
	"fmt"
)

// FabricSetup implementation
type FabricSetup struct {
	Client           api.FabricClient
	Channel          api.Channel
	EventHub         api.EventHub
	Initialized      bool
	ChannelId        string
	ChannelConfig    string
}

// Initialize reads the configuration file and sets up the client, chain and event hub
func Initialize() (*FabricSetup, error) {

	// Add parameters for the initialization
	setup := FabricSetup{
		// Channel parameters
		ChannelId:        "mychannel",
		ChannelConfig:    "fixtures/channel/mychannel.tx",
	}

	// Initialize the configuration
	// This will read the config.yaml, in order to tell to
	// the SDK all options and how contact a peer
	configImpl, err := fsgConfig.InitConfig("config.yaml")
	if err != nil {
		return nil, fmt.Errorf("Initialize the config failed: %v", err)
	}

	// Initialize blockchain cryptographic service provider (BCCSP)
	// This tool manages certificates and keys
	err = bccspFactory.InitFactories(configImpl.GetCSPConfig())
	if err != nil {
		return nil, fmt.Errorf("Failed getting ephemeral software-based BCCSP [%s]", err)
	}

	// This will make a user access (here the admin) to interact with the network
	// To do so, it will contact the Fabric CA to check if the user has access
	// and give it to him (enrollment)
	client, err := fcutil.GetClient("admin", "adminpw", "/tmp/enroll_user", configImpl)
	if err != nil {
		return nil, fmt.Errorf("Create client failed: %v", err)
	}
	setup.Client = client

	// Make a new instance of channel pre-configured with the info we have provided,
	// but for now we can't use this channel because we need to create and
	// make some peer join it
	channel, err := fcutil.GetChannel(setup.Client, setup.ChannelId)
	if err != nil {
		return nil, fmt.Errorf("Create channel (%s) failed: %v", setup.ChannelId, err)
	}
	setup.Channel = channel

	// Get an orderer user that will validate a proposed order
	// The authentication will be made with local certificates
	ordererUser, err := fcutil.GetPreEnrolledUser(
		client,
		"ordererOrganizations/example.com/users/[email protected]/keystore",
		"ordererOrganizations/example.com/users/[email protected]/signcerts",
		"ordererAdmin",
	)
	if err != nil {
		return nil, fmt.Errorf("Unable to get the orderer user failed: %v", err)
	}

	// Get an organisation user (admin) that will be used to sign the proposal
	// The authentication will be made with local certificates
	orgUser, err := fcutil.GetPreEnrolledUser(
		client,
		"peerOrganizations/org1.example.com/users/[email protected]/keystore",
		"peerOrganizations/org1.example.com/users/[email protected]/signcerts",
		"peerorg1Admin",
	)
	if err != nil {
		return nil, fmt.Errorf("Unable to get the organisation user failed: %v", err)
	}

	// Initialize the channel "mychannel" based on the genesis block by
	//  1. locating in fixtures/channel/mychannel.tx and
	//  2. joining the peer given in the configuration file to this channel
	if err := fcutil.CreateAndJoinChannel(client, ordererUser, orgUser, channel, setup.ChannelConfig); err != nil {
		return nil, fmt.Errorf("CreateAndJoinChannel return error: %v", err)
	}

	// Give the organisation user to the client for next proposal
	client.SetUserContext(orgUser)

	// Setup Event Hub
	// This will allow us to listen for some event from the chaincode
	// and act on it. We won't use it for now.
	eventHub, err := getEventHub(client)
	if err != nil {
		return nil, err
	}
	if err := eventHub.Connect(); err != nil {
		return nil, fmt.Errorf("Failed eventHub.Connect() [%s]", err)
	}
	setup.EventHub = eventHub

	// Tell that the initialization is done
	setup.Initialized = true

	return &setup, nil
}

// getEventHub initialize the event hub
func getEventHub(client api.FabricClient) (api.EventHub, error) {
	eventHub, err := events.NewEventHub(client)
	if err != nil {
		return nil, fmt.Errorf("Error creating new event hub: %v", err)
	}
	foundEventHub := false
	peerConfig, err := client.GetConfig().GetPeersConfig()
	if err != nil {
		return nil, fmt.Errorf("Error reading peer config: %v", err)
	}
	for _, p := range peerConfig {
		if p.EventHost != "" && p.EventPort != 0 {
			fmt.Printf("EventHub connect to peer (%s:%d)\n", p.EventHost, p.EventPort)
			eventHub.SetPeerAddr(fmt.Sprintf("%s:%d", p.EventHost, p.EventPort),
				p.TLS.Certificate, p.TLS.ServerHostOverride)
			foundEventHub = true
			break
		}
	}

	if !foundEventHub {
		return nil, fmt.Errorf("No EventHub configuration found")
	}

	return eventHub, nil
}

Le fichier est disponible ici : blockchain/setup.go

À ce stade, nous avons seulement initialisé un client qui communiquera avec un pair, un AC et un client. Nous avons également fait un nouveau canal et connecté ce pair à ce canal. Voir les commentaires dans le code pour plus d’informations.

c. Test

Pour s’assurer que le client a réussi à initialiser tous ses composants, nous ferons un test simple avec le réseau lancé. Pour ce faire, nous devons écrire le code Go. Comme nous n’avons pas de fichier principal, nous devons en ajouter un :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
vi main.go
package main

import (
	"github.com/chainhero/heroes-service/blockchain"
	"fmt"
	"os"
	"runtime"
	"path/filepath"
)

// Fix empty GOPATH with golang 1.8 (see https://github.com/golang/go/blob/1363eeba6589fca217e155c829b2a7c00bc32a92/src/go/build/build.go#L260-L277)
func defaultGOPATH() string {
	env := "HOME"
	if runtime.GOOS == "windows" {
		env = "USERPROFILE"
	} else if runtime.GOOS == "plan9" {
		env = "home"
	}
	if home := os.Getenv(env); home != "" {
		def := filepath.Join(home, "go")
		if filepath.Clean(def) == filepath.Clean(runtime.GOROOT()) {
			// Don't set the default GOPATH to GOROOT,
			// as that will trigger warnings from the go tool.
			return ""
		}
		return def
	}
	return ""
}

func main() {
	// Setup correctly the GOPATH in the environment
	if goPath := os.Getenv("GOPATH"); goPath == "" {
		os.Setenv("GOPATH", defaultGOPATH())
	}

	// Initialize the Fabric SDK
	_, err := blockchain.Initialize()
	if err != nil {
		fmt.Printf("Unable to initialize the Fabric SDK: %v", err)
	}
}

Le fichier est disponible ici : main.go

Comme vous pouvez le voir, nous avons corrigé le GOPATH de l’environnement s’il n’est pas réglé. Nous aurons besoin de cette fonctionnalité pour compiler le chaincode (nous verrons cela à l’étape suivante).

La dernière chose à faire avant de commencer la compilation est d’utiliser un répertoire fournisseur. Dans notre GOPATH nous avons Fabric, Fabric CA, Fabric SDK Go et peut-être d’autres projets. Lorsque nous tenterons de compiler notre application, il pourrait y avoir des conflits (comme les multiples définitions de BCCSP). Nous allons gérer cela en utilisant un outil comme govendor pour aplatir ces dépendances. Il suffit de l’installer et d’importer des dépendances externes dans le répertoire fournisseur comme ceci :

go get -u github.com/kardianos/govendor && \
cd $GOPATH/src/github.com/chainhero/heroes-service && \
govendor init && govendor add +external

Maintenant nous pouvons compiler notre application:

cd $GOPATH/src/github.com/chainHero/heroes-service && \
go build

Après un certain temps, un nouveau binaire nommé heroes-service apparaîtra à la racine du projet. Essayez de démarrer le binaire comme ceci :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
./heroes-service

À ce stade, cela ne fonctionnera pas parce qu’il n’y a pas de réseau déployé avec lequel le SDK peut communiquer. Démarrez le réseau et relancez l’application :

cd $GOPATH/src/github.com/chainHero/heroes-service/fixtures && \
docker-compose up -d && \
cd .. && \
./heroes-service

Très bien ! Nous venons donc d’initialiser le SDK avec notre réseau local. Dans la prochaine étape, nous allons interagir avec un chaincode.

d. Nettoyage et Makefile

Le Fabric SDK génère des fichiers, comme des certificats, des binaires et des fichiers temporaires. Fermer le réseau ne nettoiera pas complètement votre environnement et lorsque vous devrez le redémarrer, ces fichiers seront réutilisés pour éviter le processus de building. Pour le développement, vous pouvez les garder pour tester rapidement, mais pour un vrai test, vous devez tout nettoyer et commencer dès le début.

Comment nettoyer mon environnement ?

  • Coupez votre réseau: cd $GOPATH/src/github.com/chainhero-website/heroes-service/fixtures && docker-compose down
  • Supprimez le dossier MSP (défini dans le fichier de config, dans la section fabricCA) : rm -rf /tmp/msp
  • Supprimez les fichiers d’inscription (définis lorsque nous initialisons le SDK, dans le fichier de configuration, lorsque nous obtenons le client) : rm -rf /tmp/enroll_user
  • Supprimez certains conteneurs et images de Docker non générés par la commande docker-compose : docker rm -f -v `docker ps -a --no-trunc | grep "heroes-service" | cut -d ' ' -f 1` 2>/dev/null and docker rmi `docker images --no-trunc | grep "heroes-service" | cut -d ' ' -f 1` 2>/dev/null

Comment être plus efficace ?

Nous pouvons automatiser toutes ces tâches en une seule étape. Le processus de build et de démarrage peut également être automatisé. Pour ce faire, nous créerons un Makefile. Premièrement, assurez-vous d’avoir l’outil :

make --version

Si make n'est pas installé, entre cette commande (Ubuntu):

sudo apt install make

Puis créez un fichier nommé Makefile à la racine du projet avec ce contenu :

.PHONY: all dev clean build env-up env-down run

all: clean build env-up run

dev: build run

##### BUILD
build:
	@echo "Build ..."
	@govendor sync
	@go build
	@echo "Build done"

##### ENV
env-up:
	@echo "Start environnement ..."
	@cd fixtures && docker-compose up --force-recreate -d
	@echo "Sleep 15 seconds in order to let the environment setup correctly"
	@sleep 15
	@echo "Environnement up"

env-down:
	@echo "Stop environnement ..."
	@cd fixtures && docker-compose down
	@echo "Environnement down"

##### RUN
run:
	@echo "Start app ..."
	@./heroes-service

##### CLEAN
clean: env-down
	@echo "Clean up ..."
	@rm -rf /tmp/enroll_user /tmp/msp heroes-service
	@docker rm -f -v `docker ps -a --no-trunc | grep "heroes-service" | cut -d ' ' -f 1` 2>/dev/null || true
	@docker rmi `docker images --no-trunc | grep "heroes-service" | cut -d ' ' -f 1` 2>/dev/null || true
	@echo "Clean up done"

Le fichier est disponible ici : Makefile

Maintenant avec la tâche all:

  1. tout l’environnement sera nettoyé,
  2. puis notre programme Go sera compilé,
  3. après quoi le réseau sera déployé et
  4. l’application sera enfin opérationnelle.

Pour l’utiliser, allez dans la racine du projet et entrez la commande make :

  • Tâche all : make ou make all
  • Tâche build : make build
  • Tâche env-up : make env-up
  • ...

e. Installation et instanciation du chaincode

À cette étape, nous pouvons presque utiliser le système blockchain. Mais pour l’instant, nous n’avons pas encore mis en place de chaincode (contrat intelligent) qui traitera les requêtes de notre application. Tout d’abord, créons un nouveau répertoire nommé chaincode et ajoutons un nouveau fichier nommé main.go (c’est le principal point d’entrée de notre contrat intelligent) :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
mkdir chaincode && \
vi chaincode/main.go
package main

import (
	"fmt"
	"github.com/hyperledger/fabric/core/chaincode/shim"
	pb "github.com/hyperledger/fabric/protos/peer"
)

// HeroesServiceChaincode implementation of Chaincode
type HeroesServiceChaincode struct {
}

// Init of the chaincode
// This function is called only one when the chaincode is instantiated.
// So the goal is to prepare the ledger to handle future requests.
func (t *HeroesServiceChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
	fmt.Println("########### HeroesServiceChaincode Init ###########")

	// Get the function and arguments from the request
	function, _ := stub.GetFunctionAndParameters()

	// Check if the request is the init function
	if function != "init" {
		return shim.Error("Unknown function call")
	}

	// Put in the ledger the key/value hello/world
	err := stub.PutState("hello", []byte("world"))
	if err != nil {
		return shim.Error(err.Error())
	}

	// Return a successful message
	return shim.Success(nil)
}

// Invoke
// All future requests named invoke will arrive here.
func (t *HeroesServiceChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	fmt.Println("########### HeroesServiceChaincode Invoke ###########")

	// Get the function and arguments from the request
	function, args := stub.GetFunctionAndParameters()

	// Check whether it is an invoke request
	if function != "invoke" {
		return shim.Error("Unknown function call")
	}

	// Check whether the number of argument is sufficient
	if len(args) < 1 {
		return shim.Error("The number of arguments is insufficient.")
	}

	// In order to manage multiple type of request, we will check the first argument.
	// Here we have one possible argument: query (every query request will read in the ledger without modification)
	if args[0] == "query" {
		return t.query(stub, args)
	}

	// If the arguments given don’t match any function, we return an error
	return shim.Error("Unknown action, check the first argument")
}

// query
// Every readonly functions in the ledger will be here
func (t *HeroesServiceChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {

	// whether the number of arguments is sufficient
	if len(args) < 2 {
		return shim.Error("The number of arguments is insufficient.")
	}

	// Like the Invoke function, we manage multiple type of query requests with the second argument.
	// We also have only one possible argument: hello
	if args[1] == "hello" {

		// Get the state of the value matching the key hello in the ledger
		state, err := stub.GetState("hello")
		if err != nil {
			return shim.Error("Failed to get state of hello")
		}

		// Return this value in response
		return shim.Success(state)
	}

	// If the arguments given don’t match any function, we return an error
	return shim.Error("Unknown query action, check the second argument.")
}

func main() {
	// Start the chaincode and make it ready for futures requests
	err := shim.Start(new(HeroesServiceChaincode))
	if err != nil {
		fmt.Printf("Error starting Heroes Service chaincode: %s\n", err)
	}
}

Le fichier est disponible ici : chaincode/main.go

Nous choisissons de mettre le chaincode ici pour simplifier l’application, mais du point de vue de l’architecture, il sera préférable d’utiliser l’architecture donnée par le SDK et de mettre le chaincode dans le dossier src de fixtures. Le chaincode n’est pas vraiment lié à l’application, nous pouvons avoir un dépôt pour l’application et un autre pour le chaincode. Pour votre information, dans un futur proche, le chaincode pourrait être écrit dans d’autres langages.

Pour l’instant, le chaincode ne fait rien d’extraordinaire, il suffit de mettre la clé/valeur hello/world dans le ledger à l’initialisation. En outre, il y a une fonction que nous pouvons invoquer par un appel : query hello. Cette fonction obtient l’état du ledger, i.e. hello et le donne en réponse. Nous testerons ça à l’étape suivante, après avoir installé et instancié le chaincode avec succès.

Pour installer et instancier le chaincode, nous devons ajouter du code dans l’application. Éditez le fichier blockchain/setup.go avec les lignes suivantes :

ligne 3 de blockchain/setup.go: nous ajoutons l’import de l'OS pour accéder à la variable GOPATH dans l’environnement

import (
	api "github.com/hyperledger/fabric-sdk-go/api"
	fsgConfig "github.com/hyperledger/fabric-sdk-go/pkg/config"
	bccspFactory "github.com/hyperledger/fabric/bccsp/factory"
	fcutil "github.com/hyperledger/fabric-sdk-go/pkg/util"
	"github.com/hyperledger/fabric-sdk-go/pkg/fabric-client/events"
	"fmt"
	"os"
)

ligne 13 de blockchain/setup.go: nous ajoutons les paramètres du chaincode

// FabricSetup implementation
type FabricSetup struct {
	Client           api.FabricClient
	Channel          api.Channel
	EventHub         api.EventHub
	Initialized      bool
	ChannelId        string
	ChannelConfig    string
	ChaincodeId      string
	ChaincodeVersion string
	ChaincodeGoPath  string
	ChaincodePath    string
}

ligne 28 de blockchain/setup.go: nous définissons de nouveaux paramètres

func Initialize() (*FabricSetup, error) {

	// Add parameters for the initialization
	setup := FabricSetup{
		// Channel parameters
		ChannelId:        "mychannel",
		ChannelConfig:    "fixtures/channel/mychannel.tx",

		// Chaincode parameters
		ChaincodeId:      "heroes-service",
		ChaincodeVersion: "v1.0.0",
		ChaincodeGoPath:  os.Getenv("GOPATH"),
		ChaincodePath:    "github.com/chainhero/heroes-service/chaincode",
	}
    
    [...]
}

ligne 156 de blockchain/setup.go: nous ajoutons la fonction qui installera et instanciera le chaincode

// Install and instantiate the chaincode
func (setup *FabricSetup) InstallAndInstantiateCC() error {

	// Check if chaincode ID is provided
	// otherwise, generate a random one
	if setup.ChaincodeId == "" {
		setup.ChaincodeId = fcutil.GenerateRandomID()
	}

	fmt.Printf(
		"Chaincode %s (version %s) will be installed (Go Path: %s / Chaincode Path: %s)\n",
		setup.ChaincodeId,
		setup.ChaincodeVersion,
		setup.ChaincodeGoPath,
		setup.ChaincodePath,
	)

	// Install ChainCode
	// Package the go code and make a proposal to the network with this new chaincode
	err := fcutil.SendInstallCC(
		setup.Client, // The SDK client
		setup.Channel, // The channel concerned
		setup.ChaincodeId,
		setup.ChaincodePath,
		setup.ChaincodeVersion,
		nil,
		setup.Channel.GetPeers(), // Peers concerned by this change in the channel
		setup.ChaincodeGoPath,
	)
	if err != nil {
		return fmt.Errorf("Send install proposal return error: %v", err)
	} else {
		fmt.Printf("Chaincode %s installed (version %s)\n", setup.ChaincodeId, setup.ChaincodeVersion)
	}

	// Instantiate ChainCode
	// Call the Init function of the chaincode in order to initialize in every peer the new chaincode
	err = fcutil.SendInstantiateCC(
		setup.Channel,
		setup.ChaincodeId,
		setup.ChannelId,
		[]string{"init"}, // Arguments for the invoke request
		setup.ChaincodePath,
		setup.ChaincodeVersion,
		[]api.Peer{setup.Channel.GetPrimaryPeer()}, // Which peer to contact
		setup.EventHub,
	)
	if err != nil {
		return err
	} else {
		fmt.Printf("Chaincode %s instantiated (version %s)\n", setup.ChaincodeId, setup.ChaincodeVersion)
	}

	return nil
}

Le fichier est disponible ici : blockchain/setup.go

Astuce : prenez soin de la version chaincode, si vous voulez mettre à jour votre chaincode, incrémentez ce nombre. Sinon le réseau gardera le même chaincode.

Enfin, nous ajoutons l’appel à cette fonction dans le fichier main.go après l’initialisation du SDK :

ligne 38 de main.go : nous ajoutons la fonction qui installera et instanciera le chaincode

func main() {

[...]

	// Initialize the Fabric SDK
	fabricSdk, err := blockchain.Initialize()
	if err != nil {
		fmt.Printf("Unable to initialize the Fabric SDK: %v\n", err)
	}

	// Install and instantiate the chaincode
	err = fabricSdk.InstallAndInstantiateCC()
	if err != nil {
		fmt.Printf("Unable to install and instantiate the chaincode: %v\n", err)
	}
}

Le fichier est disponible ici : main.go

Nous pouvons le tester, uniquement avec la commande make de l’étape précédente :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
make

Astuce : l’installation et l’instanciation n’ont pas besoin d’être exécutées à chaque démarrage de l’application, seulement lorsque nous mettons à jour le chaincode (et la version chaincode). Une solution est de fournir un argument lorsque nous lançons l’application pour dire de faire cette procédure supplémentaire avant de passer. Dans ce tutoriel, nous allons nettoyer l’environnement chaque fois que nous ne nous soucions pas vraiment de cela.

f. Chaincode query

Comme une base de données, le chaincode est branché et prêt à répondre. Essayons la requête hello.

Nous mettrons toutes les fonctions de requête dans un nouveau fichier nommé query.go dans le dossier blockchain :

cd $GOPATH/src/github.com/chainhero/heroes-service && \
vi blockchain/query.go
package blockchain

import (
	fcutil "github.com/hyperledger/fabric-sdk-go/pkg/util"
	api "github.com/hyperledger/fabric-sdk-go/api"
	"fmt"
)

// QueryHello query the chaincode to get the state of hello
func (setup *FabricSetup) QueryHello() (string, error) {

	// Prepare arguments
	var args []string
	args = append(args, "invoke")
	args = append(args, "query")
	args = append(args, "hello")

	// Make the proposal and submit it to the network (via our primary peer)
	transactionProposalResponses, _, err := fcutil.CreateAndSendTransactionProposal(
		setup.Channel,
		setup.ChaincodeId,
		setup.ChannelId,
		args,
		[]api.Peer{setup.Channel.GetPrimaryPeer()}, // Peer contacted when submitted the proposal
		nil,
	)
	if err != nil {
		return "", fmt.Errorf("Create and send transaction proposal return error in the query hello: %v", err)
	}
	return string(transactionProposalResponses[0].ProposalResponse.GetResponse().Payload), nil
}

Le fichier est disponible ici : blockchain/query.go

Ajoutez l’appel à cette nouvelle fonction dans le fichier main.go:

ligne 49 de main.go

func main() {

[...]

	// Query the chaincode
	response, err := fSetup.QueryHello()
	if err != nil {
		fmt.Printf("Unable to query hello on the chaincode: %v\n", err)
	} else {
		fmt.Printf("Response from the query hello: %s\n", response)
	}
}

Le fichier est disponible ici : main.go

Essayons :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
make

g. Changer l'état du ledger

La prochaine chose à réaliser pour passer en revue les bases du Fabric SDK Go est de faire une demande au chaincode afin de changer l’état du ledger.

Tout d’abord, nous allons ajouter cette capacité dans le chaincode. Éditez le fichier chaincode/main.go :

ligne 97 de chaincode/main.go

// invoke
// All functions that read and write in the ledger will be here
func (t *HeroesServiceChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response {

	if len(args) < 2 {
		return shim.Error("The number of arguments is insufficient, you need to provide the function for the invoke.")
	}

	if args[1] == "hello" && len(args) == 3 {

		//
		err := stub.PutState("hello", []byte(args[2]))
		if err != nil {
			return shim.Error("Failed to update state of hello")
		}

		// Return this value in response
		return shim.Success(nil)
	}

	// If the arguments given don't match any function, we return an error
	return shim.Error("Unknown invoke action, check the second argument.")
}

ligne 57 de chaincode/main.go

func (t *HeroesServiceChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {

[...]

	if args[0] == "query" {
		return t.query(stub, args)
	}

	// The update argument will manage all update in the ledger
	if args[0] == "invoke" {
		return t.invoke(stub, args)
	}

[...]

}

Le fichier est disponible ici : chaincode/main.go

Du côté application, nous ajoutons une nouvelle fonction pour faire l’invocation du chaincode. Ajoutez un fichier nommé invoke.go dans le dossier blockchain :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
vi blockchain/invoke.go
package blockchain

import (
	fcutil "github.com/hyperledger/fabric-sdk-go/pkg/util"
	api "github.com/hyperledger/fabric-sdk-go/api"
	"fmt"
	"time"
)

// InvokeHello
func (setup *FabricSetup) InvokeHello(value string) (string, error) {

	// Prepare arguments
	var args []string
	args = append(args, "invoke")
	args = append(args, "invoke")
	args = append(args, "hello")
	args = append(args, value)

	// Add data that will be visible in the proposal, like a description of the invoke request
	transientDataMap := make(map[string][]byte)
	transientDataMap["result"] = []byte("Transient data in hello invoke")

	// Make a next transaction proposal and send it
	transactionProposalResponse, txID, err := fcutil.CreateAndSendTransactionProposal(
		setup.Channel,
		setup.ChaincodeId,
		setup.ChannelId,
		args,
		[]api.Peer{setup.Channel.GetPrimaryPeer()},
		transientDataMap,
	)
	if err != nil {
		return "", fmt.Errorf("Create and send transaction proposal in the invoke hello return error: %v", err)
	}

	// Register the Fabric SDK to listen to the event that will come back when the transaction will be send
	done, fail := fcutil.RegisterTxEvent(txID, setup.EventHub)

	// Send the final transaction signed by endorser
	if _, err := fcutil.CreateAndSendTransaction(setup.Channel, transactionProposalResponse); err != nil {
		return "", fmt.Errorf("Create and send transaction in the invoke hello return error: %v", err)
	}

	// Wait for the result of the submission
	select {
	// Transaction Ok
	case <-done:
		return txID, nil

	// Transaction failed
	case <-fail:
		return "", fmt.Errorf("Error received from eventhub for txid(%s) error(%v)", txID, fail)

	// Transaction timeout
	case <-time.After(time.Second * 30):
		return "", fmt.Errorf("Didn't receive block event for txid(%s)", txID)
	}
}

Le fichier est disponible ici : blockchain/invoke.go

Ajoutez l’appel à cette fonction dans le fichier main.go:

ligne 49 de main.go

func main() {

[...]

	// Query the chaincode
	response, err := fSetup.QueryHello()
	if err != nil {
		fmt.Printf("Unable to query hello on the chaincode: %v\n", err)
	} else {
		fmt.Printf("Response from the query hello: %s\n", response)
	}

	// Invoke the chaincode
	txId, err := fSetup.InvokeHello("chainHero")
	if err != nil {
		fmt.Printf("Unable to invoke hello on the chaincode: %v\n", err)
	} else {
		fmt.Printf("Successfully invoke hello, transaction ID: %s\n", txId)
	}

	// Query again the chaincode
	response, err = fSetup.QueryHello()
	if err != nil {
		fmt.Printf("Unable to query hello on the chaincode: %v\n", err)
	} else {
		fmt.Printf("Response from the query hello: %s\n", response)
	}
}

Le fichier est disponible ici : main.go

Essayons :

cd $GOPATH/src/github.com/chainHero/heroes-service && \
make

6. Faire tout ceci dans une application web

Nous pouvons également rendre cela utilisable pour tout utilisateur. Le meilleur choix est une application web et nous avons de la chance parce que le langage Go fournit nativement un serveur web qui gère les requêtes HTTP et qui modélise le HTML.

Pour l’instant, nous avons seulement deux actions différentes : la requête et l’invocation de la valeur hello. Faisons deux pages HTML pour chaque action. Nous ajoutons un répertoire web avec trois autres répertoires :

  • web/templates: contient toutes les pages HTML (templates)
  • web/assets: contient tout le CSS, Javascript, Polices, Images…
  • web/controllers: contient toutes les fonctions qui rendront les templates

Nous utilisons le MVC (Model-View-Controller) pour le rendre plus lisible. Le modèle sera la partie blockchain, les vues sont des templates et les contrôleurs sont fournis par les fonctions dans le répertoire controllers.

Remplissez chacun d'entre eux avec le code approprié (nous avons également ajouté Bootstrap pour rendre le résultat un peu plus joli) :

Enfin, nous modifions le fichier main.go afin d’utiliser l’interface web au lieu d’interroger directement la blockchain.

Lancez l'application et allez à l'URL localhost:3000/home.html:

cd $GOPATH/src/github.com/chainhero/heroes-service && \
make

La page home effectue une requête dans la blockchain pour obtenir la valeur de la clé hello et l’afficher.

La page request contient un formulaire permettant de modifier la valeur hello. Après un envoi réussi, l'ID de la transaction est donné.

Nous pouvons voir le changement en retournant à la page home.

C’est la fin de la première partie. Une application plus complexe viendra prochainement.

7. Références

Publié par Antoine Chabert