Rejoignez-Nous sur

Rusty Chains: une implémentation de base de la blockchain écrite dans Pure Rust

News

Rusty Chains: une implémentation de base de la blockchain écrite dans Pure Rust

TyCbBivxFAZs5SPIe55gVgQQvaH3 dn10d3zup
Photo de profil de l'auteur

Un tutoriel pratique sur les bases de la blockchain, la taxonomie et la rouille.

Qu'y a-t-il à l'intérieur de l'histoire?

Dans cet article, je vais vous emmener un peu tout au long de mon parcours dans la blockchain. Le prochain projet de mon travail sera alimenté par la technologie blockchain. Comme je n'avais (n'avais) aucune idée réelle de ce sujet en dehors de quelques idées générales de haut niveau, j'ai commencé à chercher des informations sur le sujet et j'ai finalement décidé que le meilleur moyen de surmonter le niveau de la simple répétition des mots à la mode et d'obtenir un niveau réel de compréhension c'est se salir les mains: Mettre en œuvre la chose à partir de zéro.

Pas tout ce qui serait nécessaire pour déployer votre propre blockchain publique, mais une partie fonctionnelle de la technologie de base qui ressemble déjà à une blockchain (locale) entièrement fonctionnelle. Nous utiliserons le langage de programmation plutôt nouveau Rouille faire cela. Comme le langage n'a pas une énorme base d'utilisateurs comme Java, C ++ ou Python, ce module sera très détaillé dans la partie implémentation en présentant non seulement le code mais expliquant ses idées et syntaxe.

Il y a beaucoup à couvrir. Lors du suivi, vous aurez un naturel compréhension des blockchains en créant un prototype de blockchain extensible et apprendre quelques bases utilisables dans Rust. Qui sait à quoi cela pourrait servir à l'avenir? Mais ne vous inquiétez pas, je vais essayer de vous guider pas à pas tout au long du processus et au lieu de simplement laisser tomber des extraits de code.

Conditions préalables

Pour comprendre l'article, la seule chose que vous devrez apporter est un niveau de base d'expérience en programmation et le souhait de plonger dans le sujet des blockchains. Une connaissance pratique spécifique de Rust n'est pas strictement nécessaire. J'expliquerai les concepts pertinents lors de mes déplacements.

Cependant, une installation de Rust(1) et un éditeur de code approprié serait un plus (par exemple, CLion ou Visual Studio Code) si vous souhaitez essayer le code et le suivi des détails est recommandé.

Fast Track 🏃

Voici les pièces pertinentes:

Fondamentaux

La première partie de l'article vous guidera à travers quelques bases et ma vision personnelle de la technologie afin que tout le monde soit sur la même longueur d'onde.

Qu'est-ce qu'une blockchain?

Tout d'abord, parlons un peu de la blockchain elle-même (non vraiment: prenez d'abord un café ou un thé ☕ nous avons du travail à faire). Si nous voulons parler de blockchains, nous devons d'abord parler de Livres distribués. Vous avez probablement entendu ce terme dans le même souffle que la blockchain.

Peut-être avez-vous découvert le protocole de partage de fichiers peer-to-peer BitTorrent (2). Ce protocole permet le partage de fichiers dans un décentralisé manière. Normalement, lors du téléchargement d'un fichier depuis quelque part sur Internet, vous enverrez une demande à un serveur spécifique qui stocke le fichier entier et transmet les données à votre appareil. C'est classique architecture client-serveur. BitTorrent partagera cependant un seul fichier entre plusieurs acteurs / pairs en même temps. Si vous téléchargez un fichier à partir du réseau Torrent, vous le téléchargerez effectivement à partir de plusieurs sources et deviendrez éventuellement une source vous-même en participant au réseau.

Les grands livres distribués partagent certaines caractéristiques communes. Les données (c'est-à-dire le grand livre) – qui ne sont généralement pas simplement un simple fichier mais plutôt une sorte de base de données ou vraiment Quoique tu veuilles que cela soit – est répartis entre plusieurs acteurs / pairs. Il n’existe pas d’autorité unique chargée de tenir, de modifier ou de distribuer ce registre. Plutôt, tous les connectés pairs garder un copie du même grand livre.

Cette idée comporte de nombreux problèmes et questions comme: Qui peut ajouter des informations au réseau? Qui peut le modifier? Qui peut le lire? Combien de données peut-on ajouter? Qui paie le stockage? Qui paie le traitement? Quelles données sont ajoutées en premier? Comment reconnaître les échecs? Comment empêcher les participants malveillants?

Comment l'authentification est-elle gérée? Qu'en est-il de la latence du réseau? C'est beaucoup de technologie compliquée couvrir. Toutes ces questions nécessitent une forme de consensus sur laquelle tous les pairs participants sont d'accord. Le consensus est un sujet assez long en soi. Il existe différents types d'algorithmes de consensus adaptés à différents cas d'utilisation (3) ayant tous leurs propres avantages et inconvénients.

Dans cet article, nous allons implémenter une sorte de Preuve de travail variante, qui est utilisée dans les blockchains publiques connues comme Ethereum (pourrait passer à Preuve d'enjeu dans les futures versions) ou Bitcoin. Nous y reviendrons au chapitre II de la rédaction.

Cela dit, quel est le lien entre registres distribués et blockchains? Pour être honnête: en termes de taxonomie – ce n’est pas si simple (4):

Le terme blockchain est un mot à la mode mal défini

Si nous voulons visualiser la relation dans un diagramme de Venn, cela peut ressembler à ceci:

TyCbBivxFAZs5SPIe55gVgQQvaH3 dk6y3zs0

Alors, une blockchain est-elle un registre distribué? Eh bien, ça pourrait être; sorte de. Doit-il l'être? Pas nécessairement. Mon point de vue:

Le bchaîne de verrouillage lui-même est juste un type spécial de registre

C'est un type spécial de registre qui se souvient de tous les états-transitions il est jamais passé. Imaginez avoir une base de données régulière, où chaque changement / transaction vous vous engagez sera enregistré tel que le état final de la base de données peut toujours être obtenue par exécuter toutes les transactions enregistrées dans l'ordre. L'état final dans le contexte des blockchains est appelé État du monde.

Étant donné que cet état ne contient pas l'historique complet des transactions, il est généralement beaucoup plus petit que la blockchain réelle. Cependant, il n'est pas strictement nécessaire de stocker l'état du monde car il peut toujours être récupéré en rejouant toutes les transactions. Ce chaîne des transactions commandées est sécurisé de telle sorte qu'il est impossible de modifier une transaction intermédiaire (parties précédentes de la chaîne) sans casser le tout. Ceci est souvent appelé immuable Etat.

Si vous partagez cette blockchain entre plusieurs parties (y compris le monde entier si public) en tenant compte des fonctionnalités mentionnées précédemment sur le consensus, eh bien, vous avez un registre distribué où le registre est cette blockchain.

Mais pourquoi les gens parlent-ils de blockchains alors qu'ils parlent de réalisations concrètes comme Bitcoin ou Ethereum? Eh bien, le blockchain lui-même fait partie de tous. Je préférerais les appeler Cadres de blockchain et écosystèmes de blockchain.

La compréhension commune de la blockchain se réfère en fait aux frameworks blockchain et aux écosystèmes blockchain

Le marketing a décidé que c'était la même chose, mais gardez à l'esprit: ce n'est vraiment pas le cas. Les cadres et les écosystèmes de la blockchain offrent généralement beaucoup de technologies et de logiciels, la plupart du temps avec quelques Composants offrant différents ensembles de fonctionnalités, éventuellement aussi avec un base d'utilisateurs / réseau. Il existe de nombreux frameworks blockchain dans la nature, qui peuvent ou non être compatibles avec un autre.

À côté de l'Ethereum et du Bitcoin déjà mentionnés, il existe des représentants de premier plan comme Tissu Hyperledger, Hyperledger Besu, Substrat, Quora ou Corda juste pour nommer une vue; tous ayant des fonctionnalités et des cas d'utilisation / publics différents (5).

Pourquoi avons-nous besoin de blockchains? (Cas d'utilisation)

Il n'est pas si facile de répondre à cette question et pourrait également susciter des opinions. Les cadres de blockchain peuvent être classés en au moins deux saveurs différentes: blockchains publiques et blockchains privées. Ceux-ci ne sont pas fondamentalement différents, mais leurs composants et leurs cas d'utilisation diffèrent.

Blockchains publiques sont fondamentalement accessibles à tous. Les gens peuvent adhérer et participer s'ils parlent la même langue, c'est-à-dire en suivant la même protocoles (ayant le même consensus). Par conséquent, il existe une multitude de clients pour des blockchains spécifiques.

Ethereum a plus de 10 projets actifs répertoriés (6) qui sont implémentés dans toutes sortes de langages de programmation, y compris dans Rust (Parité Ethereum).

Le cas d'utilisation le plus évident des blockchains publiques est de fournir des moyens de fournir et de stocker de la valeur numérique. Bitcoin, l'acteur le plus important dans le domaine, fait exactement cela en fournissant une monnaie numérique: le Bitcoin (BTC). En raison de la quantité limitée de pièces, de l'immuabilité du grand livre et de la nature décentralisée, ces valeurs ne peuvent ni apparaître, ni disparaître au hasard, ni personne ne peut détourner quoi que ce soit.

Ils sont uniques et stables. Les changements ne sont plus opaques. Et voici le point important à retenir: aucun participant n'a besoin de confiance dans n'importe qui. Tout le monde regarde. Il est presque impossible de manipuler le système car il ne dépend pas d’un seul acteur.

Cela signifie aussi, vous ne pouvez pas DDoS (7) le réseau. Ces vecteurs d'attaque disparaissent tout simplement. Sécurité vient intentionnellement.

Assez sûrement, la monnaie et les pièces ne sont pas les seules choses qui peuvent être stockées. Les contrats et les testaments sont-ils conservés à jamais de manière immuable? Cadastre(8)? Preuve de possession de biens numériques? Votre poème est-il conservé pour toujours? Publication d'informations que personne ne peut jamais détruire?

La blockchain vous couvre. Avec des fonctionnalités telles que la machine virtuelle Ethereum (EVM (9)), il est même possible de déployer une logique (c'est-à-dire du code qui effectue des transitions d'état) sur la chaîne. Là, les possibilités et les défis semblent prometteurs pour l'avenir.

Qu'est-ce que les gens intelligents et créatifs pourraient proposer à l'avenir?

Blockchains privées ne sont fondamentalement pas différentes des blockchains publiques. Ils utilisent la même technologie mais sont dotés de fonctionnalités supplémentaires, qui ne sont pas courantes dans les chaînes publiques. Les blockchains privées ne sont pas destinées à être rejointes par quiconque mais par public réglementé. Il peut s'agir de différentes organisations partageant des flux de travail, des biens ou des valeurs communs.

Pour activer un collaboration comme ça, les frameworks ont également besoin de fonctionnalités telles que l'authentification, l'autorisation, autorisation (rôles des utilisateurs), gestion des droits, déploiement, mise en réseau, surveillance, etc. Mais pourquoi quelqu'un voudrait-il faire face à une technologie aussi compliquée simplement pour partager une chaîne de transactions et de données entre certaines sociétés anonymes?

Eh bien, le public semble beaucoup plus petit. Vous vous souvenez de l'état partagé que tous les participants stockent et du consensus sur lequel les participants s'accordent? Il y a pas d'autorité centrale ce que tu dois juste confiance. Personne ne peut simplement manipuler les données. Il n'y a pas deux réalités valables à la fois. De plus, comme chaque partie stocke au moins une copie du grand livre, il y a aucun risque sur perdre l'accès aux données. Si vous partagez un intérêt avec quelqu'un d'autre mais que vous ne pouvez pas absolument faire confiance à l'intégrité de l'entreprise, les blockchains privées peuvent vous couvrir.

J'espère que cette introduction vous a un peu aidé à vous faire une idée de la technologie elle-même et nous a mis sur la même longueur d'onde. Si vous sentez que quelque chose manque ou êtes en désaccord sur certaines déclarations, n'hésitez pas à m'envoyer un message. J'apprécie vraiment les discussions.

Implémentation de la blockchain – PARTIE I

Cette section commencera par la mise en œuvre de la blockchain elle-même. Nous définirons le projet disposition y compris le basique structures de données qui constituent la blockchain. Ceux-ci serviront de squelette pour toutes les implémentations qui seront nécessaires ultérieurement. Veuillez noter que je ne suis en aucun cas un expert de Rust (ou de la blockchain). En fait, c'est mon premier projet Rust.

Étant donné que je ne suis pas un développeur C (++), le code n'est peut-être pas du tout idiomatique. Veuillez ne pas utiliser le code que je fournis comme modèle pour tout ce que vous faites qui pourrait être pertinent pour la sécurité sous quelque forme que ce soit. Tu étais prévenu. Cependant, j'espère que cela clarifiera les concepts sous-jacents et les éléments de base. Encore une fois, tout commentaire que vous me laisserez sera volontiers utilisé pour améliorer le code.

La structure du projet

Nous allons utiliser une structure de dossiers très simple, qui consiste essentiellement en trois fichiers. Un fichier principal (main.rs) qui contiendra le code qui invoquera la blockchain et effectuera quelques exemples d'opérations pour démontrer la fonctionnalité. Un autre fichier qui se trouvera dans un sous-répertoire contiendra nos structures et implémentations de données blockchain réelles.

Le dernier fichier sera le cargo.toml fichier qui contiendra les dépendances externes du projet ainsi que certaines métadonnées du projet. La structure sera la suivante:

/rchain
 |-- src
 |---- rchain
 |------ mod.rs
 |-- main.rs
 |-- cargo.toml

le cargo.toml vient avec une seule dépendance pour le moment qui sera une bibliothèque de hachage (Blake2(dix)) et ressemble à ça:

(package)
name = "rchain_v1"
version = "0.1.0"
authors = ("Richard Vogel ")
edition = "2018"
 
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
(dependencies)
blake2 = "*"

La première fois que vous exécutez cargo build dans le répertoire pour construire le projet et créer des exécutables, cargo téléchargera et compilera les dépendances pour votre architecture. C'est une grande étape de la gestion compliquée des dépendances en C (++): c'est similaire à ce que pépin pour Python fait. Au moment de la rédaction de cet article, il y avait plus de 44000 soi-disant caisses dans l'index (11).

Ce n'est pas la même chose que 255683 versions dans l'index du package Python mais devrait contenir quelque chose pour la plupart des usages. Vous pouvez également appeler les bibliothèques C de Rust.

le

rchain

le répertoire dans le projet est un module. C'est la manière Rusts d'organiser votre code source en plusieurs fichiers et répertoires. Encore une fois, cela est conceptuellement similaire aux modules de Python (sans que vous ayez à inclure un

__init__.py
 

fichier).

Vous pouvez facilement mettre des modules dans des modules, créant ainsi des moyens de hiérarchie. Contrairement à Python, Rust prend en charge les modificateurs d'accès, qui peuvent être utilisés pour autoriser et empêcher d'autres modules ou sous-modules d'accéder à des membres spécifiques.

Le module Blockchain

Entrons dans les détails de la mise en œuvre elle-même. Nous commençons par un Diagramme de classes UML pour illustrer la façon dont tout se déroule dans le module (bien que Rust n’ait pas de classes explicites). Reportez-vous à ce graphique à cet endroit lorsque vous vous sentez perdu dans tous les détails.

TyCbBivxFAZs5SPIe55gVgQQvaH3 bza13zg2

Certains détails sont un peu simplifiés ou omis afin de garder le diagramme raisonnable. Nous allons parcourir chacun des composants étape par étape. le code lui-même sera lourdement commenté et documenté. S'il vous plaît lire les extraits de code que je fournis ici (lisez également le code dans le référentiel pour avoir une vue d'ensemble), même si vous ne comprenez pas tous les petits détails de la syntaxe.

Rust peut être un peu difficile pour les nouveaux arrivants, surtout si vous venez d'un langage de haut niveau comme JavaScript ou Python. J'expliquerai une partie de la syntaxe Rusty la plus particulière dans l'article. Si vous avez peu ou pas d'expérience en codage et que vous voulez juste savoir comment fonctionnent les blockchains: quand même lire les commentaires du code!

le première partie se concentrera sur le structures de données lui-même (c'est-à-dire à quoi ils servent et quelles données ils contiennent). le deuxième partie se concentrera sur certains des plus importants implémentations (c.-à-d. fonctions et méthodes). le troisième partie va enfin démontrer un simple usage du

Blockchain

-Module.

Structures de données

Cette section de l'implémentation expliquera en détail toutes les structures de données et tous leurs attributs / membres. Rust nous permet de diviser la déclaration des données et son implémentation en deux parties différentes. Si vous avez déjà écrit du code C ou C ++, cela pourrait être similaire aux fichiers d'en-tête (.h / .hpp-files) et (.c / .cpp) qui séparent la déclaration et l'implémentation.

Cependant, dans Rust, seuls les membres de données sont déclarés dans cette partie déclarative (à l'exception des traits, qui sont quelque peu spéciaux pour Rust; nous en verrons un exemple ici). Cette section fournira également de nombreux concepts de concepts pertinents pour la blockchain.

Encore une fois, même si vous ne comprenez pas tous les détails du code, lisez le texte qui vous fera comprendre la signification et l'utilisation de chaque bloc de construction.

Blockchain

La définition du

Blockchain

struct est la suivante:

La rouille fait ne pas avoir un mot-clé de classe. Cela ne signifie pas que la rouille ne prend pas en charge les structures de projet de type classe. Nous utiliserons structs à la place, qui présentent beaucoup de ce que les classes dans d'autres langues peuvent faire

Évidemment, la structure stocke une liste de Blocs que nous définirons en détail plus tard. En outre, il existe une carte / dictionnaire de Comptes stocké, qui garde une trace de l'état final actuel de la blockchain. La cartographie de cette carte est Identifiant utilisateur au compte. Pour l'instant, le identifiant d'utilisateur est arbitraire, dans le prochain article, ces clés seront utilisées pour représenter le Clé publique du compte.

Nos blockchains état du monde est entièrement représenté par cette carte. Finalement. pending_transactions sera utilisé pour la comptabilité des transactions prêtes mais pas encore exécutées. Nous n'utiliserons pas cela pour le moment. Ceci est une préparation pour le prochain article.

La première chose que vous remarquerez peut-être est que j'ai utilisé "///" pour les commentaires ici au lieu du "//" typique. Ce sont commentaires doc. Rust est livré avec le sien constructeur de documentation, ce qui génère une belle interface Web documenter l'ensemble du projet et toutes ses dépendances.

Tout ce qui est marqué

///

sera inclus en tant que description dans l'interface Web. Vous pouvez appeler le générateur en tapant documentation de fret dans le dossier principal. Vous pouvez trouver une version de la documentation actuelle sur ici.

Vous pouvez aussi vous demander la ligne

#(derive(Debug, Clone))

. Il s'agit de la syntaxe Rust pour injecter des fonctionnalités / comportements dans une structure. Techniquement, cela fournit des implémentations par défaut de traits, que le compilateur fournit gratuitement (la prochaine partie sera consacrée aux traits).

Ici nous utilisons Déboguer et Cloner. Déboguer est une aide pour joliment imprimer une structure en utilisant le impression!-command, qui autrement ne fonctionnerait pas aussi facilement; Le clone est utilisé pour pouvoir créer doublons d'objets.

Noter la

pub

mot-clé devant certains membres de la structure. Si vous ne fournissez pas ce mot-clé, ceux-ci ne seront pas visibles / accessibles de l'extérieur de la structure elle-même (c'est-à-dire, private par défaut).

WorldState

Comme mentionné précédemment, la blockchain n'est qu'un type spécial de grand livre qui stocke les informations d'une manière spéciale. Comme on le voit dans le

Blockchain

structure ci-dessus, les blocs et les comptes résultants sont stockés dans deux membres différents. Si nous ne nous soucions pas de l'historique des transactions mais uniquement du résultat, nous pourrait supprimer les blocs tout à fait.

En fait, nous pourrions simplement écrire des transactions dans un fichier ou une base de données ou vraiment là où vous voulez qu'elles soient stockées de la manière que vous souhaitez qu'elles soient structurées. Faites peut-être une pause ici et réfléchissez un peu à la raison pour laquelle cela est vrai. Bien que cela introduise une difficulté supplémentaire à l'article, je voulais attribuer ce fait en le modélisant dans le code.

En outre, cela démontre une partie importante de la syntaxe rusts: le traits, qui ressemble à ça:

Traits définir essentiellement un comportement ou interface qui peut être partagé entre plusieurs objets. Cette interface particulière définit simplement certaines fonctionnalités qu'un objet doit fournir pour être un WorldState. Notre blockchain implémentera ce trait plus tard.

Un backend de base de données pourrait faire aussi, ou un backend JSON ou autre chose. Cette fonctionnalité n'est pas du tout spécifique aux chaînes de blocs.

Vous remarquerez peut-être une notation assez inhabituelle lorsque vous n'avez pas vu le langage rust. La première chose pourrait être le

 -> ~

-notation. C'est juste la manière Rust de définir un type de retour. Si vous êtes un développeur Python, ceux-ci sont identiques aux indices de type, sauf qu'ils ne sont pas facultatifs et que votre programme ne se compilera pas si vous les vissez. La prochaine chose à savoir est que

Les types de rouilles ne peuvent pas être définis sur null

(ou Aucun si vous venez de Python; ne pas confondre cela avec Rusts Option :: None, qui n'est pas exactement la même chose) et fait pas de support Des exceptions. Par conséquent, vous voyez des valeurs de retour comme Option et Résultat. L'option indique que la fonction peut renvoyer des données valides (par exemple, Option<&Account>indique que la fonction pourrait renvoyer une référence (&) à certains Compte-instance ou non).

De même, pour indiquer une fonction qui peut échouer ou réussir (où vous pouvez utiliser des exceptions dans d'autres langues), vous pouvez utiliser le Erreur-type. C'est un peu plus impliqué dans cet exemple car il est écrit comme

-> Result<(), &'static str>

.

Considérez les deux cas: une fonction peut réussir ou échouer (peut-être pour plusieurs raisons, tout comme il existe différents types d'exceptions). Pour cette raison, vous devrez fournir un type de données pour chaque cas: le cas de réussite et le cas d'échec. Pour cette fonction, nous avons fourni

()

pour le succès et

&’static str

pour le cas d'échec. Les deux ont besoin d'explications.

le

()

indique simplement que si nous réussissons, nous ne voulons vraiment pas renvoyer d’informations supplémentaires. C'est juste un type qui ne contient aucune information (un peu comme

void

en C (++) ou plus généralement un type unitaire (12)). le

&’static str

le type est un peu plus spécial.

le

static

(

-notation) est vraiment quelque chose d'unique à Rust. C'est un soi-disant spécificateur à vie et c'est l'une des raisons pour lesquelles Rust a l'impression d'être un langage managé (comme Java, JavaScript ou Python) sans avoir besoin de Garbage Collector (13) qui nettoie le désordre que nous laissons dans la mémoire.

Et comme vous pouvez le deviner, les choses se compliquent là-bas. Nous n'allons pas creuser là-dedans. Est-ce que cet article peut fournir suffisamment d'informations (il remplirait d'autres articles entiers) ou je ne me sens pas assez mûr pour l'expliquer.

Pour l'instant: ce spécificateur de durée de vie spécifique signifie simplement que nous voulons renvoie un pointeur vers une chaîne qui est intégrée au binaire (ainsi la valeur est statique pour la durée de vie de l'ensemble du programme). La dernière chose à noter ici est que

Result

et

Option

ne sont techniquement rien d'autre que des énumérations de rouille courantes.

Nous verrons bientôt que les énumérations Rust peuvent transporter des données variables dans leur variantes, qui permet également ce cas d'utilisation exact. En raison de l'utilisation fréquente, ces Enums sont globalement disponibles dans chaque module par défaut.

Si vous avez lu les chapitres précédents, vous reconnaîtrez la plupart de la syntaxe ici, à l'exception du pub unique (crate). Il s'agit d'un modificateur d'accès spécial qui permet à toutes les méthodes avec ce projet d'accéder à la variable, contrairement aux tiers. Nous utiliserons cela dans l'exemple d'utilisation. Concentrons-nous sur les membres.

En règle générale, un bloc n'inclut pas qu'une seule action à effectuer, par conséquent transactions membre stocke un vecteur de toutes les actions qu'il comprend. Le hachage stockera une représentation normalisée de l'ensemble du bloc (y compris toutes les transactions) à l'intérieur.

Pour générer un tel hachage, une fonction de hachage est nécessaire. Vous avez peut-être entendu parler des hachages MD5 ou SHA. Ceux-là en ont entrée longue arbitraire données et produire un (presque) sortie unique et pratique tout en préservant certaines propriétés cryptographiques (14).

Étant donné que ces hachages sont l'ingrédient qui permet à une blockchain de se coller, je veux illustrer graphiquement ce que fait réellement une fonction de hachage. Notez que même le plus petit changement d'intrant produira une sortie significativement différente. C'est l'une des propriétés qu'une bonne fonction de hachage doit fournir.

Nous utiliserons cette propriété lors de l'implémentation de l'ajout de Preuve de travail dans l'article suivant.

TyCbBivxFAZs5SPIe55gVgQQvaH3 eie03zh0

Nous allons générer et stocker ce hachage du bloc dans chaque transaction. Si quelqu'un modifiait n'importe quoi dans le bloc, ces hachages ne correspondraient plus, ce qui peut facilement être vérifié par hachage à nouveau. Et voici la colle de la blockchain: Les blocs (sauf le premier) stockent également le hachage de leur bloc précédent (ici:

prev_hash

).

Donc si quelqu'un trafique avec la blockchain, cette personne invaliderait non seulement ce même bloc, mais aussi invalider tous les pointeurs vers les hachages précédents dans les blocs à venir. La blockchain entière s'effondrera.

Une blockchain en béton avec deux blocs connectés pourrait ressembler à ça:

TyCbBivxFAZs5SPIe55gVgQQvaH3 gleb3z98

Normalement, l'ajout de blocs à une blockchain distribuée est calcul intensif et / ou nécessite une autorisation. Encore une fois, nous verrons pourquoi en ajoutant le Preuve de travail dans la partie II de notre blockchain. Pour l'instant, disons simplement que cela est pratiquement impossible dans des scénarios réels à moins que les ordinateurs quantiques ne puissent changer cela.

La dernière variable, le nonce (nombre utilisé une fois) sera (miss-) utilisé plus tard aussi pour cela la preuve de travail. Nous parlerons de ce nonce également lorsque nous parlerons des transactions.

Ceci termine la description du bloc lui-même. Assurez-vous de bien comprendre le sujet des hachages et comment ils sont utilisés pour lier les blocs, car c'est le partie intégrante de la résistance à l’altération des blockchains.

Transaction

le Transaction la structure représente une demande à la blockchain qui, lors de l'exécution, conduira à un changement de WorldState (une transition d'état) et se présente comme suit:

Chaque transaction doit être liée à un utilisateur à un moment précis temps, qui sera stocké dans le

from

et le

created_at

champ, respectivement. Ce sont probablement les domaines les plus faciles à comprendre.

le

record

détient les informations réelles sur le changement que nous voulons commettre. Il représente une commande avec ses données accompagnées (

TransactionData

), que nous autorisons en tant que développeur à être transmis à la blockchain. Nous les regarderons plus tard.

le

signature

tiendra votre signe numérique du message. Ce champ s'assurera que personne, sauf la personne qui est stockée dans le

from

peut être celui qui a envoyé la transaction, c'est-à-dire que personne ne sera en mesure de soumettre des transactions au nom de quelqu'un d'autre. Par conséquent, il représente la couche de base pour authentification. Ceci sera réalisé en utilisant des clés publiques et privées que nous créerons directement à partir du code. La deuxième partie de la mise en œuvre plongera dans les détails.

Nous avons déjà vu le

nonce

dans la structure de bloc. UNE nonce représente un nombre, qui sera utilisé exactement une fois dans un but précis. C'est ce que son acronyme numéro utilisé une fois représente. Le système doit s'assurer qu'aucune transaction ayant le même nonce devrait être accepté dans le système.

Bien qu'il ne soit pas complètement implémenté, j'ai marqué la position du code où vous auriez à ajouter du code pour appliquer cette règle. Mais pourquoi quelqu'un devrait-il s'en soucier du tout? Utilisant un nonce aidera empêcher au moins trois problèmes: problèmes de réseaux, erreurs de l'utilisateur et intention malveillante. Tout cela grâce à un seul numéro.

le pensée de base est comme suit: en n'acceptant pas un nonce deux fois, un demande qui sera transmis plus d'une fois au système sera rejeté. Cela corrige (i) les piles réseau défectueuses, qui pour une raison quelconque transmettent deux fois un paquet réseau, (ii) les utilisateurs ou les interfaces utilisateur défectueuses qui, pour une raison quelconque, transmettent accidentellement le même message plusieurs fois (vous ne voulez pas envoyer le même paiement accidentellement deux fois), et (iii) un attaquant pourrait capturer la requête telle quelle et – sans aucune modification – la soumettre à nouveau.

Cette attaque peut également vous faire dépenser plusieurs fois vos pièces durement gagnées. L'attaque (qui n'est pas unique aux blockchains distribuées) est connue sous le nom de attaque de relecture pour des raisons évidentes. Si nous n'avions pas de nonce, même un le message signé ne peut pas empêcher les attaques de relecture.

Données de transaction

le

TransactionData

la structure fait partie de la transaction elle-même. Ici, nous définissons quelles actions notre blockchain devrait pouvoir traiter. Si vous souhaitez étendre le prototype et ajouter des fonctionnalités à votre blockchain personnelle, cette structure de données est le point de départ. La définition actuelle ressemble à ça:

Vous avez peut-être remarqué que cette fois, nous ne définissons pas de

struct

mais plutôt une énumération (

enum

). Fondamentalement, une énumération représente un type de données qui ne contient qu'un ensemble défini de valeurs (variantes) et seulement exactement un à la fois. La plupart des langages de programmation prennent en charge les énumérations. Cet exemple spécifique définit les quatre variantes

CreateUserAccount

,

ChangeStoreValue TransferTokens

,

CreateTokens

.

Quelque peu spécifique à Rust est que chaque variante peut tenir variable supplémentaire Les données. le

CreateUserAccount

détient exactement un anonyme valeur de type String qui représente l'ID de compte du compte utilisateur qui doit être créé.

Ces arguments peuvent également être nommés pour une meilleure clarté (comme dans

TransferTokens {to: String, amount: u128}

). Les implémentations à venir fourniront un exemple sur la façon d'utiliser et de déstructurer ces valeurs.

Compte et type de compte

Enfin, nous nous concentrerons sur la définition des comptes qui représentent le résultat de l'exécution de tous les blocs au sein de la blockchain (c'est-à-dire l'état du monde):

Un compte rendu de notre blockchain hold

tokens

qui représentent le crédit des comptes (c'est-à-dire que cela porte la valeur comme ETH ou BTC). Je n'ai pas choisi le nom des pièces, donc si vous le souhaitez, modifiez simplement ce membre à votre guise. La signification des deux autres membres peut ne pas être évidente. Commençons par le

store

. Encore une fois, ce membre détient un dictionnaire de valeurs. Comme vous l'avez peut-être remarqué, il y avait une variante

ChangeStoreValue

défini pour le

TransactionData

.

Cette transaction est destinée à écrire valeurs arbitraires dans le compte utilisateur et stockez-le dans le magasin. Nous pourrions l'utiliser, par exemple, pour stocker différents types de valeurs numériques (par exemple, des achats de musique numérique) ou des valeurs physiques (par exemple, des propriétés détenues) ou simplement toute autre information que nous aimons dans le compte.

Le membre restant

acc_type

est destiné à représenter le type de compte et est modélisé comme une énumération tout comme

TransactionData

:

Différents comptes peuvent être utilisés comme élément de base pour réaliser votre propre réalisation de autorisation. Peut-être souhaitez-vous que seuls des comptes spécifiques puissent soumettre des demandes spécifiques? Ou seuls certains comptes ont l'autorisation d'ajouter ou de vérifier des blocs, de créer des jetons ou de conserver une logique métier (comme des contrats intelligents ou du code de chaîne)? Voici le point de départ de ces implémentations.

Ceci termine les déclarations des structures de données. Si vous avez réussi jusqu'à ici: vous avez couvert presque tout ce qui est nécessaire pour comprendre les rouages ​​de cette blockchain, bien joué 😊. Le reste sera des détails gourmands dans le besoin. Mais c'est pour ça que tu es venu, non?

Implémentations de structure

Alors que la dernière section était très détaillée et couvrait presque toutes les lignes de code, cette section ne sera concentrer sur les parties importantes des implémentations. Cependant, si vous ne venez pas ici pour apprendre un peu de Rust: la plupart des détails ici sont en fait déjà traités de manière informative dans les sections précédentes. Vous pouvez simplement lire cette section.

Compte 👤

Nous commençons par la mise en œuvre de la structure du compte. J'ai choisi cette structure en premier parce qu'elle ne contient vraiment rien d'intéressant et aucune fonctionnalité du tout afin que nous puissions nous concentrer sur Rust. Alors voilà:

Les implémentations de structure commencent par

impl

mot-clé suivi de la structure que nous voulons implémenter. La seule fonction que nous voulons ajouter est la nouvelle fonction. Qui agit comme un constructeur pour le compte (une fonction qui sert à construction l'objet d'une manière définie). Rouille a pas de définition stricte pour le constructeur fonctions (comme

__init__()

in Python or a function that’s named like the class itself in Java and C++).

In fact, that the function is called new is completely arbitrary. But since constructors are so common in object-oriented thinking, defining a function named new is the de-facto standard in Rust for implementing constructors. The action that function performs is construction the Account with some default values.

You may have noticed the return type

Self

(notice the uppercase S) which is also used to construct the structure. C'est un special type that just represents the currents structure’s type (NOT its instance, which is written with lower case s). If you have Python background: that’s conceptionally similar to the

cls

parameter in a

@classmethod

) There is not much interesting going on except for the fact, that the store is initialized with an empty

HashMap

by also calling the

new

function which is an example for that de-facto standard within the standard library.

The rest of the chapter will not list constructors anymore. You can expect them to be available where reasonable, we will focus on the relevant code.

Append a block to the blockchain

The functionality to add a block to the blockchain is a core ingredient. Here you will have to enforce tout policies a block has to comply with in order to be accepted. Due to the function is very lengthy and doing multiple things the code will be snippet and explained in pieces.

This snippet defines the function

append_block

. This function should add the Block and return a

Result

indicating the success or failure of the operation (in joint with an appropriate error text). The parameter

 &mut self

indicates a reference to the instance itself (instance function) and indicates that we are going to possibly change the structures state (we possibly add a block) using the

mut

modifier.

This is similar to the (implicit)

this

pointer in C++, Java ou JavaScript or to the

self

parameter in Python. Now the only thing we do is creating an indicator variable genesis which will be true if the length of the blockchain is zero (i.e., we are adding the first block which is denoted as genesis block).

Note that even if Rust is a strongly typed language you don’t always have to provide the data type explicitly. In this case the compiler infers that we are defining a Boolean variable (bool). In modern C++ et C# you would use the

auto-

keyword or the

var

-keyword in recent Java versions.

where

verify_own_hash()

is defined as a function on Block as:

This code snippet checks first if the hash is set and if it is set it will verify if its valid by comparing it to the correct hash. This is done by calling

.is_some()

on the block’s hash member. Remember that it is declared as hash:

Option

, a variable which may or may not hold data. The indicator

.is_some()

will indicate if the variable does actually contain data.

Knowing this fact, we can safely use

.unwrap()

to get the value. This call would panique (exiting the program with error) if no value was set. Since we do not want to take ownership of the variable inside, we use

.as_ref()

to tell Rust that we just want its reference. The call to .eq on that String will compare it to the calculated hash (

.calculate_hash()

) defined for the Block.

We will inspect that function later. Si

verify_own_hash()

evaluates to

false

we will create the

Err

holding the appropriate error message. Notice the

.into()

call after the error message? This is the way for implicitly convert between compatible types.

When creating a string using “Some String” we actually create a

&str

type (i.e., a pointer to fixed length string somewhere in memory) but the result wants to store a

String

(i.e., an owned possibly mutable String, which is not exactly the same), so we need to convert those.

Next, we vérifier if the block is meant to be appended to the current last block. We do this by checking if prev_hash stores the actual the hash of the last block in the blockchain:

Even though both

prev_hash

et

hash

are of type

Option

we can actually compare them via

==

since Rust provides a standard implementation which evaluates to

true

if both

Option

s contain no value or both contain a value et both values are the same (that is done by implementing the appropriate equals trait).

This is exactly what we want, since it deals also with the genesis block correctly which will not have a previous hash (yes, since it is the first).

Now, we check if the block contains transactions. This is just a policy we enforce, you might remove it if you want to support empty blocks.

There is nothing new to that code. Let’s move on to something more involved: verifying each transaction in the block. We want the transactions to be authentic (not tampered with) and we want them to be valid, i.e., the execution of the transaction must be possible. We do not want a transaction to spent tokens, which the account does not supply. Because a block contains multiple transactions, we will need some kind of rollback-functionality.

Otherwise, if a transaction is executed correctly and later transaction is not (forcing us to reject the block) we might end up with an inconsistence World State. That’s something we do not vouloir. Since I don’t want to implement some complicated rollback algorithm, we just clone the whole state even though it really is not the best idea, since this could eat very much memory if the state is big:

Se souvenir du

#(derive(Clone))

on top of each struct? Cloning an object actually is and rather complicated operations since it has to clone all members and all structs which might contain structs which, … you get the point.

Rust lets us get away with a standard implementation by deriving from the clone trait for many standard data types.

Now we try to execute each operation on the chain to verify their validity:

Firstly, we iterate over all the blocks' transactions this is done by a

for

-loop that iterates over (

index, transaction)

-pairs that are emitted by

block.transactions.iter().enumerate()

. That’s equivalent to

enumerate()

in Python.

Fondamentalement,

.iter()

creates an enumerable that emits a reference to each transaction of the list of transaction while

.enumerate()

consumes that list and emits a pair that not only contains the transaction but also its position in the list.

Inside the loop we try to execute each transaction by calling

transaction.execute()

, which we will inspect later. le

execute

function itself indicates its success or failure by returning a

-> Result

. The statement

if let Err(err) 

est un pattern matching syntax which checks if the transaction returns an Error variant (

Err

) and if that’s the case it assigns the Errors data (the error String) to the variable

err

before executing the block within the curly brackets.

If the transaction executes correctly (i.e., if it returns the Results’ une

Ok

variant) it just goes on with the loop instead. If it fails to execute, we write the old world state (

old_state

) and return an Error ourselves. le

format!

syntax builds our actual String using placeholders.

Again, this is comparable to Pythons’ string formatting

(

"{} World"
.format("Hello")

).

Well, if everything went fine, we just append the block and indicate everything went fine by returning the

Ok

-variant:

In Part II of this series, we will reviens to this function when adding Proof of Work et authentication (signatures).

Hashing 🝖

In the last function we already used hashes and called the Block’s calculate_hash() function. If you do not know what those hashes actually are, please refer back to the Block data structure description.

So that’s how we squash a complete block into a single hash value:

This is the place where we make use of the external crate Blake2. After creating an instance of Blake2 (

hasher

) we loop over all transactions inside the current block instance (

&self

). For each transaction we call the hasher’s

.update()

fonction. Each update will feed some new data into the hasher which are some arbitrary bytes (datatype

u8

), which in this code is actually a hash itself: the hash of the transaction.

Yes: we actually hash the hashes of the transactions. Since the block itself also contains some Les données which is unique to the block, we will have to take those into account also when creating the whole block’s hash. We do this by just formatting the relevant information (previous hash and nonce) to a string and feeding it to the update function. This is somewhat lazy.

We could feed both values separately (which would make the representation more stable) but since those values are Option types we would have to account for them being values or none. This blows up the code unnecessarily.

To tell the hasher we are done adding data and start the hashing, we use its

finalize()

function, which will output the hash as an array of bytes (

(u8)

). We transform that array to a Vector of bytes and return it.

The transactions'

calculate_hash()

function looks very similar to this function, that’s why skip its details here.

Verify the blockchain 🗹

To check if the blockchain that is presented to you is valid (i.e., no one tampered with), we need to basically do the same checks (making sure the blocks are not tampered and correctly connected) as when adding the block to the blockchain, while just omitting some different messages. Refer to the Block’s function

check_validity()

for details.

Executing a transaction

Normally, the execution of a transaction comes with some change in the world state. Specifically, for this implementation this actually is a change in the

accounts

dictionary of the

Blockchain

struct. Remember, when we dealt with the world state we modeled those specifics away and said our blockchain should implement a specific interface (the

trait
WorldState

) instead, which just describes the functionality the backend (in that case the blockchain struct) should support.

Notre

Blockchain

struct implements that interface like that:

Here we define the implementation of trait

WorldState

pour le

Blockchain

. Specifically, that snippet shows the implementation of the function

get_user_ids()

, which just returns all keys of our accounts directory.

Because we did not come around that kind of syntax for now, let me explain this expression. le

.keys()

function returns all indices of the

accounts

(which represent the unique ids of the accounts). We could use the keys as iterable in a f

or id in …

loop.

But instead we make use of functional programming paradigms here. If you coded in R ou Python ou JavaScript you probably came around the

map()

fonction. This is basically just a function which takes every element in an iterable (here: the account ids) and feeds them into another function, which can transform leur. This idiom is technically referred to as function composition(15).

Here, the inline function

|s| s.clone()

really just takes each value into

s

(which is actually of type

&String

) and clones that value into a new position in memory. Le final

.collect()

just turns the resulting iterable into the return type

Vec

.

Now having that settled, we can now come to the actual implementation of the

execute()

-function in the transaction which starts like that:

This looks a bit weird if you’re not used to template parameters. The interesting part might be the

syntax right after the functions’ name

execute

. You can read the whole line like that: declare a public function named

execute

which takes a parameter world_state

 

whose type conforms to the

trait
WorldState

.

You basically declare a new type and set the constraint that this very type should implement the

WorldState

. You do not fix it to any specific concrete type (we could easily just remove the

T

and replace it with the declaration

world_state: &mut Blockchain

), which is arguably easier to read. However, this implementation allows us to use any transaction we define on any backend that implements the WorldState trait.

This backend could easily be a PostreSQL ou MySQL handler. The actions we perform are for the most parts not specific to blockchains.

Having this covered, lets take a look in the implementation of the

execute

function which goes on like that:

Before each execution we vérifier if the user who wants to execute it really exists. Here we use the

trait

’s function

get_account_by_id()

which is defined as

fn get_account_by_id(&self, id: &String) -> Option<&Account>

(i.e., as a function which might return an

account

or not).

Again, we destructure that

Option

by using the i

f let

pattern matching syntax which will execute the block if the function returns “some account”. If it doesn’t, we drop an error message.

After the pre-checks follows the implementation of the transaction’s specific logic pieces:

The statement match

&self.record

is just another way of pattern matching. Recall that the record is of type

TransactionData

which is an enumeration holding the action to execute and possibly additional data needed for execution. Here we test for each possible variant of the enumeration

TransactionData

and execute the appropriate actions on the

WorldState

if possible.

If we fail to do so for any reason, we will return an

Err

(which would force the blockchain implementation to rollback the state). If you are interested in the concrete realizations of the omitted commands, I encourage you to watch the source 😊.

Those are all the implementations for the first part. If you made it until here and where no Rust developer, I am aware this was a lot to cover. Do not worry if some syntax still feels alien.

Some of it does for me still. Cependant, le great error messages spilled out by the compiler are often quite helpful in spotting the problems. If you write code, its sometimes even fun to fix them and make the compiler happy (I fixed like a hundred of them while writing this little piece of code…).

If you compile the code and see some warning indicating unused variables of code and unconstructed variants: you can safely ignore them. Most of them are just some preparations for Part II of this writeup or examples.

Usage example

This example will guide you through

  1. Initializing a valid blockchain adding some transactions (adding users, transferring tokens)
  2. Tampering with a transaction and detecting it
  3. Smarter tampering with a transaction, still detecting it

The contents of the following file are found in the main.rs fichier. If you read the implementations above, this code will be fairly simple to understand.

We start by declaring our module (

rchain

), import the dependencies, define the main function and create our blockchain object:

Like in C(++) or Java, a Rust program have a standard entry point declared by a function named

main()

, which we defined in that snippet.

Now we create a genesis block and define the users we want to create:

After doing so we loop over the

initial_users

vector in order to create transactions that create those users in the blockchain and assign them 100,000,000 tokens each:

The last lines add all the transactions to the genesis block. The value 0 in the transaction’s constructor should be the nonce. However, since we do not enforce this nonce at the blockchain right now we just fill it with a dummy zero.

It will be a good starting point for your own coding aventures to implement the policy to enforce of a correct nonce.

The created and filled block is now added to the blockchain. The result of the append function will be printed after which the whole blockchain also will be printed as a string representation:

le

{:#?}

formatter is used for pretty-printing. The output of the object will look almost like a valid JSON-object with spacings nicely laid out, whereas

{:?}

will not print out those spacings.

You should see the line Genesis block successfully added: Ok(()) in your terminal, after which the blockchain output starts. Notice the printout of the “accounts” which should contain alice and bob both having 100,000,000 tokens.

Now we add another block containing an action to transfer one token from alice to bob. The blockchain will again be printed. You should notice the changes in the balance of the accounts.

The last line should report a valid blockchain as Blockchain valid: Ok(()).

Now that everything seems fine, the first attack will start. Bob was not satisfied with this one coin Alice transferred. So, he decides to directly change the transaction in the blockchain.

Since we do not support signed messages for now, this can be easily done:

We first clone our working blockchain to have a new one to operate on which has the same state. Then we get ourselves a mutable reference to the second transaction in the first block using

bc_attack_1.blocks(1).transactions(0).borrow_mut()

. Now we destructure the record which is stored in the transaction (remember: this is a

TransactionData

struct) using the match-syntax.

C'est le

TransferTokens

action we wanted to manipulate. Since the values should be changed in-place (directly in the memory where they are stored), we have to account for that fact by using the appropriate patterns for the match construct (

&mut

,

 ref mut amount

).

The binding syntax

{to: _, ...}

just indicates that we do not care for that

to

variable and silence a compiler warning indicating the declaration without usage.

Now we set the value at the memory position where amount refers to, to 100, which is Bob’s desired amount of coins, using

*amount=100

. After doing so we check the blockchain again:

Which should print: Is the Blockchain still valid? Err(“Stored hash for Block #2 does not match calculated hash (Code: 665234234)”,)

→ The blockchain is invalid.

While Bob failed, Alice also wants to try her luck. Since she didn’t want to steal coins from Bob, she changed the

CreateTokens

action in the genesis block to create her account having 100,000,000,000 tokens to start with.

Also, she noticed Bob’s mistake and didn’t want to run into the same trap, so she decided to also update the manipulated blocks hash. The basic attack looks conceptually the same as Bob’s attack.

However, she added the following line after changing the transaction:

will print something like (your output will slightly differ):

Is the Blockchain still valid? Err("Block #1 is not connected to previous block (Hashes do not match. Should be `ÜQu{14}²Jüø9É7x;u{86}æÇÐu{8d}À¤þMy

→ The blockchain broke again 🎉🥳

Conclusions and outlook

This writeup covered a lot of blockchain basics and went down the rabbit hole by implementing the blockchain ledger as a simple prototype using the rather new programming language Rust. The usage of the implementation was demonstrated showing two possible attacks to the blockchain ledger.

The current state of the implementation covers a blockchain prototype, which supports multiple transactions which can easily be extended. It supports a simple rollback functionality in case transactions cannot be executed.

le upcoming Part II will add the following functionality:

  • Sign transactions using public/private keys to make them tamper proof
  • Add a transaction queue to the blockchain which holds requested transactions that are not yet executed (candidates for blocks)
  • Ajouter Proof of Work to secure the blockchain against tampering

I hope you had a nice time reading through all the texts. Feel free to go on implementing some of the missing functionality and bring you own ideas. Il y a quelques

@TODO

markers which are a good starting point.

I am happy to hear any suggestions for improvements. If there is something that can be done better or easier, if there is something you’d like to add, if you see mistakes or if you need some more explanation somewhere: feel free to write me any suggestions. Just drop me a message. I hope to see you in Part II.

References 📚

Mots clés

The Noonification banner

Subscribe to get your daily round-up of top tech stories!



Traduction de l'article de mereep : Article Original

BlockBlog

Le Meilleur de l'Actualité Blockchain Francophone & Internationale | News, Guides, Avis & Tutoriels pour s'informer et démarrer facilement avec Bitcoin, les Crypto-Monnaies et le Blockchain. En Savoir Plus sur L'Équipe BlockBlog

Commenter cet Article

Commenter cet Article

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Plus dans News

Les Plus Populaires

Acheter des Bitcoin

Acheter des Alt-Coins

Sécuriser vos Cryptos

Vêtements et Produits Dérivés

Top