REX Blockchain IV Hyperledger Fabric

Aujourd’hui nous allons parler de blockchain, technologie dont le buzz ne cesse de monter (et la crypto de descendre). Ce POC a duré un peu plus longtemps que d’habitude, de par sa complexité technique et sa conception. Il était essentiel pour l’équipe Innovation de tester au moins un framework et de mettre les mains dans le cambouis, afin d’être en mesure de vous éclairer sur cette technologie.

Concepts du POC

Le sujet retenu pour ce test est l’information voyageur communautaire à grande échelle. Aujourd’hui chaque acteur a son information (aérien, train, transport public,.. ) qu’il expose plus ou moins publiquement. L’objectif est de créer un système décentralisé qui contient toute l’information voyageur de tous les acteurs :

  • Historisée : L’information sera de fait historisée grâce à la chaine
  • Sécurisée : personne ne peut contredire un retard
  • Mutualisée et liée : un événement peut dépendre d’un autre
  • Communautaire : plusieurs acteurs peuvent êtres sollicités pour valider une information (via Smart Contract)
  • Bienveillante : des Smart Contracts peuvent être ajoutés à la chaine pour anticiper et aider un utilisateur à trouver une solution pour pallier à son problème de voyage

Pour le MVP, nous nous sommes arrêtés à une information issue des voyageurs, qui nécessite plusieurs voyageurs pour la valider, grâce à des systèmes de Web Push et de Smart Contracts.

Interface de l’application du POC

Concepts Hyperledger Fabric

Le concept de blockchain est décliné sous plusieurs implémentations. Hyperledger Fabric est celle proposée par la Linux Foundation, et est orientée sur un usage privé et professionnel. Elle est organisée autour de différents concepts :

  • Organization : Un acteur de la chaine, par exemple Oui.sncf
  • Peer : Un noeud du réseau. Un noeud appartient à une Organization, qui est chargée de maintenir son fonctionnement
  • Orderer : brick chargée d’organiser la vie du réseau. Elle va par exemple installer un Chaincode (Smart-Contract) sur un Peer
  • Channel : un Channel est une ‘sous blockchain’ dans laquelle on va inscrire les blocks concernants un sujet. Par exemple on peut avoir un Channel Public, un Channel train, aérien, … Chaque Peer peut être abonné à un Channel différent afin d’ajouter des blocks au bon fil de discussion
  • CA : brick chargée de vérifier les certificats. Elle permet notamment :

— d’ajouter des identités (Organizations, Admin, Users, …), ou de se connecter à un LDAP

— génération de certificats (d’Enrollment)

— renouvellement et révocation de certificats

  • Gossip : Hyperledger Fabric optimise les performances, la sécurité et l’évolutivité du réseau en divisant la charge de travail entre les Peers. Pour cela Fabric implémente un protocole de diffusion de données à base de Gossip. Les Peers exploitent les rumeurs pour diffuser les données du Ledger et des Channels de manière évolutive. L’envoie de Gossips est continue et chaque Peer sur un Channel reçoit en permanence des données de Ledger actuelles et cohérentes provenant de plusieurs Peers. Chaque message Gossip est signé, permettant ainsi aux Peers de repérer facilement des messages factices et d’empêcher la distribution du ou des messages à des cibles indésirables. Les Peers affectés par des retards, des pertes de réseau ou d’autres causes entraînant des blocks manquants seront éventuellement synchronisés avec l’état actuel du Ledger en contactant des Peers en possession de ces blocks manquants
  • Transactions / Block cachés : Private data : Permet depuis la version 1.2 de Fabric de créer des données privées dans un Channel plutôt que de créer un nouveau Channel dédié. Cela permet de partager entre plusieurs Organizations un channel, mais de cacher certaines données spécifiques à un acteur
Exemple de Private State
  • Cycle de vie du Chaincode : Pour utiliser un Chaincode, il faut l’installer sur le réseau, puis exécuter une commande spéciale pour l’Instancier
Schéma de l’organisation des différents acteurs du réseau

Architecture Hyperledger Fabric

Le framework se base sur plusieurs technologies connues qui ont fait leurs preuves :

  • Création de block : La chaine se compose de 2 parties : le world state et la blockchain. Le World State est l’état des objets, du ledger. Il ne contient pas tous les blocks/transactions, seulement l’état final des objets. Contrairement aux blocks physiques qui sont eux des fichiers, et qui sont créés sur les Peers à chaque insertion dans la chaine.
  • CouchDb : utilisé pour le stockage et pour des complex queries sur le world state. Permet par exemple de faire des requêtes de jointure entre les Channels, blocks, …
  • Gestion des DNS des instances : réseau docker
  • Kafka : Kafka est un Orderer/Orchestrateur de messages résiliant. Chaque Channel de la blockchain est mappé sur une topic unique Kafka. Lorsqu’un OSN(ordering service nodes, groupe d’Orderers) reçoit des transactions via le Broadcast RPC (Remote Procedure Call), il vérifie que le client qui broadcast est autorisé à écrire sur le Channel, puis relaie ces transactions à la partition appropriée de Kafka. Cette partition est également consommée par l’OSN qui regroupe les transactions reçues en blocs localement, les conserve dans son Ledger local et les sert aux Peers via le RPC Deliver.
  • Un certificat par Peer/Organisation/…, les crypto materials : la stack Hyperledger nécessite la génération de certificats pour chaque composants. Des outils sont fournis pour faciliter cette création en fonction de la topologie de la chaine. Des certificats d’utilisateurs sont également générés, qui correspondent à des utilisateurs (Admin, User, …) qui ont des droits de lecture/écriture sur différents objets de le chaine.
  • Docker : toute la stack est utilisable sous forme de conteneurs, via docker compose. Différents tutoriels propose une installation sur Kubernetes
  • Zookeeper : service centralisé pour la maintenance des informations de configuration, nommage, la synchronisation distribuée et les services de groupe. Tous ces types de services sont utilisés sous une forme ou une autre par des applications distribuées
  • Node.js : il est possible de développer les Chaincode (Smart Contracts) en javascript. Un container dédié est créé avec un thread node pour exécuter le code du contrat
  • Go : Go est le second langage supporté pour les smart contracts. Plusieurs bricks de la stack sont développées en Go
  • Java : il est également possible de développer les chain codes en java
Différence entre le ledger, le state (BDD CouchDB), et la blockchain
Contenu d’un block Hyperledger
  • Explorer : ce dashboard, branché à la chaine, permet d’observer le contenu du réseau, les blocks, les transactions, le nombres de transactions par secondes, …
Example d’interface Hyperledger Explorer
Example d’interface Hyperledger Explorer

Architecture du POC

Le POC est composé de différents éléments :

  • Une instance Hyperledger vierge : il s’agit d’images basiques issues du repo officiel ou de contributeurs de la fondation. Elle est paramètrée avec des certificats auto générés Oui.sncf
  • Un server Node.js : un SDK officiel Node est fourni afin de pouvoir communiquer avec la blockchain (appeler un smart contracts, lire des blocks, écouter la création de blocks,.. ). Il existe également des SDK Python, Java, Go
  • Un système de Push Notifications Web : pour demander la validation d’informations, le Chaincode (Smart Contract) appelle ce serveur qui pousse des notifications Web dans la PWA aux utilisateurs de l’application
  • Une PWA : application Web installable sur mobile, capable de recevoir des push même si elle n’est pas allumée
Fonctionnement App Blockchain
Détail Workflow

REX Complexité Hyperledger Fabric

  • Scaling des peers : ajouter des noeuds au réseau est complexe. En effet sans outil de déploiement, il est nécessaire de générer des clés/certificats (crypto materials) pour chaque nouveau noeuds, et d’injecter ces éléments dans les différents conteneurs (orderer, explorer, …). Ce nouveau noeud doit ensuite être accepté sur le réseau, et il faut installer les channels et smart contracts dessus. C’est donc relativement lourd.
  • Gestion des différentes clés de certificats : les clés générées doivent êtres accessibles depuis différents conteneurs. Il faut donc un volume sécurisé accessible, ce qui limite la capacité de déploiement sur plusieurs machines.
  • Configuration de la topologie : il manque encore cruellement l’intégration d’outils qui permettent la découverte des nœuds automatiquement type Consul. Cependant ces concepts arrivent dans la dernière version 1.2
  • Exemple usage SDK Node.js : la documentation du SDK Node est très complète, mais manque d’exemples d’utilisation. Les workflows étant complexes, ce manque est un problème à son utilisation vs le lancement de commandes en direct sur les noeuds. L’utilisation de Typescript est un point positif à souligner dans le SDK.
  • Stabilité : le système de Gossip génère parfois une forte utilisation CPU à des moments inattendus. Cela peut faire planter la machine si elle est sous dimensionnée.
  • Outil de visualisation des blocks, Explorer : cet outil permet de suivre la création de blocks ainsi que les transactions. On peut également voir le contenu des transactions, la liste des channels, le nombre de transactions par minutes, …
  • Utilisation de docker : docker n’est pas toujours bien exploité entre les différents projet. Il faut vraiment rentrer dans le code et les documentations pour customiser les configuration (sur l’explorer par exemple, il est nécessaire de modifier les configs pour que le système puisse rejoindre le réseau créé par la stack, ce n’est pas variabilisé).
  • Listener création de blocks : il est possible d’écouter l’ajout de blocks dans la chaine, notamment avec les SDK proposés. C’est une logique alternative au Chaincodes (Smart Contract), qui permet de déclencher des logiques métiers en fonction du contenu des blocks créés.
  • Qualité du code : du fait de sa création communautaire, la qualité du code n’est pas toujours au rendez-vous. Le projet Explorer par exemple contient des dépendances mélangées React / Material Design /Jquery / Bootstrap / … ce qui n’est pas forcément très heureux.

REX application blockchain avec push

  • L’ écriture et la lecture de blocks est assez simple avec les Chaincodes et le SDK.
  • La gestion des push grâce à un serveur maison, sans passer par des outils Google grâce à la norme W3C est assez simple.
  • L’exécution des transactions est assez rapide, et un outil de test de charge est disponible ici : https://github.com/hyperledger/caliper
  • La gestion du state du Ledger avec CouchDB permet de faire des requêtes complexes sur les objets injectés dans la chaine (objets JSON).
  • Les Chaincodes (Smart Contract) permettent de gérer librement des règles métiers complexes, le cadre est assez ouvert.

Un mot sur Cello

La fondation Hyperledger fournit différents outils pour aider à son usage ou installation. Cello permet de gérer le déploiement d’une stack vierge sur plusieurs serveurs. Nous l’avons testé, c’est encore assez instable, mais prometteur pour une utilisation industrialisée. L’idée est de remplir des server avec des stacks prêtes à l’emploi pour créer de nouvelles applications grâce à un dashboard :

Un mot sur Composer

Composer est une sur-couche proposée à Fabric. Elle permet de créer des Chaincodes de gestion d’assets dans une interface graphique. L’idée derrière est de ne pas avoir besoin de gérer la chaine, mais de se concentrer sur l’application grâce à des instances all inclusive proposées par IBM. Dans les fait, on se retrouve limité au cas d’usage de la gestion d’assets.

Cloud ready?

La stack Hyperledger est proposée par plusieurs cloud providers, preuve que la technologie est déployable. Attention cependant les documentations de déploiement sur Kubernetes ou autre orchestrateurs sont encore assez succinctes.

Côté fournisseurs, les plus aboutit sont IBM ou éventuellement chez Microsoft. Le template AWS est à proscrire, il s’agit d’un clone du repo gitlab officiel..

Hyperledger : promesse tenue ?

La stack propose plusieurs points fort :

  • Rapidité d’exécution
  • Packaging : le packaging Hyperledger a récemment été amélioré grâce à Docker compose. Le déploiement est donc relativement simple, bien qu’il nécessite une compréhension de chaque composant afin de créer une configuration de réseau adaptée au projet. Dans l’ensemble, il faut paramétrer beaucoup de fichiers de configuration différents voir redondant pour obtenir le réseau voulut.
  • Multi channel : ce système permet de séparer les blocks en fonction des thématiques et des acteurs. Le fait que les Peers soient associés à des Channels et validés par des organisations permettent de façon sécurisée de partager un réseau entre différents acteurs.
  • Multi acteurs
  • Sécurité innérante au système blockchain couplée aux certificats de la chaine privée
  • Debug : il est possible de lancer une version allégée de la chaine pour faciliter le debug notamment des Chaincodes
  • Qualité du code : malgré quelques composants comme Explorer, la base de Fabric est solide, et plusieurs entreprises comme Huawei, Airbus, Cisco, ont investit dans Fabric.

Autres stacks hyperledger

  • Iroha : blockchain platform implementation and one of the Hyperledger projects hosted by The Linux Foundation. Hyperledger Iroha is written in C++ incorporating unique chain-based Byzantine Fault Tolerant consensus algorithm, called Yet Another Consensus and the BFT ordering service
  • Sawtooth : modular platform for building, deploying, and running distributed ledgers
  • Burrow : is one of the Hyperledger projects hosted by The Linux Foundation. Hyperledger Burrow was originally contributed by Monax and co-sponsored by Intel. Hyperledger Burrow provides a modular blockchain client with a permissioned smart contract interpreter partially developed to the specification of the Ethereum Virtual Machine (EVM)
  • Indy : is a distributed ledger, purpose-built for decentralized identity. It provides tools, libraries, and reusable components for creating and using independent digital identities rooted on blockchains or other distributed ledgers so that they are interoperable across administrative domains, applications, and any other “silo.”

Exemple de Chaicode (Smart Contract)

'use strict';
const shim = require('fabric-shim');
const util = require('util');
const request = require('request-promise-native');
const baseUrl = 'https://blockchain-iv-back.lab-o.io'
const ivValidator = 2;
let Chaincode = class {
    // The Init method is called when the Smart Contract 'fabcar' is instantiated by the blockchain network
// Best practice is to have any Ledger initialization in separate function -- see initLedger()
async Init(stub) {
console.info('=========== Instantiated IV chaincode ===========');
console.info('Chaincode instantiation is successful');
return shim.success();
}
    // The Invoke method is called as a result of an application request to run the Smart Contract
// The calling application program has also specified the particular smart contract
// function to be called, with arguments
async Invoke(stub) {
let ret = stub.getFunctionAndParameters();
console.info(ret);
        let method = this[ret.fcn];
if (!method) {
console.error('no function of name:' + ret.fcn + ' found');
throw new Error('Received unknown function ' + ret.fcn + ' invocation');
}
try {
let payload = await method(stub, ret.params);
return shim.success(payload);
} catch (err) {
console.log(err);
return shim.error(err);
}
}
    async queryIV(stub, args) {
console.info('============= START : queryIV ===========');
if (args.length !== 1) {
throw new Error('Incorrect number of arguments. Expecting IV ex: 01-08-2018-DELAY-5555');
}
let IVNumber = args[0];
        let IVAsBytes = await stub.getState(IVNumber); //get the IV from chaincode state
if (!IVAsBytes || IVAsBytes.toString().length <= 0) {
throw new Error(IVAsBytes + ' does not exist: ');
}
console.log(IVAsBytes.toString());
console.info('============= END : queryIV ===========');
return IVAsBytes;
}
    async initLedger(stub, args) {
console.info('============= START : Initialize Ledger ===========');
console.info('============= END : Initialize Ledger ===========');
}
    async createIV(stub, args) {
console.info('============= START : Create IV ===========');
console.log('============= Args : ===========', args);
if (args.length !== 6) {
throw new Error('Incorrect number of arguments. Expecting 6');
}
        let simpleKey = args[0] + '-' + args[1] + '-' + args[2] + '-' + args[3];
        let IV = {
docType: 'IV',
date: args[0],
eventType: args[1],
trainNumber: args[2],
place: args[3],
pushed: args[4],
validators: [args[5]]
};
        // await stub.putState(key, Buffer.from(JSON.stringify(IV)));
await stub.putState(simpleKey, Buffer.from(JSON.stringify(IV)));
console.info('============= END : Create IV ===========');
console.info('============= START : Create IV Send Notification ===========');
let options = {
method: 'POST',
uri: baseUrl+'/notification',
body: {
title: 'Info Voyageur à valider',
message: 'Une nouvelle information voyageur est à valider.',
data: {
idIV: simpleKey,
IVTrain: args[2],
IVType: args[1],
IVDate: args[0],
IVPlace: args[3],
validators: [args[5]],
type: 'action'
}
},
json: true,
headers: {
'auth-secret': 'this_is_my_secret_key'
}
};
request(options)
.then(function (parsedBody) {
console.log('Success send notification', parsedBody)
console.info('============= END : Create IV Send Notification ===========');
})
.catch(function (err) {
console.log('Failed send notification', err)
console.info('============= END : Create IV Send Notification ===========');
});
}
    async queryAllIVs(stub, args) {
console.info('============= START : queryAllIVs ===========');
console.log('============= Args : ===========', args);
        let iterator = await stub.getStateByRange('', '');
        let allResults = [];
while (true) {
let res = await iterator.next();
            console.log('Response before JSON parse :', res);
            if (res.value && res.value.value.toString()) {
let jsonRes = {};
console.log(res.value.value.toString('utf8'));
                jsonRes.Key = res.value.key;
try {
jsonRes.Record = JSON.parse(res.value.value.toString('utf8'));
} catch (err) {
console.log(err);
jsonRes.Record = res.value.value.toString('utf8');
}
allResults.push(jsonRes);
}
if (res.done) {
console.log('end of data');
await iterator.close();
console.info(allResults);
return Buffer.from(JSON.stringify(allResults));
}
}
}
    async addIVValidator(stub, args) {
console.info('============= START : addIVValidator ===========');
if (args.length !== 2) {
throw new Error('Incorrect number of arguments. Expecting 2');
}
        let IVAsBytes = await stub.getState(args[0]);
console.log(IVAsBytes.toString());
        let IV = JSON.parse(IVAsBytes.toString());
try{
if(IV.validators.indexOf(args[1]) === -1){
IV.validators.push(args[1]);
await stub.putState(args[0], Buffer.from(JSON.stringify(IV)));
} else {
console.log('This user has already validate this IV');
}
} catch(error){
console.log(error)
}
console.info('============= END : addIVValidator ===========');
console.log('After : ', IV);
try{
if(IV.validators.length === ivValidator && (IV.pushed === 'false')){
console.info('============= START : addIVValidator Send Notification ===========');
let options = {
method: 'POST',
uri: baseUrl+'/notification',
body: {
title: 'Nouvelle info voyageur',
message: 'Nouvelle IV pour le train : ' + IV.trainNumber,
data: {
idIV: args[0],
IVTrain: IV.trainNumber,
IVType: IV.eventType,
IVDate: IV.date,
IVPlace: IV.place,
type: 'info'
}
},
json: true,
headers: {
'auth-secret': 'this_is_my_secret_key'
}
};
request(options)
.then(async function (parsedBody) {
console.log('Success send notification', parsedBody)
try {
IV.pushed = true;
await stub.putState(args[0], Buffer.from(JSON.stringify(IV)));
} catch (error) {
console.log(error)
}
console.info('============= END : addIVValidator Send Notification ===========');
})
.catch(function (err) {
console.log('Failed send notification', err)
console.info('============= END : addIVValidator Send Notification ===========');
});
}
} catch(error){
console.log(error)
}
}
};
shim.start(new Chaincode());

Exemples d’appels du SDK Node.js

Appel en écriture au Smart Contract
import * as path from 'path';
import Client = require('fabric-client');
import config from './config';
import {Organization, getClient, getOrderer, getPeers} from './client';
import {CreateIVDto} from "./types/IV/create-iv.dto";
import {ConfirmIVDto} from "./types/IV/confirm-iv.dto";
async function invokeTransactionCreateIVOnPeers(createIVDto: CreateIVDto) {
    const client = await getClient(Organization.ORG1);
const orderer = await getOrderer(client);
    console.log('Creating a Channel object ..');
const channel = client.newChannel(config.CHANNEL_NAME);
    console.log('Specifying the orderer to connect to ..');
channel.addOrderer(orderer);
    console.log('Getting the peers ..');
const peers = await getPeers(client, Organization.ORG1);
    peers.map(p => channel.addPeer(p));
    console.log('Initializing the channel ..');
await channel.initialize();
    console.log('Sending the Invoke Create IV Proposal ..', createIVDto);
const proposalResponse = await channel.sendTransactionProposal({
chaincodeId: config.CHAIN_CODE_ID,
fcn: 'createIV',
args: [createIVDto.date, createIVDto.eventType, createIVDto.trainNumber, createIVDto.place, 'false', createIVDto.validators.join(',')],
txId: client.newTransactionID()
});
    console.log('Sending the Create IV Transaction ..');
const transactionResponse = await channel.sendTransaction({
proposalResponses: proposalResponse[0],
proposal: proposalResponse[1]
});
    return transactionResponse;
}
Appel en lecture au Smart Contract
import * as path from 'path';
import Client = require('fabric-client');
import config from './config';
import { Organization, getClient, getOrderer, getPeers } from './client';
async function getChannel(client: Client, org: Organization): Promise<Channel> {
const orderer = await getOrderer(client);
  console.log('Creating a Channel object ..');
const channel = client.newChannel(config.CHANNEL_NAME);
  console.log('Specifying the orderer to connect to ..');
channel.addOrderer(orderer);
  console.log('Getting the peers ..');
const peers = await getPeers(client, org);
  peers.map(p => channel.addPeer(p));
  console.log('Initializing the channel ..');
await channel.initialize();
  return channel;
}
async function queryAllIVs() : Promise<string> {
const client = await getClient(Organization.ORG1);
const channel = await getChannel(client, Organization.ORG1);
    console.log(`Quering AllIVs the Chaincode on the peers of ${Organization.ORG1} ..`);
const response = await channel.queryByChaincode({
chaincodeId: config.CHAIN_CODE_ID,
fcn: 'queryAllIVs',
args: [],
txId: client.newTransactionID()
});
    console.log(`Peer0 of ${Organization.ORG1} has ${response[0].toString('utf8')} ..`);
console.log(`Peer1 of ${Organization.ORG1} has ${response[1].toString('utf8')} ..`);
return response[0].toString();
}
Installation du Smart Contract
import * as path from 'path';
import Client = require('fabric-client');
import { Organization, getClient, getOrderer, getPeers } from './client';
import config from './config';
async function installChaincodeOnPeers(org: Organization) {
  const client = await getClient(org);
const orderer = await getOrderer(client);
  console.log('Creating a Channel object ..');
const channel = client.newChannel(config.CHANNEL_NAME);
  console.log('Specifying the orderer to connect to ..');
channel.addOrderer(orderer);
  console.log('Getting the peers ..');
const peers = await getPeers(client, org);
  const proposalResponse = await client.installChaincode({
targets: peers,
chaincodeId: config.CHAIN_CODE_ID,
chaincodePath: __dirname + '/../../chaincode/ivsmartcontract/node',
chaincodeVersion: 'v0',
chaincodeType: 'node'
});
}
async function main() {
  await installChaincodeOnPeers(Organization.ORG1);
}
main();
Instantiation du Smart Contract
import * as path from 'path';
import Client = require('fabric-client');
import config from './config';
import { Organization, getClient, getOrderer, getPeers } from './client';
async function instantiateChaincodeOnPeers(org: Organization) {
  const client = await getClient(org);
const orderer = await getOrderer(client);
  console.log('Creating a Channel object ..');
const channel = client.newChannel(config.CHANNEL_NAME);
  console.log('Specifying the orderer to connect to ..');
channel.addOrderer(orderer);
  console.log('Getting the peers ..');
const peers = await getPeers(client, org);
  peers.map(p => channel.addPeer(p));
  console.log('Initializing the channel ..');
await channel.initialize();
  console.log('Sending the Instantiate Proposal ..');
const proposalResponse = await channel.sendInstantiateProposal({
chaincodeId: config.CHAIN_CODE_ID,
chaincodeVersion: 'v0',
fcn: 'Init',
args: [],
txId: client.newTransactionID(),
chaincodeType: 'node'
});
  console.log(JSON.stringify(proposalResponse));
  console.log('Sending the Transaction ..');
const transactionResponse = await channel.sendTransaction({
proposalResponses: proposalResponse[0],
proposal: proposalResponse[1]
});
}
async function main() {
  await instantiateChaincodeOnPeers(Organization.ORG1);
}
main();