En voyant ce titre, vous vous direz sans doute que je pars dans tous les sens et c’est vrai, j’aime expérimenter pleins de choses et j’aime écrire sur ce que je fais. J’aime le dev, l’ops, le devops (ba-dam bam!), le web, et j’en passe.

Le week-end particulièrement, j’aime bien me poser, réfléchir, me plonger dans un sujet, l'étudier à fond et en sortir un article avec comme deadline le dimanche soir. C’est mon petit défi et ça me permet d’apprendre plein de choses.

Ceci étant dit, je me lance dans cet article un peu atypique.

En premier lieu, la blockchain, c’est quoi ? Difficile à imaginer pour des humains comme nous, on n’est pas des bêtes en crypto ou en math (en tout cas pour ma part). Le truc, c’est que le principe de base est bien plus simple que ce que je ne croyais. Ce sont en fait les différentes applications qui peuvent être compliquées.

Dans cet article, j’explique avec mes mots ce qu’est une blockchain, puis je vous montre une démo que j’ai faite d’un modèle de blockchain simple en Python.

Qu’est-ce qu’une blockchain

Commençons par le début, tant qu'à faire.

Si je devais expliquer ce qu’est une blockchain avec mes mots et sans regarder Wikipedia, je dirais que c’est un support de stockage de données informatique logiciel sécurisé et durable, s’apparentant à une chaîne de blocs (quelle surprise), où chaque bloc contient de la donnée.

Pourquoi sécurisé? Parce qu’un bloc ne peut être corrompu sans corrompre toute la chaîne, son intégrité dépendant du bloc précédent. Pourquoi dans le temps ? Car l’historique est constamment conservé depuis le premier bloc. Pour exploiter une blockchain, il faut donc la posséder dans son entièreté.

Le monde connaît principalement la blockchain pour son application dans les cryptomonnaies comme le Bitcoin, mais les applications sont en fait illimitées, du fait qu’on peut y stocker tous types de données.

Un bloc possède cette structure :

bloc n
-------
index
previous hash
timestamp
data
nonce

Je vous explique les différents éléments de cette structure :

  • index : c’est le numéro de position du bloc dans la blockchain. Le bloc d’index 4 est après le bloc d’index 3, etc.
  • previous hash : c’est le hash du bloc précédent notre bloc. Le previous hash du bloc 4 est le hash du bloc 3.
  • timestamp : le timestamp de la création du bloc.
  • data : toute la donnée qu’on a décidé de mettre dans notre bloc.
  • nonce : il représente le nombre de tentatives effectuées avant que le bloc puisse être miné dans les limites des contraintes données.

Bon, jusqu’ici, pourquoi pas. Et donc une blockchain, c’est une chaîne de ces blocs, merci cap’tain obvious 😄

Cela fonctionne de la façon suivante : la chaîne est initiée à partir d’un bloc de genèse dont le hash est nul. C’est un bloc factice miné immédiatement pour avoir un hash correct et démarrer ainsi la chaîne. On considère donc que le premier bloc est valide si :

  • son index est 0
  • son previous hash est inexistant
  • si son hash est existant et valide

Le second membre ainsi que les suivants sont valides si :

  • de l’incrémentation de l’index du bloc n-1 résulte l’index du bloc n
  • le hash du bloc n-1 existe et est valide
  • le previous hash du bloc n est égal au hash du bloc n-1

Donc, chaque bloc n-1 valide le bloc n par son hash, sécurisant ainsi toute la chaîne. Par conséquence, une blockchain est valide à seulement deux conditions :

  • si le premier bloc est valide
  • si chaque bloc est valide, via l'énoncé précédent

Et le minage dans tout ça ? C’est le fait de calculer le bloc suivant en utilisant une fonction de hash et une “difficulté” définie et fixe, que l’on retrouvera en début de notre hash.

Le hash de chaque bloc est lui calculé par une fonction prenant en arguments tous les éléments du bloc précédent :

  • index
  • previous hash (par conséquence le hash du bloc n-2 par rapport au bloc que l’on essaie de déterminer)
  • timestamp
  • data
  • nonce

Vous voyez maintenant pourquoi c’est si sécurisé, et surtout pourquoi on dit qu’une blockchain grandit exponentiellement dans le temps ?

Chaque bloc reprend tous les éléments du bloc précédent, rien que dans un seul de ses champs 😨

Vicieux, non?

J’espère avoir fait aussi clair que possible. Maintenant, la démo en python.

Le modèle en Python

Il s’agit là d’une représentation d’une blockchain, afin d’en expliquer les concepts.

En premier lieu, j’importe quelques outils et définit deux fonctions :

from hashlib import sha256
from datetime import datetime

def calculateHash(block):
    bloc = str(block.index) + str(block.previousHash) + str(block.timestamp) + str(block.data) + str(block.nonce)
    return(sha256(bloc.encode('utf-8')).hexdigest())

def repeat(string, length):
    return(string * (int(length/len(string))+1))[:length]

Les imports sont :

  • la fonction de calcul sha256 provenant de hashlib
  • datetime dont je vais utiliser la méthode now() pour le timestamp de mes blocs

Aucune installation de module nécessaire, ces deux-là sont présents dans la Standard Library de Python.

Les fonctions :

  • Calcul du hash d’un bloc
  • Un bête repeat de string (repeat("tata", 5) donnera tatatatatatatatatata)

sha256() ne sait travailler que sur un bloc Unicode, d’où l’encodage à la volée.

Si j’ai défini ces fonctions en dehors d’une classe, c’est parce qu’elles ne sont pas propres à une des deux classes suivantes.

J'écris deux classes pour ce modèle, l’une représentant un bloc, l’autre représentant une blockchain.

La classe Block

Commençons par la classe Block, la plus simple des deux. C’est donc celle qui modélise un bloc de la blockchain avec tous ses attributs.

Le constructeur prend donc en arguments tous les attributs d’un bloc :

class Block(object):
    def __init__(self, index, previousHash, timestamp, data):
        self.index = index
        self.previousHash = previousHash
        self.timestamp = timestamp
        self.nonce = 0
        self.hash = calculateHash(self)

Avouez que le self.hash vous retourne la gueule 😄

J’indique donc en paramètre du constructeur tout ce qui constitue un bloc, sauf le nonce.

La seule méthode de ma classe Block est mineBlock, qui va déterminer le bloc suivant. Je vais donc revenir un peu sur le minage. Ça consiste à calculer différentes valeurs de signatures SHA-256 pour le bloc afin de le déterminer en fonction du précédent et de ses données.

Ma valeur pour nonce est zéro pour le moment mais va évoluer, car à chaque bloc j’aurais un nouveau nombre de tentatives. Je le réinitialise donc à chaque fois qu’un minage est nécessaire. Partant de là, je vais miner avec des hash sur la difficulté désirée et jusqu'à tomber sur une valeur correcte, et donc incrémenter mon nonce à chaque tentative.

La méthode :

    def mineBlock(self, difficulty):
        zeros = repeat("0", difficulty)
        self.nonce = 0
        while self.hash[0:difficulty] != zeros:
            self.nonce += 1
            self.hash = calculateHash(self)

La classe Block complète :

class Block(object):
    def __init__(self, index, previousHash, timestamp, data):
        self.index = index
        self.previousHash = previousHash
        self.timestamp = timestamp
        self.nonce = 0
        self.hash = calculateHash(self)

    def mineBlock(self, difficulty):
        zeros = "0".repeat(difficulty)

        while self.hash[0:difficulty] != zeros:
            self.nonce = self.nonce + 1
            self.hash = calculateHash(self)

La classe Blockchain

Le constructeur de ma blockchain est en théorie simple, sauf qu’il se doit également de générer le premier bloc : je ne peux pas créer une blockchain vide, pour toutes les raisons énoncées précédemment.

Ma blockchain est une liste de blocs (comme c’est poétique), et mon constructeur prend comme unique argument la “difficulté”.

class Blockchain(object):
    def __init__(self, difficulty):
        self.difficulty = difficulty
        self.blocks = []

Le premier bloc est miné depuis un bloc que je définis manuellement. Pourquoi ? Parce que le minage est nécessaire pour obtenir un hash et donc un bloc valide, comme expliqué plus haut. Je génère donc un bloc avec un hash nul qui va servir de support, en quelque sorte.

        genesisBlock = Block(0, None, datetime.now(), "Genesis block")

A partir de ce bloc Genesis, je mine un bloc valide qui lui sera le premier de ma chaine.

        genesisBlock.mineBlock(self.difficulty)

Puis j’ajoute le bloc résultant comme premier bloc de ma blockchain.

        self.blocks.append(genesisBlock)

Le constructeur complet :

class Blockchain(object):
    def __init__(self, difficulty):
        self.difficulty = difficulty
        self.blocks = []
    
        genesisBlock = Block(0, None, datetime.now(), "Genesis block")
        genesisBlock.mineBlock(self.difficulty)
        self.blocks.append(genesisBlock)

Première méthode maintenant, celle de la création d’un bloc. Comme dit précédemment, l’ajout d’un bloc à la blockchain passe par deux étapes.

  1. Création du bloc
  2. Minage puis ajout du bloc à la blockchain

J’ai séparé ça en deux méthodes. Pour créer le bloc, j’utilise la classe Bloc et deux données du précédent bloc, l’index et le hash, parce que l’index du nouveau bloc est l’index n-1 incrémenté. En argument, la donnée que je veux mettre dans ce bloc.

    def newBlock(self, data):
        latestBlock = self.blocks[-1]
        return(Block(latestBlock.index + 1, latestBlock.hash, datetime.now(), data))

Il est donc nécessaire de miner ce bloc en fonction des données du bloc précédent afin de lui donner un hash valide et donc une intégrité vis-à-vis du reste de la blockchain, ce qui m’amène à la méthode addBlock. Je mine mon bloc et l’ajoute à la suite de la liste qui représente ma blockchain.

    def addBlock(self, block):
        block.mineBlock(self.difficulty)
        self.blocks.append(block)

Et voilà, on a fait le plus dur. Maintenant, j’ajoute simplement quelques méthodes pour tester/valider ma blockchain et ses membres.

La première chose à valider est le premier bloc. Ce dernier ne valide pas toutes les conditions d’un bloc traditionnel car il n’a pas de bloc parent, son champ previous hash est donc nul.

Pour qu’il soit valide, il suffit donc que :

  • son index soit 0
  • son previous hash soit nul
  • mais que son hash soit quand même valide
    def isFirstBlockValid(self):
        firstBlock = self.blocks[0]

        if firstBlock.index != 0:
            return False
        
        if firstBlock.previousHash is not None:
            return False

        if (firstBlock.hash is None or calculateHash(firstBlock) != firstBlock.hash):
            return False

        return True

Passons ensuite à la validation d’un bloc traditionnel. Pour qu’un bloc soit valide, il doit respecter les conditions vues au début de cet article, petit rappel :

  • index du bloc n-1 + 1 = index du bloc n
  • le hash du bloc précédent existe et il est égal au previous hash présent dans le bloc n
  • son hash est valide, c’est à dire que le calcul du hash du bloc n est bien égal à ce hash

La méthode isValidBlock prend donc en argument un bloc et le bloc précédent.

    def isValidBlock(self, block, previousBlock):
        if previousBlock.index+1 != block.index:
            return False

        if (block.previousHash is None or block.previousHash != previousBlock.hash):
            return False
        
        if (block.hash is None or calculateHash(block) != block.hash):
            return False
        
        return True

La dernière chose qui doit être validée, partant de là, est la blockchain elle-même. Une blockchain est valide à deux conditions :

  • Si son premier bloc est valide
  • Si tous ses blocs sont valides

Un peu obvious, non ? La subtilité, c’est que pour vérifier les blocs, il faut partir de l’index 1 et non 0, le premier bloc ne pouvant être validé par les mêmes conditions que les blocs suivants.

La méthode suivante valide donc le premier bloc, puis boucle sur l’ensemble de la liste des blocs formant la blockchain afin de les valider un par un.

    def isBlockchainValid(self):
        if not self.isFirstBlockValid():
            return False
        
        for i in range(1, len(self.blocks)):
            previousBlock = self.blocks[i-1]
            block = self.blocks[i]
            if not self.isValidBlock(block, previousBlock):
                return False 

        return True

Dernière méthode avant de vous exposer le tout, une méthode permettant d’afficher la blockchain résultant de notre modèle. Je vous préviens, c’est un peu sale et je n’en suis pas fier, mais ça fait un affichage sexy. Notez que j’affiche en plus, pour l’exemple, le hash du bloc présent.

    def display(self):
        for block in self.blocks:
            chain = "Block #"+str(block.index)+" ["+"\n\tindex: "+str(block.index)+"\n\tprevious hash: "+str(block.previousHash)+"\n\ttimestamp: "+str(block.timestamp)+"\n\tdata: "+str(block.data)+"\n\thash: "+str(block.hash)+"\n\tnonce: "+str(block.nonce)+"\n]\n"
            print(str(chain))

Il ne reste qu'à tester le tout avec un code somme toute simple. Dans cet exemple, je positionne une difficulté de 4, donc 4 zéros en début de chaque hash. Dans les blocs, du texte tout simple comme donnée. Je valide la blockchain puis je l’affiche.

if __name__ == '__main__':
    bchain = Blockchain(4)

    blockn1 = bchain.newBlock("Second Block")
    bchain.addBlock(blockn1)

    blockn2 = bchain.newBlock("Third Block")
    bchain.addBlock(blockn2)

    blockn3 = bchain.newBlock("Fourth Block")
    bchain.addBlock(blockn3)

    print("Blockchain validity:", bchain.isBlockchainValid())

    bchain.display()

Ce qui va donne au moment où j'écris ces lignes :

Blockchain validity: True
Block #0 [
	index: 0
	previous hash: None
	timestamp: 2019-12-22 12:05:47.902654
	data: Genesis block
	hash: 000024654ea02bfefcb159df0701373b92997a680d3a0358a3a9e7d065a54795
	nonce: 74766
]

Block #1 [
	index: 1
	previous hash: 000024654ea02bfefcb159df0701373b92997a680d3a0358a3a9e7d065a54795
	timestamp: 2019-12-22 12:05:48.074296
	data: Second Block
	hash: 000082fd8ea8de4fc0e289107e31bb4849cd13b0832ba34df50660652afde877
	nonce: 92187
]

Block #2 [
	index: 2
	previous hash: 000082fd8ea8de4fc0e289107e31bb4849cd13b0832ba34df50660652afde877
	timestamp: 2019-12-22 12:05:48.283522
	data: Third Block
	hash: 000074feb9ce0825ceed58646f5a594ad030120652039c3fe0e3d7131a11b035
	nonce: 6316
]

Block #3 [
	index: 3
	previous hash: 000074feb9ce0825ceed58646f5a594ad030120652039c3fe0e3d7131a11b035
	timestamp: 2019-12-22 12:05:48.297757
	data: Fourth Block
	hash: 000044c70013ab83cc2c7a360b660aedd0aa81e568023a1d9e0e167ef0991eb2
	nonce: 235300
]

Voilà, une blockchain toute belle et valide. Vous noterez que :

  • le champ nonce recense bien les tentatives jusqu'à arriver à un hash valide
  • nous avons bien 4 zéros en début de chaque hash
  • le previous hash du bloc #0 est nul
  • à partir du bloc #1, chaque previous hash est bien égal au hash du bloc précédent

Je vous laisse évidemment le code complet à cette adresse, et j’espère que cet article vous aura plu.

Note : Exercice fun à partir de là : reprendre le code et insérer manuellement un faux bloc, puis retester la validité de la blockchain!

Si je n'écris pas un édito d’ici là, passez de bonnes fêtes ! 🎉 🎄