Outils pour utilisateurs

Outils du site


nsi:tds:cryptographie:blockchain

Block Chain

Vous pourrez utiliser le module rsa.py qui contient ce qu'il faut pour chiffrer et déchiffrer des messages de taille arbitraire en rsa.

Fiche Wikipedia

Une blockchain, est une technologie de stockage et de transmission d'informations sans organe de contrôle. Comme pour le réseau internet devant fonctionner de façon décentralisée, la blockchain distribue ses données en divers point tout en garantissant leur authenticité.

Contrairement à la base de données classique, sur un serveur, sous le contrôle d'un propriétaire, dans le cas d'une blockchain, tous les participants possèdent une copie de la chaîne et l'algorithme permet d'obtenir un consensus pour toute modification.

Preuve de travail

Une blockchain met en œuvre le problème des généraux byzantins : des généraux de l'armée byzantine campent autour d'une cité ennemie. Ils ne peuvent communiquer qu'à l'aide de messagers et doivent établir un plan de bataille commun, faute de quoi la défaite sera inévitable. Cependant certains de ces généraux peuvent être des traîtres, qui essayeront donc de semer la confusion parmi les autres. Le problème est donc de trouver un algorithme pour s'assurer que les généraux loyaux arrivent tout de même à se mettre d'accord sur un plan de bataille.

L'ensemble des participants à la blockchain doivent maintenir la fiabilité même en cas de défaillance ou de piratage sur part minoritaire des participants. La solution à ce problème est la preuve de travail. Il s'agit d'un problème mathématique dont la solution permet de vérifier que le mineur a bien réalisé un travail. La résolution de la preuve nécessite une puissance de calcul élevée. Les mineurs fournissent cette puissance et doivent prouver leur travail.

Les calculs nécessaires pour le minage ont un coût énergétique important et devient un désastre environnemental. Dans le cas des bitcoins, seul le mineur ayant obtenu le premier un résultat reçoit un paiement. Les mineurs sont en concurrences mais doivent être solidaires car le problème devient plus ardu quand il y a beaucoup de mineurs. L'idée est d'empêcher qu'un groupe minoritaire prenne des décisions. D'autres approches existent. Le Burstcoin utilise une preuve de stockage qui a une faible consommation énergétique.

L'aspect décentralisé d'une blockchain est très important, et donc la façon d'obtenir un consensus l'est également. Nous n'étudierons pourtant pas cet aspect. Nous envisagerons ici une chaîne sur une machine unique et nous allons voir comment on peut garantir son authenticité.

Transaction et bloc

Une transaction est une opération consistant à modifier l'état de la chaîne, c'est à dire à ajouter des données.

Les transactions enregistrées sont regroupées en blocs. Quand un bloc est créé, les mineurs analyse l'historique complet de la chaîne. Si le bloc est valide, il est horodaté et ajouté à la chaîne.

Un bloc contient :

  • des transactions
  • une somme de contrôle – empreinte, hachage
  • la somme de contrôle du bloc précédent
  • une mesure de la quantité de travail qui a été nécessaire pour produire le bloc

Implémentation

Nous allons faire l'implémentation d'une chaîne représentant l'évolution des comptes d'un certain nombre d'utilisateurs. Pour faire plus simple, les utilisateurs seront connus dès le début.

Chaque utilisateur aura :

  • un nom,
  • un compte en unité de la monnaie de la blockchain,
  • une paire de clés Kpu, Kpr.

La blockchain ne connaît que le clé publique Kpu de chaque utilisateur. Les utilisateurs doivent conserver secrètement leur clé privée Kpr.

Les seules transactions envisagées sont des transaction ou un utilisateur A donne à un utilisateur B une quantité de monnaie.

Blocs

Pour implémenter un bloc, vous pouvez par exemple utiliser une classe Block.

Un objet Block devra, pour être créé, recevoir :

  • empreinte, hachage du bloc précédent,
  • un tableau contenant les informations nécessaires.

On doit distinguer deux situations :

  • le bloc initial contenant les informations sur les utilisateurs et servant d'amorce à la chaîne,
  • les blocs suivants contenant les échanges.

Bloc initial

Comme ce bloc n'a pas de bloc avant lui, et donc pas d'empreinte de bloc précédent, on peut le créer en lui fournissant "0" à la place de l'empreinte du bloc précédent.

Les informations qu'on lui donne on la forme (nom, compte, Kpu). La clé, suivant le protocole RSA, est en deux morceaux. Par exemple :

[
  ("Paul", 45, 11758971846281690449, 65537),   # Kpr = (11758971846281690449, 3602673231362159633)
  ("Judith", 92, 16218573123863373407, 65537), # Kpr = (16218573123863373407, 6385025328607971353)
  ("Michel", 52, 15421843742480106653, 65537), # Kpr = (15421843742480106653, 1380358199906286977)
  ("Laure", 39, 11452260091047556889, 65537),  # Kpr = (11452260091047556889, 32502561540434945)
]

Blocs d'échanges

Les blocs ordinaires recevront l'empreinte (hachage) du bloc précédent et les informations de transaction.

Pour simplifier, on supposera qu'il n'y a qu'une transaction dans un tel bloc. Une transaction prend la forme suivante :

("Paul", "Judith", 5, S)
  • Paul est la source (il donne de crédits)
  • Judith est la cible (elle reçoit les crédits)
  • 5 est la quantité de crédits échangés
  • S est la signature permettant d'authentifier la transaction (vérifie que Paul en est l'auteur).

Voir plus bas pour le fonctionnement de la signature.

Le bloc s'achève avec l'empreinte (hachage) de l'ensemble des informations qu'il contient :

  • empreinte du bloc précédent,
  • données (initialisation pour le bloc 0 et transaction pour les autres)

On utilise le module hashlib pour le hachage.

import hashlib # module de fonctions de hachage

# exemple de hachage d'une donnée de type bytes
b = "tralala".encode('utf8')
h = hashlib.sha256(b)
# on peut produire une écriture hexadécimale de h
h_hex = h.hexdigest()
# on produire un écriture en bytes
h_bytes = h.digest()

On peut indiquer l'empreinte de chaque bloc dans sa version hexdigest, c'est du texte et c'est plus simple.

Signature

Dans la transaction ("Paul", "Judith", 5, S), la signature S permet d'être certain que Paul est bien à l'origine de la transaction.

Voyons comment la signature sera crée :

  • Paul veut donner 5 unité à Judith. On crée le texte T = "Paul;Judith;5"
  • on encode le texte et on hache. On utilise pour cela hashlib :
    z = hashlib.sha256(T.encode('utf8'))
  • on calcule la signature Sb par chiffrement de z en utilisant la clé privée de Paul :
    S = rsa.cypher(z, Kpr)
    on utilise rsa.py correspondant à ce que l'on a vu dans le cours RSA
  • Sb est de type bytes. On préfère une écriture texte hexadécimale. On peut calculer S = Sb.hex().
  • la transaction est alors complétée : ("Paul", "Judith", 5, S)

Dans l'exemple, la signature est :

'12d1cc8b27164fdc440bde8190347fe0484489bf9e04e71f2cd6edf182910dd493c02c68bf01fa752a297fa34c358d6a451deffc77df39b3'

Au moment de la vérification, on reçoit (par exemple) ("Paul", "Judith", 5, S).

  • comme dans ce qui précède, on produit le texte T = "Paul;Judith;5",
  • on hache ce texte : z = hashlib.sha256(T.encode('utf8'))
  • on cherche la version bytes de S : Sb = bytes.from_hex(S)
  • on déchiffre Sb avec la Kpu de Paul : z2 = rsa.decypher(Sb, Kpu)
  • on compare si z == z2. Si oui, la signature est valide.

Sauvegarde

Chaque bloc sera sauvegardé sous forme d'un fichier.

Le fichier aura pour nom l'empreinte du bloc précédent suivi de l'extension .blc

Dans le cas du bloc initial, le bloc contiendra simplement les données. Par exemple :

Paul;45;11758971846281690449;65537
Judith;92;16218573123863373407;65537
Michel;52;15421843742480106653;65537
Laure;39;11452260091047556889;65537

Dans le cas d'un bloc d'échange, le bloc contiendra la transaction. Par exemple ("Paul", "Judith", 5, S). Les 3 premiers éléments ne posent pas de problème. Pour S on voudrait une écriture pas trop lourde. On adopte une écriture hexadécimale : il suffit d'écrire S.hex() pour l'obtenir.

Le fichier contient donc : nom de la source;nom de la cible;str de value;hex de S

chargement

Si je sais que le dernier bloc à l'empreinte E, alors je peux chercher un fichier <E>.blc. Si le fichier existe, on peut l'ouvrir et récupérer les différents champs :

  • nom de la source
  • nom de la cible
  • value que l'on peut convertir en int
  • signature qui est sous forme hex et que l'on peut passer en bytes ainsi : S = bytes.fromhex(…)

Chaîne

Créons une classe BlockChain.

  • L'objet BlockChain, à la création, va chercher l'existence d'un fichier 0.blc pour s'amorcer.
    Le fichier 0.blc contient les données des comptes utilisateurs (exemple donné plus haut)
  • chaque fois que la blockchain charge un bloc, elle lit l'empreinte de ce bloc et cherche s'il existe un fichier nommer d'après cette empreinte.
    Si oui, la blockchain charge ce bloc et ainsi de suite.

Le bloc initial indique le compte initial des utilisateurs. Les transactions indiquent des échanges d'un utilisateur à l'autre.

Ainsi, si Paul commence à 45, Judith à 92 et que Paul donne 5 à Judith, alors Paul aura 40 et Judith 97.

On peut garder l'état des différents comptes avec un dictionnaire qui devra se créer automatiquement à la lecture de 0.blc :

comptes = {
    "Paul": 45,
    "Judith": 92,
    "Michel":52,
    "Laure":39
}

La blockchain, au gré des transactions, met à jour les valeurs des différents comptes.

Il pourra être utile aussi de créer, à la lecture de 0.blc, un dictionnaire pour les clés :

keys = {
    "Paul": (11758971846281690449, 65537),
    "Judith": (16218573123863373407, 65537),
    "Michel": (15421843742480106653, 65537),
    "Laure": (11452260091047556889, 65537)
}

ajout de transaction

La classe BlockChain doit posséder une méthode pour ajouter une transaction.

Une transaction est composée des informations : identifiant source, identifiant cible, valeur de la transaction, signature.

la blockchain doit :

  • vérifier que les identifiants existent,
  • vérifier la signature,
  • vérifier que la source dispose d'assez de crédits pour la transaction envisagée,

On convient que la fonction renvoie False si les vérifications échouent. Autrement on peut poursuivre en exécutant la transaction :

  • mise à jour des comptes,
  • création du bloc correspondant,
  • ajout du bloc à la suite des autres,
  • sauvegarde du bloc

et dans ce cas la fonction renvoie True.

Proposition d'implémentation

Il faut utiliser, comme déjà dit, rsa.py

# blockchain.py
import hashlib
import os
import rsa

class Block:
    def __init__(self, empreinte_precedent:str, data):
        """
        empreinte_precedent: empreinte du bloc précédent. "0" si premier bloc
        data: tableau donnant la liste des données à prendre en compte
        """
        self.data = data
        self.empreinte_precedent = empreinte_precedent
    
    def empreinte(self) -> str:
        """
        renvoie l'empreinte du bloc courant
        """
        # mettre empreinte_precedent,
        # suivi de la version texte de data (__str__ ci dessous)
        # encoder en utf8 pour obtenir un bytes b
        # faire le hash  avec hashlib.sha256(b)
        # renvoyer le hexdigest du hash obtenu
        
    def __str__(self) -> str:
        """
        renvoie une version texte de data
        """
        # pas besoin d'ajouter les empreintes
    
    def save(self):
        """
        sauvegarde le bloc dans un fichier dont le nom est l'empreinte
        du bloc précédent suivit de l'extension .blc
        """
        # le fichier doit contenir le contenu de __str__
        # si le fichier existe déjà, il ne faut rien faire
        
    def next_filename(self) -> str:
        """
        renvoie le nom de fichier du bloc suivant
        """
        return self.FOLDER + self.empreinte() + self.EXT    


class BlockChain:
    def __init__(self):
        """
        lance la procédure de chargement pour construire la blockchain
        """
        self.blocs = []
        self.keys = {}
        self.credits = {}
        self.load()

    def load(self):
        """
        charge la blockchain selon les fichiers
        """
        # créer une liste self.blocs vide
        # cherche le fichier 0.bloc qui est l'amorce
        # ce fichier contient :
        #   0
        #   les infos des comptes sous la forme : identifiant:str;credit:int;n:int;e:int
        #   empreinte du bloc
        # créer le bloc0 avec les infos du fichier mettre bloc0 dans self.blocs
        # créer un self.keys = { identifiant: (n, e) } pour stocker les clés des comptes
        # créer un self.credits = { identifiant: credit } pour stocker les crédits des comptes
        # à partir de là, tant que l'empreinte du dernier bloc correspond à un fichier,
        # charger le fichier, lire la transaction, l'ajouter à la chaîne
    
        # pour info, on peut obtenir les noms de fichiers en faisant :
        # fichiers = [f for f in os.listdir('./') if f.endswith('.blc')]
        
    def add_transaction(id_source:str, id_cible:str, value:int, s:str) -> bool:
        """
        id_source: identifiant de la source de l'échange
        id_cible: identifiant de la cible de l'échange
        value: quantité à transférer de id_source à id_cible
        s: signature pour authentifie la transaction
        renvoie True si la transaction a réussi, False sinon
        """
        # vérifie que source et cible existent bien
        # vérifie la signature
        # vérifie si le compte source a assez de crédits
        # modifie l'état des comptes concernés
        # crée un bloc pour la transaction
        #   en indiquant l'empreinte du dernier bloc pour l'empreinte précédente
        # ajoute le bloc dans la liste des blocs
        # sauvegarder le bloc

    def empreinte_transaction(self, id_source:str, id_cible:str, value:int) -> bytes:
        """
        id_source: identifiant source
        id_cible: identifiant cible
        value: quantité  à transférer
        renvoie l'empreinte sous forme digest
        """
        # fabrique le texte formé de la façon id_source;id_cible;value
        # encode, hash
        # renvoie digest

    def credit(self, identifiant:str) -> int:
        """
        renvoie la valeur de crédit pour identifiant
        """

Il faudra donc créer manuellement un bloc initial 0.bloc contenant le texte :

Paul;45;11758971846281690449;65537
Judith;92;16218573123863373407;65537
Michel;52;15421843742480106653;65537
Laure;39;11452260091047556889;65537

On a besoin de simuler un client agissant sur la blockchain (un des utilisateurs). Si par exemple Paul veut faire une transaction, il faut qu'il calcule la signature. On va automatiser tout cela.

# client.py

import hashlib
import rsa

class Client:
    def __init__(self, identifiant:str, Kpr):
        """
        identifiant: identifiant du compte
        Kpr: clé privée (n,d)
        """
        self.identifiant = identifiant
        self.Kpr = Kpr
    
    def transaction(self, id_cible:str, value:int):
        """
        renvoie la transaction signée
        """
        # la transaction signée est formée ainsi :
        # (id_source, id_cible, value, signature)
        # pour le calcul de la signature, on reprend le principe
        # rencontré dans BlockChain.empreinte_transaction :
        #   fabrique le texte formé de la façon id_source;id_cible;value
        #   encode, hash, digest
        #   calcule rsa.cypher sur le digest avec Kpr
        # le résultat est la signature

On pourra par montrer le fonctionnement de la façon suivante :

# demo.py

from blockchain import BlockChain
from client import Client

b = BlockChain() # charge le fichier 0.bloc
paul = Client("Paul", (11758971846281690449, 3602673231362159633))
judith = Client("Judith", (16218573123863373407, 6385025328607971353))

print(f"Le compte actuel de Paul est de {b.credit('Paul')}")
print(f"Le compte actuel de Judith est de {b.credit('Judith')}")
t = paul.transaction("Judith", 5)
success = b.add_transaction(*t) # * pour ventiler les morceaux du tuple t dans les arguments

if success:
    print("Transaction effectuée.")
    # la transaction a été enregistrée et sera donc pris en compte
    # à la prochaine exécution du script
else:
    print("Échec de la transaction.")
nsi/tds/cryptographie/blockchain.txt · Dernière modification : de goupillwiki