Création d'une crypto-monnaie dans le langage de programmation Crystal
Publié: 2022-03-11Cet article est ma tentative de comprendre les aspects clés de la blockchain en explorant les éléments internes. J'ai commencé par lire le livre blanc original sur le bitcoin, mais je pensais que la seule façon de vraiment comprendre la blockchain était de créer une nouvelle crypto-monnaie à partir de zéro.
C'est pourquoi j'ai décidé de créer une crypto-monnaie en utilisant le nouveau langage de programmation Crystal, et je l'ai surnommée CrystalCoin . Cet article ne traitera pas des choix d'algorithmes, de la difficulté de hachage ou de sujets similaires. Au lieu de cela, l'accent sera mis sur le détail d'un exemple concret, qui devrait fournir une compréhension plus approfondie et pratique des forces et des limites des blockchains.
Si vous ne l'avez pas encore lu, pour plus d'informations sur les algos et le hachage, je vous suggère de jeter un œil à l'article de Demir Selmanovic Cryptocurrency for Dummies: Bitcoin and Beyond.
Pourquoi j'ai choisi le langage de programmation Crystal
Pour une meilleure démonstration, j'ai voulu utiliser un langage productif comme Ruby sans compromettre les performances. La crypto-monnaie a de nombreux calculs chronophages (à savoir l'extraction et le hachage ), et c'est pourquoi les langages compilés tels que C++ et Java sont les langages de choix pour créer de "vraies" crypto-monnaies. Cela étant dit, je voulais utiliser un langage avec une syntaxe plus propre afin que je puisse garder le développement amusant et permettre une meilleure lisibilité. Les performances du cristal ont tendance à être bonnes de toute façon.
Alors, pourquoi ai-je décidé d'utiliser le langage de programmation Crystal ? La syntaxe de Crystal est fortement inspirée de celle de Ruby, donc pour moi, c'est naturel à lire et facile à écrire. Il présente l'avantage supplémentaire d'une courbe d'apprentissage plus faible, en particulier pour les développeurs Ruby expérimentés.
Voici comment l'équipe de Crystal lang le présente sur son site officiel :
Rapide comme C, lisse comme Ruby.
Cependant, contrairement à Ruby ou JavaScript, qui sont des langages interprétés, Crystal est un langage compilé, ce qui le rend beaucoup plus rapide et offre une empreinte mémoire plus faible. Sous le capot, il utilise LLVM pour compiler en code natif.
Crystal est également typé statiquement, ce qui signifie que le compilateur vous aidera à détecter les erreurs de type au moment de la compilation.
Je ne vais pas expliquer pourquoi je considère le langage Crystal comme génial car il dépasse le cadre de cet article, mais si vous ne trouvez pas mon optimisme convaincant, n'hésitez pas à consulter cet article pour un meilleur aperçu du potentiel de Crystal.
Remarque : cet article suppose que vous avez déjà une compréhension de base de la programmation orientée objet (POO).
Chaîne de blocs
Alors, qu'est-ce qu'une blockchain ? Il s'agit d'une liste (chaîne) de blocs liés et sécurisés par des empreintes digitales (également appelées hachages cryptographiques).
La façon la plus simple de le considérer est comme une structure de données de liste chaînée. Cela étant dit, une liste chaînée ne nécessitait qu'une référence à l'élément précédent ; un bloc doit avoir un identifiant dépendant de l'identifiant du bloc précédent, ce qui signifie que vous ne pouvez pas remplacer un bloc sans recalculer chaque bloc qui vient après.
Pour l'instant, considérez la blockchain comme une série de blocs avec des données liées à une chaîne, la chaîne étant le hachage du bloc précédent.
La chaîne de blocs entière existerait sur chaque nœud qui veut interagir avec elle, ce qui signifie qu'elle est copiée sur chacun des nœuds du réseau. Aucun serveur ne l'héberge, mais toutes les sociétés de développement de blockchain l'utilisent, ce qui le rend décentralisé .
Oui, c'est bizarre par rapport aux systèmes centralisés conventionnels. Chacun des nœuds disposera d'une copie de l'intégralité de la blockchain (> 149 Go dans la blockchain Bitcoin d'ici décembre 2017).
Hachage et signature numérique
Alors, quelle est cette fonction de hachage ? Considérez le hachage comme une fonction, qui renvoie une empreinte digitale unique lorsque nous lui donnons un texte/objet. Même le plus petit changement dans l'objet d'entrée changerait radicalement l'empreinte digitale.
Il existe différents algorithmes de hachage, et dans cet article, nous utiliserons l'algorithme de hachage SHA256
, qui est celui utilisé dans Bitcoin
.
En utilisant SHA256
, nous obtiendrons toujours une longueur de 64 caractères hexadécimaux (256 bits) même si l'entrée est inférieure à 256 bits ou bien supérieure à 256 bits :
Contribution | Résultats hachés |
---|---|
TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG TEXTE TRÈS LONG | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
Toptal | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
Topal. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
Notez avec le dernier exemple, qu'il suffit d'ajouter un .
(point) a entraîné un changement radical du hachage.
Par conséquent, dans une blockchain, la chaîne est construite en passant les données du bloc dans un algorithme de hachage qui générerait un hachage, qui est lié au bloc suivant, formant désormais une série de blocs liés aux hachages des blocs précédents.
Construire une crypto-monnaie en cristal
Commençons maintenant à créer notre projet Crystal et construisons notre cryptage SHA256
.
En supposant que le langage de programmation Crystal est installé, créons le squelette de la base de code CrystalCoin
en utilisant l'outil de projet intégré crystal init app [name]
:
% crystal init app crystal_coin create crystal_coin/.gitignore create crystal_coin/.editorconfig create crystal_coin/LICENSE create crystal_coin/README.md create crystal_coin/.travis.yml create crystal_coin/shard.yml create crystal_coin/src/crystal_coin.cr create crystal_coin/src/crystal_coin/version.cr create crystal_coin/spec/spec_helper.cr create crystal_coin/spec/crystal_coin_spec.cr Initialized empty Git repository in /Users/eki/code/crystal_coin/.git/
Cette commande créera la structure de base du projet, avec un référentiel git déjà initialisé, des fichiers de licence et readme. Il est également livré avec des stubs pour les tests et le fichier shard.yml
pour décrire le projet et gérer les dépendances, également appelées shards.
Ajoutons le fragment openssl
, qui est nécessaire pour construire l'algorithme SHA256
:
# shard.yml dependencies: openssl: github: datanoise/openssl.cr
Une fois cela fait, retournez dans votre terminal et exécutez crystal deps
. Cela supprimera openssl
et ses dépendances que nous pourrons utiliser.
Maintenant que la bibliothèque requise est installée dans notre code, commençons par définir la classe Block
, puis construisons la fonction de hachage.
# src/crystal_coin/block.cr require "openssl" module CrystalCoin class Block def initialize(data : String) @data = data end def hash hash = OpenSSL::Digest.new("SHA256") hash.update(@data) hash.hexdigest end end end puts CrystalCoin::Block.new("Hello, Cryptos!").hash
Vous pouvez maintenant tester votre application en exécutant crystal run crystal src/crystal_coin/block.cr
depuis votre terminal.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5
Concevoir notre Blockchain
Chaque bloc est stocké avec un timestamp
et, éventuellement, un index
. Dans CrystalCoin
, nous allons stocker les deux. Pour aider à assurer l'intégrité dans toute la blockchain, chaque bloc aura un hachage d'auto-identification. Comme Bitcoin, le hachage de chaque bloc sera un hachage cryptographique du bloc ( index
, timestamp
, data
et le hachage du hachage du bloc précédent previous_hash
). Les données peuvent être tout ce que vous voulez pour le moment.
module CrystalCoin class Block property current_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @current_hash = hash_block end private def hash_block hash = OpenSSL::Digest.new("SHA256") hash.update("#{@index}#{@timestamp}#{@data}#{@previous_hash}") hash.hexdigest end end end puts CrystalCoin::Block.new(data: "Same Data").current_hash
Dans Crystal lang, nous remplaçons les attr_accessor
, attr_getter
et attr_setter
de Ruby par de nouveaux mots-clés :
Mot-clé rubis | Mot-clé cristal |
---|---|
attr_accessor | biens |
attr_reader | getter |
attr_writer | setter |
Une autre chose que vous avez peut-être remarquée dans Crystal est que nous voulons indiquer au compilateur des types spécifiques via notre code. Crystal déduit les types, mais chaque fois que vous avez une ambiguïté, vous pouvez également déclarer explicitement des types. C'est pourquoi nous avons ajouté des types String
pour current_hash
.
Exécutons maintenant block.cr
deux fois et notons que les mêmes données généreront des hachages différents en raison de l' timestamp
différent :
crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3
Nous avons maintenant notre structure de blocs, mais nous créons une blockchain. Nous devons commencer à ajouter des blocs pour former une véritable chaîne. Comme je l'ai mentionné précédemment, chaque bloc nécessite des informations du bloc précédent. Mais comment le premier bloc de la blockchain y arrive-t-il ? Eh bien, le premier bloc, ou bloc de genesis
, est un bloc spécial (un bloc sans prédécesseurs). Dans de nombreux cas, il est ajouté manuellement ou possède une logique unique lui permettant d'être ajouté.
Nous allons créer une nouvelle fonction qui renvoie un bloc de genèse. Ce bloc est d' index=0
, et il a une valeur de données arbitraire et une valeur arbitraire dans le paramètre previous_hash
.
Construisons ou classons la méthode Block.first
qui génère le bloc genesis :
module CrystalCoin class Block ... def self.first(data="Genesis Block") Block.new(data: data, previous_hash: "0") end ... end end
Et testons-le en utilisant p CrystalCoin::Block.first
:
#<CrystalCoin::Block:0x10b33ac80 @current_hash="acb701a9b70cff5a0617d654e6b8a7155a8c712910d34df692db92455964d54e", @data="Genesis Block", @index=0, @timestamp=2018-05-13 17:54:02 +03:00, @previous_hash="0">
Maintenant que nous sommes capables de créer un bloc de genèse , nous avons besoin d'une fonction qui générera des blocs successifs dans la blockchain.
Cette fonction prendra le bloc précédent dans la chaîne comme paramètre, créera les données pour le bloc à générer et renverra le nouveau bloc avec les données appropriées. Lorsque de nouveaux blocs hachent les informations des blocs précédents, l'intégrité de la blockchain augmente à chaque nouveau bloc.
Une conséquence importante est qu'un bloc ne peut pas être modifié sans changer le hachage de chaque bloc consécutif. Ceci est démontré dans l'exemple ci-dessous. Si les données dans le bloc 44 sont modifiées de LOOP
à EAST
, tous les hachages des blocs consécutifs doivent être modifiés. En effet, le hachage du bloc dépend de la valeur de previous_hash
(entre autres).
Si nous ne le faisions pas, il serait plus facile pour un tiers de modifier les données et de remplacer notre chaîne par une entièrement nouvelle. Cette chaîne de hachages agit comme une preuve cryptographique et permet de garantir qu'une fois qu'un bloc est ajouté à la blockchain, il ne peut pas être remplacé ou supprimé. Créons la méthode de classe Block.next
:
module CrystalCoin class Block ... def self.next(previous_node, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_node.index + 1})", index: previous_node.index + 1, previous_hash: previous_hash.hash ) end ... end end
Pour tout essayer ensemble, nous allons créer une blockchain simple. Le premier élément de la liste est le bloc de genèse. Et bien sûr, nous devons ajouter les blocs suivants. Nous allons créer cinq nouveaux blocs pour démontrer CrystalCoin
:
blockchain = [ CrystalCoin::Block.first ] previous_block = blockchain[0] 5.times do new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block end p blockchain
[#<CrystalCoin::Block:0x108c57c80 @current_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @index=0, @previous_hash="0", @timestamp=2018-06-04 12:13:21 +03:00, @data="Genesis Block>, #<CrystalCoin::Block:0x109c89740 @current_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @index=1, @previous_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @timestamp=2018-06-04 12:13:21 +03:00, @data="Transaction data number (1)">, #<CrystalCoin::Block:0x109cc8500 @current_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @index=2, @previous_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (2)">, #<CrystalCoin::Block:0x109ccbe40 @current_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @index=3, @previous_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (3)">, #<CrystalCoin::Block:0x109cd0980 @current_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @index=4, @previous_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (4)">, #<CrystalCoin::Block:0x109cd0100 @current_hash= "00a0c70e5412edd7389a0360b48c407ce4ddc8f14a0bcf16df277daf3c1a00c7", @index=5, @previous_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (5)">
Preuve de travail
Un algorithme de preuve de travail (PoW) est la façon dont de nouveaux blocs sont créés ou exploités sur la blockchain. Le but de PoW est de découvrir un nombre qui résout un problème. Le numéro doit être difficile à trouver mais facile à vérifier informatiquement par n'importe qui sur le réseau. C'est l'idée centrale derrière la preuve de travail.
Démontrons avec un exemple pour nous assurer que tout est clair. Nous supposerons que le hachage d'un entier x multiplié par un autre y doit commencer par 00
. Alors:
hash(x * y) = 00ac23dc...
Et pour cet exemple simplifié, fixons x=5
et implémentons ceci dans Crystal :
x = 5 y = 0 while hash((x*y).to_s)[0..1] != "00" y += 1 end puts "The solution is y = #{y}" puts "Hash(#{x}*#{y}) = #{hash((x*y).to_s)}"
Exécutons le code :
crystal_coin [master●●] % time crystal src/crystal_coin/pow.cr The solution is y = 530 Hash(5*530) = 00150bc11aeeaa3cdbdc1e27085b0f6c584c27e05f255e303898dcd12426f110 crystal src/crystal_coin/pow.cr 1.53s user 0.23s system 160% cpu 1.092 total
Comme vous pouvez le voir, ce nombre y=530
était difficile à trouver (force brute) mais facile à vérifier en utilisant la fonction de hachage.
Pourquoi s'embêter avec cet algorithme PoW ? Pourquoi ne créons-nous pas simplement un hachage par bloc et c'est tout ? Un hachage doit être valide . Dans notre cas, un hachage sera valide si les deux premiers caractères de notre hachage sont 00
. Si notre hachage commence par 00......
, il est considéré comme valide. C'est ce qu'on appelle la difficulté . Plus la difficulté est élevée, plus il faut de temps pour obtenir un hachage valide.
Mais, si le hachage n'est pas valide la première fois, quelque chose doit changer dans les données que nous utilisons. Si nous utilisons les mêmes données encore et encore, nous obtiendrons le même hachage encore et encore et notre hachage ne sera jamais valide. Nous utilisons quelque chose appelé nonce
dans notre hachage (dans notre exemple précédent, c'est le y
). C'est simplement un nombre que nous incrémentons à chaque fois que le hachage n'est pas valide. Nous obtenons nos données (date, message, hachage précédent, index) et un nonce de 1. Si le hachage que nous obtenons avec ceux-ci n'est pas valide, nous essayons avec un nonce de 2. Et nous incrémentons le nonce jusqu'à ce que nous obtenions un hachage valide .
Dans Bitcoin, l'algorithme Proof of Work s'appelle Hashcash. Ajoutons une preuve de travail à notre classe Block et commençons à exploiter pour trouver le nonce. Nous allons utiliser une difficulté codée en dur de deux zéros '00' :
Reconcevons maintenant notre classe Block pour prendre en charge cela. Notre bloc CrystalCoin
contiendra les attributs suivants :
1) index: indicates the index of the block ex: 0,1 2) timestamp: timestamp in epoch, number of seconds since 1 Jan 1970 3) data: the actual data that needs to be stored on the blockchain. 4) previous_hash: the hash of the previous block, this is the chain/link between the blocks 5) nonce: this is the number that is to be mined/found. 6) current_hash: The hash value of the current block, this is generated by combining all the above attributes and passing it to a hashing algorithm
Je vais créer un module séparé pour faire le hachage et trouver le nonce
afin que nous gardions notre code propre et modulaire. Je l'appellerai proof_of_work.cr
:
require "openssl" module CrystalCoin module ProofOfWork private def proof_of_work(difficulty = "00") nonce = 0 loop do hash = calc_hash_with_nonce(nonce) if hash[0..1] == difficulty return nonce else nonce += 1 end end end private def calc_hash_with_nonce(nonce = 0) sha = OpenSSL::Digest.new("SHA256") sha.update("#{nonce}#{@index}#{@timestamp}#{@data}#{@previous_hash}") sha.hexdigest end end end
Notre classe Block
ressemblerait à quelque chose comme :
require "./proof_of_work" module CrystalCoin class Block include ProofOfWork property current_hash : String property index : Int32 property nonce : Int32 property previous_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end def self.first(data = "Genesis Block") Block.new(data: data, previous_hash: "0") end def self.next(previous_block, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_block.index + 1})", index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
Peu de choses à noter sur le code Crystal et les exemples de langage Crystal en général. Dans Crystal, les méthodes sont publiques par défaut. Crystal exige que chaque méthode privée soit préfixée avec le mot-clé privé, ce qui pourrait prêter à confusion venant de Ruby.
Vous avez peut-être remarqué que les types Integer de Crystal sont Int8
, Int16
, Int32
, Int64
, UInt8
, UInt16
, UInt32
ou UInt64
par rapport au Fixnum
de Ruby . true
et false
sont des valeurs dans la classe Bool
plutôt que des valeurs dans les classes TrueClass
ou FalseClass
dans Ruby.
Crystal a des arguments de méthode facultatifs et nommés en tant que fonctionnalités de base du langage, et ne nécessite pas l'écriture de code spécial pour gérer les arguments, ce qui est plutôt cool. Découvrez Block#initialize(index = 0, data = "data", previous_hash = "hash")
puis appelez-le avec quelque chose comme Block.new(data: data, previous_hash: "0")
.
Pour une liste plus détaillée des différences entre les langages de programmation Crystal et Ruby, consultez Crystal for Rubyists.
Essayons maintenant de créer cinq transactions en utilisant :
blockchain = [ CrystalCoin::Block.first ] puts blockchain.inspect previous_block = blockchain[0] 5.times do |i| new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block puts new_block.inspect end
[#<CrystalCoin::Block:0x108f8fea0 @current_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759", @index=0, @nonce=17, @timestamp=2018-05-14 17:20:46 +03:00, @data="Genesis Block", @previous_hash="0">] #<CrystalCoin::Block:0x108f8f660 @current_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0", @index=1, @nonce=24, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (1)", @previous_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759"> #<CrystalCoin::Block:0x109fc5ba0 @current_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5", @index=2, @nonce=61, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (2)", @previous_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0"> #<CrystalCoin::Block:0x109fdc300 @current_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97", @index=3, @nonce=149, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (3)", @previous_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5"> #<CrystalCoin::Block:0x109ff58a0 @current_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484", @index=4, @nonce=570, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (4)", @previous_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97"> #<CrystalCoin::Block:0x109fde120 @current_hash="00720bb6e562a25c19ecd2b277925057626edab8981ff08eb13773f9bb1cb842", @index=5, @nonce=475, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (5)", @previous_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484">
Regarde la différence? Désormais, tous les hachages commencent par 00
. C'est la magie de la preuve de travail. En utilisant ProofOfWork
nous avons trouvé le nonce
et la preuve est le hachage avec la difficulté de correspondance, c'est-à-dire les deux zéros non significatifs 00
.
Notez qu'avec le premier bloc que nous avons créé, nous avons essayé 17 nonces jusqu'à trouver le numéro porte-bonheur correspondant :
Bloquer | Boucles / Nombre de calculs de hachage |
---|---|
#0 | 17 |
#1 | 24 |
#2 | 61 |
#3 | 149 |
#4 | 570 |
#5 | 475 |
Essayons maintenant une difficulté de quatre zéros non significatifs ( difficulty="0000"
):

Bloquer | Boucles / Nombre de calculs de hachage |
---|---|
#1 | 26 762 |
#2 | 68 419 |
#3 | 23 416 |
#4 | 15 353 |
Dans le premier bloc, j'ai essayé 26762 nonces (comparez 17 nonces avec la difficulté '00') jusqu'à trouver le numéro porte-bonheur correspondant.
Notre Blockchain comme API
Jusqu'ici tout va bien. Nous avons créé notre blockchain simple et c'était relativement facile à faire. Mais le problème ici est que CrystalCoin
ne peut fonctionner que sur une seule machine (il n'est pas distribué/décentralisé).
À partir de maintenant, nous commencerons à utiliser les données JSON pour CrystalCoin
. Les données seront des transactions, donc le champ de données de chaque bloc sera une liste de transactions.
Chaque transaction sera un objet JSON détaillant l' sender
de la pièce, le receiver
de la pièce et le amount
de CrystalCoin qui est transféré :
{ "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }
Quelques modifications à notre classe Block
pour prendre en charge le nouveau format transaction
(précédemment appelé data
). Donc, juste pour éviter toute confusion et maintenir la cohérence, nous utiliserons désormais le terme transaction
pour désigner les data
publiées dans notre exemple d'application.
Nous allons introduire une nouvelle classe simple Transaction
:
module CrystalCoin class Block class Transaction property from : String property to : String property amount : Int32 def initialize(@from, @to, @amount) end end end end
Les transactions sont regroupées en blocs, de sorte qu'un bloc ne peut contenir qu'une ou plusieurs transactions. Les blocs contenant les transactions sont générés fréquemment et ajoutés à la blockchain.
La blockchain est censée être une collection de blocs. Nous pouvons stocker tous les blocs de la liste Crystal, et c'est pourquoi nous introduisons la nouvelle classe Blockchain
:
Blockchain
aura des tableaux chain
et uncommitted_transactions
. La chain
inclura tous les blocs minés dans la blockchain, et uncommitted_transactions
aura toutes les transactions qui n'ont pas été ajoutées à la blockchain (toujours non minées). Une fois que nous avons initialisé Blockchain
, nous créons le bloc de genèse à l'aide de Block.first
et l'ajoutons au tableau de chain
, et nous ajoutons un tableau vide uncommitted_transactions
.
Nous allons créer la méthode Blockchain#add_transaction
pour ajouter des transactions au tableau uncommitted_transactions
.
Construisons notre nouvelle classe Blockchain
:
require "./block" require "./transaction" module CrystalCoin class Blockchain getter chain getter uncommitted_transactions def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction end def add_transaction(transaction) @uncommitted_transactions << transaction end end end
Dans la classe Block
, nous allons commencer à utiliser transactions
au lieu de data
:
module CrystalCoin class Block include ProofOfWork def initialize(index = 0, transactions = [] of Transaction, previous_hash = "hash") @transactions = transactions ... end .... def self.next(previous_block, transactions = [] of Transaction) Block.new( transactions: transactions, index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
Maintenant que nous savons à quoi ressembleront nos transactions, nous avons besoin d'un moyen de les ajouter à l'un des ordinateurs de notre réseau blockchain, appelé node
. Pour ce faire, nous allons créer un simple serveur HTTP.
Nous allons créer quatre points de terminaison :
- [POST]
/transactions/new
: pour créer une nouvelle transaction vers un bloc - [GET]
/mine
: pour dire à notre serveur de miner un nouveau bloc. - [GET]
/chain
: pour retourner la blockchain complète au formatJSON
. - [GET]
/pending
: pour retourner les transactions en attente (uncommitted_transactions
).
Nous allons utiliser le framework Web Kemal. Il s'agit d'un micro-framework qui facilite le mappage des points de terminaison aux fonctions Crystal. Kemal est fortement influencé par Sinatra for Rubyists et fonctionne de manière très similaire. Si vous recherchez un équivalent Ruby on Rails, consultez Amber.
Notre serveur formera un nœud unique dans notre réseau blockchain. Commençons par ajouter Kemal
au fichier shard.yml
en tant que et installons la dépendance :
dependencies: kemal: github: kemalcr/kemal
Construisons maintenant le squelette de notre serveur HTTP :
# src/server.cr require "kemal" require "./crystal_coin" # Generate a globally unique address for this node node_identifier = UUID.random.to_s # Create our Blockchain blockchain = Blockchain.new get "/chain" do "Send the blockchain as json objects" end get "/mine" do "We'll mine a new Block" end get "/pending" do "Send pending transactions as json objects" end post "/transactions/new" do "We'll add a new transaction" end Kemal.run
Et lancez le serveur :
crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Assurons-nous que le serveur fonctionne correctement :
% curl http://0.0.0.0:3000/chain Send the blockchain as json objects%
Jusqu'ici tout va bien. Maintenant, nous pouvons procéder à la mise en œuvre de chacun des points de terminaison. Commençons par implémenter /transactions/new
et pending
:
get "/pending" do { transactions: blockchain.uncommitted_transactions }.to_json end post "/transactions/new" do |env| transaction = CrystalCoin::Block::Transaction.new( from: env.params.json["from"].as(String), to: env.params.json["to"].as(String), amount: env.params.json["amount"].as(Int64) ) blockchain.add_transaction(transaction) "Transaction #{transaction} has been added to the node" end
Mise en œuvre simple. Nous créons simplement un objet CrystalCoin::Block::Transaction
et ajoutons la transaction au tableau uncommitted_transactions
en utilisant Blockchain#add_transaction
.
Pour le moment, les transactions sont initialement stockées dans un pool de uncommitted_transactions
. Le processus consistant à placer les transactions non confirmées dans un bloc et à calculer la preuve de travail (PoW) est connu sous le nom d' extraction de blocs. Une fois que le nonce
satisfaisant nos contraintes est déterminé, nous pouvons dire qu'un bloc a été miné, et le nouveau bloc est mis dans la blockchain.
Dans CrystalCoin
, nous utiliserons l'algorithme simple de preuve de travail que nous avons créé précédemment. Pour créer un nouveau bloc, l'ordinateur d'un mineur devra :
- Trouvez le dernier bloc de la
chain
. - Rechercher les transactions en attente (
uncommitted_transactions
). - Créez un nouveau bloc en utilisant
Block.next
. - Ajoutez le bloc miné au tableau de
chain
. - Nettoyer le tableau
uncommitted_transactions
.
Donc, pour implémenter le point final /mine
, implémentons d'abord les étapes ci-dessus dans Blockchain#mine
:
module CrystalCoin class Blockchain include Consensus BLOCK_SIZE = 25 ... def mine raise "No transactions to be mined" if @uncommitted_transactions.empty? new_block = Block.next( previous_block: @chain.last, transactions: @uncommitted_transactions.shift(BLOCK_SIZE) ) @chain << new_block end end end
Nous nous assurons d'abord que nous avons des transactions en attente à exploiter. Ensuite, nous obtenons le dernier bloc en utilisant @chain.last
, et les 25
premières transactions à extraire (nous utilisons Array#shift(BLOCK_SIZE)
pour retourner un tableau des 25 premières uncommitted_transactions
, puis supprimer les éléments commençant à l'index 0) .
Maintenant, implémentons /mine
end-point :
get "/mine" do blockchain.mine "Block with index=#{blockchain.chain.last.index} is mined." end
Et pour le point final de /chain
:
get "/chain" do { chain: blockchain.chain }.to_json end
Interagir avec notre Blockchain
Nous utiliserons cURL
pour interagir avec notre API sur un réseau.
Tout d'abord, lançons le serveur :
crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Créons ensuite deux nouvelles transactions en faisant une requête POST
à http://localhost:3000/transactions/new
avec un corps contenant notre structure de transaction :
crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"iron_man", "amount": 1000}' Transaction #<CrystalCoin::Block::Transaction:0x10c4159f0 @from="eki", @to="iron_man", @amount=1000_i64> has been added to the node% crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"hulk", "amount": 700}' Transaction #<CrystalCoin::Block::Transaction:0x10c415810 @from="eki", @to="hulk", @amount=700_i64> has been added to the node%
Listons maintenant les transactions en attente (c'est-à-dire les transactions qui n'ont pas encore été ajoutées au bloc) :
crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { "transactions":[ { "from":"ekis", "to":"huslks", "amount":7090 }, { "from":"ekis", "to":"huslks", "amount":70900 } ] }
Comme nous pouvons le voir, les deux transactions que nous avons créées précédemment ont été ajoutées à uncommitted_transactions
.
Maintenant, minons les deux transactions en faisant une requête GET
à http://0.0.0.0:3000/mine
:
crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.
On dirait que nous avons extrait avec succès le premier bloc et l'avons ajouté à notre chain
. Vérifions notre chain
et si elle inclut le bloc miné :
crystal_coin [master●] % curl http://0.0.0.0:3000/chain { "chain": [ { "index": 0, "current_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "nonce": 363, "previous_hash": "0", "transactions": [ ], "timestamp": "2018-05-23T01:59:52+0300" }, { "index": 1, "current_hash": "003c05da32d3672670ba1e25ecb067b5bc407e1d5f8733b5e33d1039de1a9bf1", "nonce": 320, "previous_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "transactions": [ { "from": "ekis", "to": "huslks", "amount": 7090 }, { "from": "ekis", "to": "huslks", "amount": 70900 } ], "timestamp": "2018-05-23T02:02:38+0300" } ] }
Consensus et décentralisation
C'est cool. Nous nous sommes procuré une blockchain de base qui accepte les transactions et nous permet d'exploiter de nouveaux blocs. Mais le code que nous avons implémenté jusqu'à présent est censé fonctionner sur un seul ordinateur, alors que tout l'intérêt des blockchains est qu'elles doivent être décentralisées. Mais s'ils sont décentralisés, comment s'assurer qu'ils reflètent tous la même chaîne ?
C'est le problème du Consensus
.
Nous devrons implémenter un algorithme de consensus si nous voulons plus d'un nœud dans notre réseau.
Enregistrement de nouveaux nœuds
Pour implémenter un algorithme de consensus, nous avons besoin d'un moyen d'informer un nœud des nœuds voisins sur le réseau. Chaque nœud de notre réseau doit conserver un registre des autres nœuds du réseau. Par conséquent, nous aurons besoin de plus de points de terminaison :
- [POST]
/nodes/register
: pour accepter une liste de nouveaux nœuds sous forme d'URL. - [GET]
/nodes/resolve
: pour implémenter notre algorithme de consensus, qui résout tous les conflits, pour s'assurer qu'un nœud a la bonne chaîne.
Nous devons modifier le constructeur de notre blockchain et fournir une méthode pour enregistrer les nœuds :
--- a/src/crystal_coin/blockchain.cr +++ b/src/crystal_coin/blockchain.cr @@ -7,10 +7,12 @@ module CrystalCoin getter chain getter uncommitted_transactions + getter nodes def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction + @nodes = Set(String).new [] of String end def add_transaction(transaction)
Notez que nous avons utilisé une structure de données Set
avec le type String
pour contenir la liste des nœuds. C'est un moyen peu coûteux de s'assurer que l'ajout de nouveaux nœuds est idempotent et que peu importe le nombre de fois que nous ajoutons un nœud spécifique, il apparaît exactement une fois.
Ajoutons maintenant un nouveau module à Consensus
et implémentons la première méthode register_node(address)
:
require "uri" module CrystalCoin module Consensus def register_node(address : String) uri = URI.parse(address) node_address = "#{uri.scheme}:://#{uri.host}" node_address = "#{node_address}:#{uri.port}" unless uri.port.nil? @nodes.add(node_address) rescue raise "Invalid URL" end end end
La fonction register_node
analysera l'URL du nœud et la formatera.
Et ici, créons /nodes/register
end-point :
post "/nodes/register" do |env| nodes = env.params.json["nodes"].as(Array) raise "Empty array" if nodes.empty? nodes.each do |node| blockchain.register_node(node.to_s) end "New nodes have been added: #{blockchain.nodes}" end
Maintenant, avec cette implémentation, nous pourrions être confrontés à un problème avec plusieurs nœuds. La copie des chaînes de quelques nœuds peut différer. Dans ce cas, nous devons nous mettre d'accord sur une version de la chaîne pour maintenir l'intégrité de l'ensemble du système. Nous devons parvenir à un consensus.
Pour résoudre ce problème, nous établirons la règle selon laquelle la chaîne valide la plus longue est celle à utiliser. En utilisant cet algorithme, nous parvenons à un consensus entre les nœuds de notre réseau. La raison derrière cette approche est que la chaîne la plus longue est une bonne estimation de la plus grande quantité de travail effectué.
module CrystalCoin module Consensus ... def resolve updated = false @nodes.each do |node| node_chain = parse_chain(node) return unless node_chain.size > @chain.size return unless valid_chain?(node_chain) @chain = node_chain updated = true rescue IO::Timeout puts "Timeout!" end updated end ... end end
Gardez à l'esprit que la resolve
est une méthode qui parcourt tous nos nœuds voisins, télécharge leurs chaînes et les vérifie à l'aide de valid_chain?
méthode. If a valid chain is found, whose length is greater than ours, we replace ours.
Now let's implement parse_chain()
and valid_chain?()
private methods:
module CrystalCoin module Consensus ... private def parse_chain(node : String) node_url = URI.parse("#{node}/chain") node_chain = HTTP::Client.get(node_url) node_chain = JSON.parse(node_chain.body)["chain"].to_json Array(CrystalCoin::Block).from_json(node_chain) end private def valid_chain?(node_chain) previous_hash = "0" node_chain.each do |block| current_block_hash = block.current_hash block.recalculate_hash return false if current_block_hash != block.current_hash return false if previous_hash != block.previous_hash return false if current_block_hash[0..1] != "00" previous_hash = block.current_hash end return true end end end
For parse_chain()
we:
- Issue a
GET
HTTP request usingHTTP::Client.get
to/chain
end-point. - Parse the
/chain
JSON response usingJSON.parse
. - Extract an array of
CrystalCoin::Block
objects from the JSON blob that was returned usingArray(CrystalCoin::Block).from_json(node_chain)
.
There is more than one way of parsing JSON in Crystal. The preferred method is to use Crystal's super-handy JSON.mapping(key_name: Type)
functionality that gives us the following:
- A way to create an instance of that class from a JSON string by running
Class.from_json
. - A way to serialize an instance of that class into a JSON string by running
instance.to_json
. - Getters and setters for keys defined in that class.
In our case, we had to define JSON.mapping
in CrystalCoin::Block
object, and we removed property
usage in the class, like so:
module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end
Now for Blockchain#valid_chain?
, we iterate through all of the blocks, and for each we:
- Recalculate the hash for the block using
Block#recalculate_hash
and check that the hash of the block is correct:
module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
- Check each of the blocks linked correctly with their previous hashes.
- Check the block's hash is valid for the number of zeros (
difficulty
in our case00
).
And finally we implement /nodes/resolve
end-point:
get "/nodes/resolve" do if blockchain.resolve "Successfully updated the chain" else "Current chain is up-to-date" end end
It's done! You can find the final code on GitHub.
The structure of our project should look like this:
crystal_coin [master●] % tree src/ src/ ├── crystal_coin │ ├── block.cr │ ├── blockchain.cr │ ├── consensus.cr │ ├── proof_of_work.cr │ ├── transaction.cr │ └── version.cr ├── crystal_coin.cr └── server.cr
Let's Try it Out
- Grab a different machine, and run different nodes on your network. Or spin up processes using different ports on the same machine. In my case, I created two nodes on my machine, on a different port to have two nodes:
http://localhost:3000
andhttp://localhost:3001
. - Register the second node address to the first node using:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}' New nodes have been added: Set{"http://0.0.0.0:3001"}%
- Let's add a transaction to the second node:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}' Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
- Let's mine transactions into a block at the second node:
crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
- At this point, the first node has only one block (genesis block), and the second node has two nodes (genesis and the mined block):
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062","nonce":157,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:45+0300"}]}%
crystal_coin [master●●] % curl http://0.0.0.0:3001/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
- Our goal is to update the chain in the first node to include the newly generated block at the second one. So let's resolve the first node:
crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%
Let's check if the chain in the first node has updated:
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
Hourra ! Our Crystal language example works like a charm, and I hope you found this lengthy tutorial crystal clear, pardon the pun.
Emballer
This Crystal language tutorial covered the fundamentals of a public blockchain. If you followed along, you implemented a blockchain from scratch and built a simple application allowing users to share information on the blockchain.
We've made a fairly sized blockchain at this point. Now, CrystalCoin
can be launched on multiple machines to create a network, and real CrystalCoins
can be mined.
I hope that this has inspired you to create something new, or at the very least take a closer look at Crystal programming.
Note: The code in this tutorial is not ready for real-world use. Please refer to this as a general guide only.