Qu’est-ce qu’Ethereum ?

Bannière Amazon du livre Maîtriser Ethereum

Ethereum est souvent décrit comme "l’ordinateur du monde." Mais qu’est-ce que cela signifie ? Commençons par une description axée sur l’informatique, puis essayons de décrypter cela avec une analyse plus pratique des capacités et des caractéristiques d’Ethereum, tout en le comparant à Bitcoin et à d’autres plateformes d’échange d’informations décentralisées (ou « chaînes de blocs » pour court).

D’un point de vue informatique, Ethereum est une machine à états déterministe mais pratiquement illimitée, composée d’un état singleton globalement accessible et d’une machine virtuelle qui applique des modifications à cet état.

D’un point de vue plus pratique, Ethereum est une infrastructure informatique à source libre décentralisée à l’échelle mondiale qui exécute des programmes appelés contrats intelligents. Il utilise une chaîne de blocs pour synchroniser et stocker les changements d’état du système, ainsi qu’une cryptomonnaie appelée ether pour mesurer et limiter les coûts des ressources d’exécution.

La plate-forme Ethereum permet aux développeurs de créer de puissantes applications décentralisées avec des fonctions économiques intégrées. Tout en offrant une disponibilité, une auditabilité, une transparence et une neutralité élevées, il réduit ou élimine également la censure et réduit certains risques de contrepartie.

Comparé à Bitcoin

Beaucoup de gens viendront à Ethereum avec une expérience préalable des cryptomonnaies, en particulier du Bitcoin. Ethereum partage de nombreux éléments communs avec d’autres chaînes de blocs ouvertes : un réseau pair à pair reliant les participants, un algorithme de consensus byzantin tolérant aux pannes pour la synchronisation des mises à jour d’état (une chaîne de blocs de preuve de travail), l’utilisation de primitives cryptographiques telles que le numérique signatures et hachages, et une monnaie numérique (ether).

Pourtant, à bien des égards, le but et la construction d’Ethereum sont étonnamment différents de ceux des chaînes de blocs ouvertes qui l’ont précédé, y compris Bitcoin.

Le but d’Ethereum n’est pas principalement d’être un réseau de paiement en monnaie numérique. Alors que la monnaie numérique ether fait à la fois partie intégrante et nécessaire au fonctionnement d’Ethereum, l’ether est conçu comme une monnaie d’utilité pour payer l’utilisation de la plate-forme Ethereum en tant qu’ordinateur mondial.

Contrairement à Bitcoin, qui a un langage de script très limité, Ethereum est conçu pour être une chaîne de blocs programmable à usage général qui exécute une machine virtuelle capable d’exécuter du code d’une complexité arbitraire et illimitée. Là où le langage de script de Bitcoin est, intentionnellement, contraint à une simple évaluation vrai/faux des conditions de dépenses, le langage d’Ethereum est Turing complet, ce qui signifie qu’Ethereum peut fonctionner directement comme un ordinateur à usage général.

Composantes d’une chaîne de blocs

Les composantes d’une chaîne de blocs ouverte et publique sont (généralement) :

  • Un réseau pair à pair (P2P) connectant les participants et propageant les transactions et les blocs de transactions vérifiées, basé sur un "échange" protocolaire standardisé.

  • Messages, sous forme de transactions, représentant des transitions d’état

  • Un ensemble de règles consensuelles, régissant ce qui constitue une transaction et ce qui constitue une transition d’état valide

  • Une machine à états qui traite les transactions selon les règles du consensus

  • Une chaîne de blocs cryptographiquement sécurisés qui agit comme un journal de toutes les transitions d’état vérifiées et acceptées

  • Un algorithme de consensus qui décentralise le contrôle sur la chaîne de blocs, en forçant les participants à coopérer à l’application des règles de consensus

  • Un schéma d’incitation théoriquement valable (par exemple, les coûts de la preuve de travail plus les récompenses globales) pour sécuriser économiquement la machine d’état dans un environnement ouvert.

  • Une ou plusieurs implémentations logicielles à source libre des éléments ci-dessus ("clients")

Tous ou la plupart de ces composants sont généralement combinés dans un seul client logiciel. Par exemple, dans Bitcoin, l’implémentation de référence est développée par le projet à source libre Bitcoin Core et implémentée en tant que client bitcoind. Dans Ethereum, plutôt qu’une implémentation de référence, il existe une spécification de référence, une description mathématique du système dans le livre jaune (voir Lectures complémentaires). Il existe un certain nombre de clients, qui sont construits selon la spécification de référence.

Dans le passé, nous utilisions le terme "chaîne de blocs" pour représenter tous les composants que nous venons d’énumérer, comme une référence abrégée à la combinaison de technologies qui englobent toutes les caractéristiques décrites. Aujourd’hui, cependant, il existe une grande variété de chaînes de blocs avec des propriétés différentes. Nous avons besoin de qualificatifs pour nous aider à comprendre les caractéristiques de la chaîne de blocs en question, telles que ouverte, publique, globale, décentralisée, neutre, et résistante à la censure, pour identifier les caractéristiques émergentes importantes d’un système "chaîne de blocs" que ces composants permettent.

Toutes les chaînes de blocs ne sont pas créées égales. Quand quelqu’un vous dit que quelque chose est une chaîne de blocs, vous n’avez pas reçu de réponse ; vous devez plutôt commencer à poser beaucoup de questions pour clarifier ce qu’elles veulent dire lorsqu’elles utilisent le mot "chaîne de blocs". Commencez par demander une description des composants de la liste précédente, puis demandez si cette "chaîne de blocs" présente les caractéristiques d’être ouverte, publique, etc.

La naissance d’Ethereum

Toutes les grandes innovations résolvent de vrais problèmes, et Ethereum ne fait pas exception. Ethereum a été conçu à une époque où les gens reconnaissaient la puissance du modèle Bitcoin et essayaient d’aller au-delà des applications de cryptomonnaie. Mais les développeurs étaient confrontés à une énigme : ils devaient soit construire sur Bitcoin, soit démarrer une nouvelle chaîne de blocs. S’appuyer sur Bitcoin signifiait vivre dans les limites intentionnelles du réseau et essayer de trouver des solutions de contournement. L’ensemble limité de types de transactions, de types de données et de tailles de stockage de données semblait limiter les types d’applications pouvant s’exécuter directement sur Bitcoin ; tout le reste nécessitait des couches hors chaîne supplémentaires, ce qui annulait immédiatement bon nombre des avantages de l’utilisation d’une chaîne de blocs publique. Pour les projets qui avaient besoin de plus de liberté et de flexibilité tout en restant pertinent, une nouvelle chaîne de blocs était la seule option. Mais cela signifiait beaucoup de travail : démarrage de tous les éléments de l’infrastructure, tests exhaustifs, etc.

Vers la fin de 2013, Vitalik Buterin, un jeune programmeur et passionné de Bitcoin, a commencé à réfléchir à l’extension des capacités de Bitcoin et Mastercoin (un protocole Bitcoin de superposition étendu pour offrir des contrats intelligents rudimentaires). En octobre de la même année, Vitalik a proposé une approche plus généralisée à l’équipe Mastercoin, qui permettait à des contrats flexibles et scriptables (mais pas Turing-complets) de remplacer le langage contractuel spécialisé de Mastercoin. Bien que l’équipe Mastercoin ait été impressionnée, cette proposition était un changement trop radical pour s’intégrer dans leur feuille de route de développement.

En décembre 2013, Vitalik a commencé à partager un livre blanc qui décrivait l’idée derrière Ethereum : une chaîne de blocs à usage général complète de Turing. Quelques dizaines de personnes ont vu cette première ébauche et ont fait part de leurs commentaires, aidant Vitalik à faire évoluer la proposition.

Les deux auteurs de ce livre ont reçu une première ébauche du livre blanc et l’ont commentée. Andreas M. Antonopoulos a été intrigué par l’idée et a posé de nombreuses questions à Vitalik sur l’utilisation d’une chaîne de blocs distincte pour appliquer des règles de consensus sur l’exécution de contrats intelligents et les implications d’un langage Turing-complet. Andreas a continué à suivre les progrès d’Ethereum avec beaucoup d’intérêt, mais en était aux premiers stades de l’écriture de son livre Mastering Bitcoin, et n’a participé directement à Ethereum que bien plus tard. Dr. Gavin Wood, cependant, a été l’une des premières personnes à contacter Vitalik et à lui proposer de l’aider avec ses compétences en programmation C++. Gavin est devenu le cofondateur, le codesigner et le CTO d’Ethereum.

Comme le raconte Vitalik dans son article "Ethereum Prehistory" :

C’était l’époque où le protocole Ethereum était entièrement ma propre création. À partir de là, cependant, de nouveaux participants ont commencé à rejoindre le giron. De loin, le plus important du côté du protocole était Gavin Wood…​

Gavin peut également être largement crédité du changement subtil de vision d’Ethereum comme une plate-forme pour créer de l’argent programmable, avec des contrats basés sur la chaîne de blocs qui peuvent contenir des actifs numériques et les transférer selon des règles prédéfinies, à une plate-forme informatique à usage général. Cela a commencé par de subtils changements d’accent et de terminologie, et plus tard cette influence s’est renforcée avec l’accent croissant mis sur l’ensemble "Web 3", qui considérait Ethereum comme un élément d’une suite de technologies décentralisées, les deux autres étant Whisper et Swarm.

À partir de décembre 2013, Vitalik et Gavin ont affiné et fait évoluer l’idée, construisant ensemble la couche de protocole qui est devenue Ethereum.

Les fondateurs d’Ethereum pensaient à une chaîne de blocs sans but précis, qui pourrait prendre en charge une grande variété d’applications en étant programmée. L’idée était qu’en utilisant une chaîne de blocs à usage général comme Ethereum, un développeur pouvait programmer son application particulière sans avoir à mettre en œuvre les mécanismes sous-jacents des réseaux pair à pair, des chaînes de blocs, des algorithmes de consensus, etc. La plateforme Ethereum a été conçue pour abstraire ces détails en fournissant un environnement de programmation déterministe et sécurisé pour les applications chaîne de blocs décentralisées.

Tout comme Satoshi, Vitalik et Gavin n’ont pas simplement inventé une nouvelle technologie ; ils ont combiné de nouvelles inventions avec des technologies existantes d’une manière nouvelle et ont livré le code prototype pour prouver leurs idées au monde.

Les fondateurs ont travaillé pendant des années, construisant et affinant la vision. Et le 30 juillet 2015, le premier bloc Ethereum a été miné. L’ordinateur du monde a commencé à servir le monde.

Note

L’article de Vitalik Buterin "A Prehistory of Ethereum" a été publié en septembre 2017 et offre une vue fascinante à la première personne des premiers instants d’Ethereum.

Les quatre étapes de développement d’Ethereum

Le développement d’Ethereum a été planifié en quatre étapes distinctes, avec des changements majeurs à chaque étape. Une étape peut inclure des sous-versions, appelées "embranchements divergents" (hard forks), qui modifient les fonctionnalités d’une manière qui n’est pas rétrocompatible .

Les quatre principales étapes de développement portent le nom de code Frontier, Homestead, Metropolis et Serenity. Les embranchements divergents intermédiaires qui se sont produits (ou sont prévus) à ce jour portent les noms de code Ice Age, DAO, Tangerine Whistle, Spurious Dragon, Byzantium et Constantinople. Les étapes de développement et les embranchements divergents intermédiaires sont présentées sur la chronologie suivante, qui est "datée" par numéro de bloc :

Bloc #0

Frontier—L’étape initiale d’Ethereum, du 30 juillet 2015 à mars 2016.

Bloc #200 000

Ice Age—Un embranchement divergent pour introduire une augmentation exponentielle de la difficulté, pour motiver une transition vers PoS lorsque prêt.

Bloc #1 150 000

Homestead—La deuxième étape d’Ethereum, lancée en mars 2016.

Bloc #1 192 000

DAO—Un embranchement divergent qui a remboursé les victimes du contrat DAO piraté et a provoqué la scission d’Ethereum et d’Ethereum Classic en deux systèmes concurrents.

Bloc #2 463 000

Tangerine Whistle—Un embranchement divergent pour modifier le calcul du gaz pour certaines opérations lourdes en E/S et pour effacer l’état accumulé d’un déni de service (DoS) attaque qui a exploité le faible coût du gaz de ces opérations.

Bloc #2 675 000

Spurious Dragon—Un embranchement divergent pour traiter plus de vecteurs d’attaque DoS, et un autre effacement d’état. En outre, un mécanisme de protection contre les attaques par relecture.

Bloc #4 370 000

Metropolis Byzantium—Metropolis est la troisième étape d’Ethereum, en cours au moment de la rédaction de ce livre, lancée en octobre 2017. Byzance est le premier des deux embranchements divergents prévus pour Metropolis.

Après Byzance, il y a un autre embranchement divergent prévu pour Metropolis : Constantinople. Metropolis sera suivi de la dernière étape du déploiement d’Ethereum, baptisée Serenity.

Ethereum : une chaîne de blocs à usage général

La chaîne de blocs d’origine, à savoir la chaîne de blocs de Bitcoin, suit l’état de unités de bitcoin et leur propriété. Vous pouvez considérer Bitcoin comme une machine à états à consensus distribué, où les transactions provoquent une transition d’état globale, modifiant la propriété des pièces. Les transitions d’état sont contraintes par les règles du consensus, permettant à tous les participants de (éventuellement) converger vers un état commun (consensus) du système, après que plusieurs blocs aient été minés.

Ethereum est également une machine à états distribuée. Mais au lieu de suivre uniquement l’état de la propriété de la devise, Ethereum suit les transitions d’état d’un magasin de données à usage général, c’est-à-dire un magasin pouvant contenir toutes les données exprimables en tant que uplet clé–valeur. Un magasin de données clé-valeur contient des valeurs arbitraires, chacune référencée par une clé ; par exemple, la valeur "Mastering Ethereum" référencée par la clé "Titre du livre". À certains égards, cela sert le même objectif que le modèle de stockage de données de Random Access Memory (RAM) utilisé par la plupart des ordinateurs à usage général. Ethereum a une mémoire qui stocke à la fois le code et les données, et il utilise la chaîne de blocs Ethereum pour suivre l’évolution de cette mémoire au fil du temps. Comme un ordinateur à programme stocké à usage général, Ethereum peut charger du code dans sa machine d’état et exécuter ce code, en stockant les changements d’état résultants dans sa chaîne de blocs. Deux des différences critiques par rapport à la plupart des ordinateurs à usage général sont que les changements d’état d’Ethereum sont régis par les règles du consensus et que l’état est distribué à l’échelle mondiale. Ethereum répond à la question : "Et si nous pouvions suivre n’importe quel état arbitraire et programmer la machine à états pour créer un ordinateur mondial fonctionnant par consensus ?"

Composants d’Ethereum

Dans Ethereum, les composantes d’un système de chaîne de blocs décrit dans Composantes d’une chaîne de blocs sont, plus précisément :

Réseau P2P

Ethereum fonctionne sur le réseau principal Ethereum, qui est adressable sur le port TCP 30303, et exécute un protocole appelé ÐΞVp2p.

Règles de consensus

Les règles de consensus d’Ethereum sont définies dans la spécification de référence, le Yellow Paper (voir Lectures complémentaires).

Transactions

Les transactions Ethereum sont des messages réseau qui incluent (entre autres) un expéditeur, un destinataire, une valeur et une charge utile de données.

Machine d’état

Les transitions d’état Ethereum sont traitées par la Ethereum Virtual Machine (EVM), une machine virtuelle basée sur la pile qui exécute le bytecode (code intermédiaire ou instructions en langage machine). Les programmes EVM, appelés « contrats intelligents », sont écrits dans des langages de haut niveau (par exemple, Solidity) et compilés en code intermédiaire pour être exécutés sur l’EVM.

Structures de données

L’état d’Ethereum est stocké localement sur chaque nœud en tant que base de données (généralement LevelDB de Google), qui contient les transactions et l’état du système dans une structure de données hachée sérialisée appelée Arbre Merkle Patricia.

Algorithme de consensus

Ethereum utilise le modèle de consensus de Bitcoin, Nakamoto Consensus, qui utilise des blocs séquentiels à signature unique, pondérés en importance par PoW pour déterminer la chaîne la plus longue et donc l’état actuel. Cependant, il est prévu de passer à un système de vote pondéré PoS, nommé Casper, dans un proche avenir.

Sécurité économique

Ethereum utilise actuellement un algorithme PoW appelé Ethash, mais cela finira par être abandonné avec le passage au PoS à un moment donné dans le futur.

Clients

Ethereum a plusieurs implémentations interopérables du logiciel client, dont les plus importantes sont Go-Ethereum (Geth) et Parity.

Lectures complémentaires

Les références suivantes fournissent des informations supplémentaires sur les technologies mentionnées ici :

Complétude d’Ethereum et de Turing

Dès que vous commencez à lire sur Ethereum, vous rencontrerez immédiatement le terme "Turing complet". Ethereum, disent-ils, contrairement à Bitcoin, est Turing complet. Qu’est-ce que cela veut dire exactement?

Le terme fait référence au mathématicien anglais Alan Turing, qui est considéré comme le père de l’informatique. En 1936, il crée un modèle mathématique d’ordinateur consistant en une machine à états qui manipule des symboles en les lisant et en les écrivant sur une mémoire séquentielle (ressemblant à une bande de papier de longueur infinie). Avec cette construction, Turing a continué à fournir une base mathématique pour répondre (par la négative) aux questions sur la calculabilité universelle, c’est-à-dire si tous les problèmes peuvent être résolus. Il a prouvé qu’il existe des classes de problèmes qui ne sont pas calculables. Plus précisément, il a prouvé que le problème d’arrêt (s’il est possible, étant donné un programme arbitraire et son entrée, de déterminer si le programme finira par s’arrêter) n’est pas résoluble.

Alan Turing a en outre défini un système comme étant Turing complet s’il peut être utilisé pour simuler n’importe quelle machine de Turing. Un tel système s’appelle une machine de Turing universelle (UTM).

La capacité d’Ethereum à exécuter un programme stocké, dans une machine à états appelée Ethereum Virtual Machine, tout en lisant et en écrivant des données dans la mémoire en fait un système Turing complet et donc un UTM. Ethereum peut calculer n’importe quel algorithme pouvant être calculé par n’importe quelle machine de Turing, compte tenu des limites de la mémoire finie.

L’innovation révolutionnaire d’Ethereum consiste à combiner l’architecture informatique à usage général d’un ordinateur à programme stocké avec une chaîne de blocs décentralisée, créant ainsi un ordinateur mondial distribué à un seul état (singleton). Les programmes Ethereum s’exécutent "partout", mais produisent un état commun qui est sécurisé par les règles de consensus.

Complétude de Turing en tant que "fonctionnalité"

En entendant qu’Ethereum est Turing complet, vous pourriez arriver à la conclusion qu’il s’agit d’une fonctionnalité qui manque d’une manière ou d’une autre dans un système qui est incomplet de Turing. C’est plutôt le contraire. La complétude de Turing est très facile à réaliser ; en fait, http://bit.ly/2ABft33 [la machine d’état Turing complète la plus simple connue] a 4 états et utilise 6 symboles, avec une définition d’état qui ne compte que 22 instructions. En effet, il arrive parfois que des systèmes soient « accidentellement Turing complets ». Une référence amusante de tels systèmes peut être trouvée à http://bit.ly/2Og1VgX.

Cependant, l’exhaustivité de Turing est très dangereuse, en particulier dans les systèmes à accès ouvert comme les chaînes de blocs publiques, en raison du problème d’arrêt que nous avons évoqué plus tôt. Par exemple, les imprimantes modernes sont Turing complètes et peuvent recevoir des fichiers à imprimer qui les envoient dans un état figé. Le fait qu’Ethereum soit Turing complet signifie que n’importe quel programme de n’importe quelle complexité peut être calculé par Ethereum. Mais cette flexibilité pose des problèmes épineux de sécurité et de gestion des ressources. Une imprimante qui ne répond pas peut être éteinte et rallumée. Ce n’est pas possible avec une chaîne de blocs publique.

Implications de la complétude de Turing

Turing a prouvé que vous ne pouvez pas prédire si un programme se terminera en le simulant sur un ordinateur. En termes simples, nous ne pouvons pas prédire le chemin d’un programme sans l’exécuter. Les systèmes Turing-complets peuvent s’exécuter en "boucles infinies", un terme utilisé (en simplifiant à l’extrême) pour décrire un programme qui ne se termine pas. Il est trivial de créer un programme qui exécute une boucle qui ne se termine jamais. Mais des boucles sans fin involontaires peuvent survenir sans avertissement, en raison d’interactions complexes entre les conditions de départ et le code. Dans Ethereum, cela pose un défi : chaque nœud participant (client) doit valider chaque transaction, en exécutant tous les contrats intelligents qu’il appelle. Mais comme Turing l’a prouvé, Ethereum ne peut pas prédire si un contrat intelligent prendra fin, ou combien de temps il durera, sans réellement l’exécuter (éventuellement pour toujours). Que ce soit par accident ou exprès, un contrat intelligent peut être créé de telle sorte qu’il s’exécute indéfiniment lorsqu’un nœud tente de le valider. Il s’agit en fait d’une attaque DoS. Et bien sûr, entre un programme qui prend une milliseconde à valider et un autre qui s’exécute indéfiniment, il existe une gamme infinie de programmes désagréables, monopolisant les ressources, gonflant la mémoire et provoquant une surchauffe du processeur qui gaspillent simplement des ressources. Dans un ordinateur mondial, un programme qui abuse des ressources arrive à abuser des ressources mondiales. Comment Ethereum limite-t-il les ressources utilisées par un contrat intelligent s’il ne peut pas prédire l’utilisation des ressources à l’avance ?

Pour répondre à ce défi, Ethereum introduit un mécanisme de mesure appelé gaz. Comme l’EVM exécute un contrat intelligent, il comptabilise soigneusement chaque instruction (calcul, accès aux données, etc.). Chaque instruction a un coût prédéterminé en unités de gaz. Lorsqu’une transaction déclenche l’exécution d’un contrat intelligent, elle doit inclure une quantité de gaz qui fixe la limite supérieure de ce qui peut être consommé en exécutant le contrat intelligent. L’EVM terminera l’exécution si la quantité de gaz consommée par le calcul dépasse le gaz disponible dans la transaction. Le gaz est le mécanisme utilisé par Ethereum pour permettre un calcul complet de Turing tout en limitant les ressources que tout programme peut consommer.

La question suivante est, "comment obtenir du gaz pour payer le calcul sur l’ordinateur mondial Ethereum?" Vous ne trouverez pas de gaz sur les échanges. Il ne peut être acheté que dans le cadre d’une transaction et ne peut être acheté qu’avec de l’ether. L’ether doit être envoyé avec une transaction et il doit être explicitement affecté à l’achat de gaz, avec un prix du gaz acceptable. Comme à la pompe, le prix de l’essence n’est pas fixe. Le gaz est acheté pour la transaction, le calcul est exécuté et tout gaz non utilisé est remboursé à l’expéditeur de la transaction.

Des chaînes de blocs à usage général aux applications décentralisées (DApps)

Ethereum a commencé comme un moyen de créer une chaîne de blocs à usage général qui peut être programmé pour une variété d’utilisations. Mais très rapidement, la vision d’Ethereum s’est élargie pour devenir une plateforme de programmation de DApps. Les DApps représentent une perspective plus large que les contrats intelligents. Un DApp est, à tout le moins, un contrat intelligent et une interface utilisateur Web. Plus généralement, une DApp est une application Web qui repose sur des services d’infrastructure ouverts, décentralisés et pair à pair.

Une DApp est composée d’au moins :

  • Contrats intelligents sur une chaîne de blocs

  • Une interface utilisateur Web frontale

De plus, de nombreux DApps incluent d’autres composants décentralisés, tels que :

  • Un protocole et une plateforme de stockage décentralisé (P2P)

  • Un protocole et une plateforme de messagerie décentralisée (P2P)

Tip

Vous pouvez voir des DApps orthographiés comme ÐApps. Le caractère Ð est le caractère latin appelé "ETH", faisant allusion à Ethereum. Pour afficher ce caractère, utilisez le point de code Unicode 0xD0, ou si nécessaire l’entité caractère HTML eth (ou entité décimale #208).

Le troisième âge d’Internet

En 2004, le terme "Web 2.0" a pris de l’importance, décrivant une évolution du Web vers un contenu généré par l’utilisateur, des interfaces réactives et l’interactivité. Web 2.0 n’est pas une spécification technique, mais plutôt un terme décrivant le nouveau centre d’intérêt des applications Web.

Le concept de DApps est destiné à faire passer le World Wide Web à sa prochaine étape d’évolution naturelle, en introduisant la décentralisation avec des protocoles pair à pair dans tous les aspects d’une application Web. Le terme utilisé pour décrire cette évolution est web3, c’est-à-dire la troisième "version" du web. Proposé pour la première fois par le Dr Gavin Wood, web3 représente une nouvelle vision et une nouvelle orientation pour les applications Web : des applications détenues et gérées de manière centralisée aux applications basées sur des protocoles décentralisés .

Dans les chapitres suivants, nous explorerons la bibliothèque JavaScript Ethereum web3.js, qui relie les applications JavaScript qui s’exécutent dans votre navigateur avec la chaîne de blocs Ethereum. La bibliothèque web3.js comprend également une interface vers un réseau de stockage P2P appelé Swarm et un service de messagerie P2P appelé Whisper. Avec ces trois composants inclus dans une bibliothèque JavaScript exécutée dans votre navigateur Web, les développeurs disposent d’une suite complète de développement d’applications qui leur permet de créer des DApps web3.

Culture de développement d’Ethereum

Jusqu’à présent, nous avons expliqué en quoi les objectifs et la technologie d’Ethereum diffèrent de ceux des autres chaînes de blocs qui l’ont précédé , comme Bitcoin. Ethereum a également une culture de développement très différente.

Dans Bitcoin, le développement est guidé par des principes conservateurs : tous les changements sont soigneusement étudiés pour s’assurer qu’aucun des systèmes existants ne soit perturbé. Pour la plupart, les modifications ne sont mises en œuvre que si elles sont rétrocompatibles. Les clients existants sont autorisés à s’inscrire, mais continueront à fonctionner s’ils décident de ne pas effectuer la mise à niveau.

Dans Ethereum, en comparaison, la culture de développement de la communauté est axée sur l’avenir plutôt que sur le passé. Le mantra (pas tout à fait sérieux) est "avancez vite et cassez des choses". Si un changement est nécessaire, il est mis en œuvre, même si cela signifie invalider les hypothèses précédentes, rompre la compatibilité ou forcer les clients à se mettre à jour. La culture de développement d’Ethereum se caractérise par une innovation et une évolution rapide et une volonté de déployer des améliorations tournées vers l’avenir, même si cela se fait au détriment d’une certaine rétrocompatibilité.

Cela signifie pour vous, en tant que développeur, que vous devez rester flexible et être prêt à reconstruire votre infrastructure à mesure que certaines des hypothèses sous-jacentes changent. L’un des grands défis auxquels sont confrontés les développeurs d’Ethereum est la contradiction inhérente entre le déploiement de code sur un système immuable et une plate-forme de développement en constante évolution. Vous ne pouvez pas simplement "mettre à niveau" vos contrats intelligents. Vous devez être prêt à en déployer de nouveaux, à migrer les utilisateurs, les applications et les fonds, et à recommencer.

Ironiquement, cela signifie également que l’objectif de construire des systèmes avec plus d’autonomie et moins de contrôle centralisé n’est toujours pas pleinement atteint. L’autonomie et la décentralisation nécessitent un peu plus de stabilité dans la plate-forme que vous n’obtiendrez probablement dans Ethereum dans les prochaines années. Afin de "faire évoluer" la plateforme, vous devez être prêt à supprimer et redémarrer vos contrats intelligents, ce qui signifie que vous devez conserver un certain degré de contrôle sur eux.

Mais, du côté positif, Ethereum avance très vite. Il y a peu d’opportunités pour la "bike-shedding", une expression qui signifie retarder le développement en se disputant sur des détails mineurs tels que la façon de construire le garage à vélos à l’arrière d’une centrale nucléaire. Si vous commencez à faire du vélo, vous découvrirez peut-être soudainement que pendant que vous étiez distrait, le reste de l’équipe de développement a changé le plan et a abandonné les vélos en faveur de l’aéroglisseur autonome.

A terme, le développement de la plateforme Ethereum ralentira et ses interfaces deviendront fixes. Mais en attendant, l’innovation est le principe moteur. Vous feriez mieux de suivre, car personne ne ralentira pour vous.

Pourquoi apprendre Ethereum ?

Les chaînes de blocs ont une courbe d’apprentissage très abrupte, car elles combinent plusieurs disciplines en un seul domaine: programmation, sécurité de l’information, cryptographie, économie, systèmes distribués, réseaux pair à pair, etc. Ethereum rend cette courbe d’apprentissage beaucoup moins abrupte, vous pouvez donc démarrer rapidement. Mais juste sous la surface d’un environnement d’une simplicité trompeuse se cache bien plus. Au fur et à mesure que vous apprenez et commencez à chercher plus profondément, il y a toujours une autre couche de complexité et d’émerveillement.

Ethereum est une excellente plate-forme pour en savoir plus sur les chaînes de blocs et construit une communauté massive de développeurs, plus rapidement que toute autre plate-forme de chaîne de blocs. Plus que tout autre, Ethereum est une chaîne de blocs de développeurs, construite par des développeurs pour des développeurs. Un développeur familiarisé avec les applications JavaScript peut se lancer dans Ethereum et commencer à produire du code fonctionnel très rapidement. Pendant les premières années de la vie d’Ethereum, il était courant de voir des T-shirts annonçant que vous pouvez créer un jeton en seulement cinq lignes de code. Bien sûr, c’est une épée à double tranchant. Il est facile d’écrire du code, mais il est très difficile d’écrire du bon code sécurisé.

Ce que ce livre vous apprendra

Ce livre plonge dans Ethereum et examine chaque composant. Vous commencerez par une transaction simple, décortiquerez son fonctionnement, établirez un contrat simple, l’améliorerez et suivrez son parcours dans le système Ethereum.

Vous apprendrez non seulement comment utiliser Ethereum - comment cela fonctionne - mais aussi pourquoi il est conçu comme il est. Vous pourrez comprendre comment chacune des pièces fonctionne, comment elles s’emboîtent et pourquoi.

Les bases d’Ethereum

Bannière Amazon du livre Maîtriser Ethereum

Dans ce chapitre, nous commencerons à explorer Ethereum, à apprendre à utiliser les portefeuilles, à créer des transactions, et aussi comment exécuter un contrat intelligent de base.

L’ether comme unité monétaire

L’unité monétaire d’Ethereum est appelée ether, également identifiée par "ETH" ou avec les symboles Ξ (de la lettre grecque « Xi » qui ressemble à un E majuscule stylisé) ou, moins souvent, ♦ : par exemple, 1 ether, ou 1 ETH, ou Ξ1, ou ♦1.

Tip

Utilisez le caractère Unicode U+039E pour Ξ et U+2666 pour ♦.

L’ether est subdivisé en unités plus petites, jusqu’à la plus petite unité possible, qui est nommée wei. Un ether est égal à 1 quintillion de wei (1 * 1018 ou 1 000 000 000 000 000 000). Vous entendrez peut-être aussi les gens se référer à la devise "Ethereum", mais c’est une erreur courante pour les débutants. Ethereum est le système, ether est la monnaie.

La valeur de l’ether est toujours représentée en interne dans Ethereum sous la forme d’une valeur entière non signée libellée en wei. Lorsque vous traitez 1 ether, la transaction encode 1000000000000000000 wei comme valeur.

Les différentes dénominations d’ether ont à la fois un nom scientifique utilisant le Système international d’unités (SI) et un nom familier qui rend hommage à de nombreux grands esprits de l’informatique et de la cryptographie.

Dénominations d’ether et noms d’unités montre les différentes unités, leurs noms familiers (communs) et leurs noms SI. Conformément à la représentation interne de la valeur, le tableau montre toutes les dénominations en wei (première ligne), avec l’ether indiqué par 1018 wei dans la 7e ligne.

Table 1. Dénominations d’ether et noms d’unités
Valeur (en wei) Exposant Nom commun Nom SI

1

1

wei

Wei

1 000

103

Babbage

Kilowei ou femtoether

1 000 000

106

Lovelace

Mégawei ou picoether

1 000 000 000

109

Shanon

Gigawei ou nanoether

1 000 000 000 000

1012

Szabo

Microether ou micro

1 000 000 000 000 000

1015

Finney

Milliether ou milli

1 000 000 000 000 000 000

1018

Ether

Ether

1 000 000 000 000 000 000 000

1021

grandiose

Kiloether

1 000 000 000 000 000 000 000 000

1024

Mégaether

Choisir un portefeuille Ethereum

Le terme "portefeuille" est venu signifier beaucoup de choses, bien qu’elles soient toutes liées et qu’elles se résument au quotidien à peu près à la même chose. Nous utiliserons le terme "portefeuille" pour désigner une application logicielle qui vous aide à gérer votre compte Ethereum. En bref, un portefeuille Ethereum est votre passerelle vers le système Ethereum. Il détient vos clés et peut créer et diffuser des transactions en votre nom. Choisir un portefeuille Ethereum peut être difficile car il existe de nombreuses options différentes avec des caractéristiques et des conceptions différentes. Certains sont plus adaptés aux débutants et certains sont plus adaptés aux experts. La plate-forme Ethereum elle-même est toujours en cours d’amélioration, et les "meilleurs" portefeuilles sont souvent ceux qui s’adaptent aux changements qui accompagnent les mises à niveau de la plate-forme.

Mais ne vous inquiétez pas ! Si vous choisissez un portefeuille et que vous n’aimez pas son fonctionnement, ou si vous l’aimez au début, mais que vous souhaitez ensuite essayer autre chose, vous pouvez changer de portefeuille assez facilement. Tout ce que vous avez à faire est d’effectuer une transaction qui envoie vos fonds de l’ancien portefeuille vers le nouveau portefeuille, ou d’exporter vos clés privées et de les importer dans le nouveau.

Nous avons sélectionné trois types de portefeuilles différents à utiliser comme exemples tout au long du livre : un portefeuille mobile, un portefeuille de bureau et un portefeuille Web. Nous avons choisi ces trois portefeuilles car ils représentent un large éventail de complexité et de fonctionnalités. Cependant, la sélection de ces portefeuilles n’est pas une approbation de leur qualité ou de leur sécurité. Ils sont simplement un bon point de départ pour des démonstrations et des tests.

N’oubliez pas que pour qu’une application de portefeuille fonctionne, elle doit avoir accès à vos clés privées, il est donc essentiel que vous ne téléchargiez et n’utilisiez que des applications de portefeuille provenant de sources de confiance. Heureusement, en général, plus une application de portefeuille est populaire, plus elle est susceptible d’être fiable. Néanmoins, il est recommandé d’éviter de "mettre tous vos œufs dans le même panier" et de répartir vos comptes Ethereum sur plusieurs portefeuilles.

Voici quelques bons portefeuilles de démarrage :

MetaMask

MetaMask est un portefeuille d’extension de navigateur qui s’exécute dans votre navigateur (Chrome, Firefox, Opera ou Brave Browser). Il est facile à utiliser et pratique pour les tests, car il est capable de se connecter à une variété de nœuds Ethereum et de tester des chaînes de blocs. MetaMask est un portefeuille basé sur le Web.

Jaxx

Jaxx est un portefeuille multiplateforme et multidevise qui fonctionne sur une variété de systèmes d’exploitation, y compris Android, iOS, Windows, macOS, et Linux. C’est souvent un bon choix pour les nouveaux utilisateurs car il est conçu pour la simplicité et la facilité d’utilisation. Jaxx est soit un portefeuille mobile, soit un portefeuille de bureau, selon l’endroit où vous l’installez.

MyEtherWallet (MEW)

MyEtherWallet est un portefeuille Web qui s’exécute dans n’importe quel navigateur. Il possède de multiples fonctionnalités sophistiquées que nous explorerons dans plusieurs de nos exemples. MyEtherWallet est un portefeuille basé sur le Web.

Emerald Wallet

Emerald Wallet est conçu pour fonctionner avec la chaîne de blocs Ethereum Classic, mais est compatible avec d’autres chaînes de blocs basées sur Ethereum. C’est une application de bureau à source libre et fonctionne sous Windows, macOS et Linux. Emerald Wallet peut exécuter un nœud complet ou se connecter à un nœud distant public, fonctionnant en mode "léger". Il dispose également d’un outil compagnon pour effectuer toutes les opérations à partir de la ligne de commande.

Nous commencerons par installer MetaMask sur un bureau, mais nous aborderons d’abord brièvement le contrôle et la gestion des clés.

Contrôle et responsabilité

Les chaînes de blocs ouvertes comme Ethereum sont importantes car elles fonctionnent comme un système décentralisé. Cela signifie beaucoup de choses, mais un aspect crucial est que chaque utilisateur d’Ethereum peut et doit contrôler ses propres clés privées, qui contrôlent l’accès aux fonds et aux contrats intelligents. Nous appelons parfois la combinaison de l’accès aux fonds et des contrats intelligents un "compte" ou un "portefeuille". Ces termes peuvent devenir assez complexes dans leur fonctionnalité, nous y reviendrons donc plus en détail plus tard. En tant que principe fondamental, cependant, c’est aussi simple qu’une clé privée équivaut à un "compte". Certains utilisateurs choisissent de renoncer au contrôle de leurs clés privées en utilisant un dépositaire tiers, tel qu’un échange en ligne. Dans ce livre, nous vous apprendrons à prendre le contrôle et à gérer vos propres clés privées.

Avec le contrôle vient une grande responsabilité. Si vous perdez vos clés privées, vous perdez l’accès à vos fonds et contrats. Personne ne peut vous aider à retrouver l’accès - vos fonds seront verrouillés pour toujours. Voici quelques conseils pour vous aider à gérer cette responsabilité :

  • N’improvisez pas la sécurité. Utilisez des approches standardisées.

  • Plus le compte est important (par exemple, plus la valeur des fonds contrôlés est élevée, ou plus les contrats intelligents accessibles sont importants), plus les mesures de sécurité doivent être prises.

  • La sécurité la plus élevée est obtenue à partir d’un appareil isolé, mais ce niveau n’est pas requis pour tous les comptes.

  • Ne stockez jamais votre clé privée en clair, en particulier sous forme numérique. Heureusement, la plupart des interfaces utilisateur actuelles ne vous permettent même pas de voir la clé privée brute.

  • Les clés privées peuvent être stockées sous forme cryptée, en tant que fichier "keystore" numérique. Étant cryptés, ils ont besoin d’un mot de passe pour se déverrouiller. Lorsque vous êtes invité à choisir un mot de passe, rendez-le fort (c’est-à-dire long et aléatoire), sauvegardez-le et ne le partagez pas. Si vous n’avez pas de gestionnaire de mots de passe, écrivez-le et conservez-le dans un endroit sûr et secret. Pour accéder à votre compte, vous avez besoin à la fois du fichier keystore et du mot de passe.

  • Ne stockez aucun mot de passe dans des documents numériques, des photos numériques, des captures d’écran, des lecteurs en ligne, des PDF cryptés, etc. Encore une fois, n’improvisez pas la sécurité. Utilisez un gestionnaire de mots de passe ou un stylo et du papier.

  • Lorsque vous êtes invité à sauvegarder une clé sous forme de séquence de mots mnémoniques, utilisez un stylo et du papier pour effectuer une sauvegarde physique. Ne laissez pas cette tâche "pour plus tard" ; vous oublierez. Ces sauvegardes peuvent être utilisées pour reconstruire votre clé privée au cas où vous perdriez toutes les données enregistrées sur votre système, ou si vous oubliez ou perdez votre mot de passe. Cependant, ils peuvent également être utilisés par des attaquants pour obtenir vos clés privées. Ne les stockez donc jamais sous forme numérique et conservez la copie physique en toute sécurité dans un tiroir ou un coffre-fort verrouillé.

  • Avant de transférer des montants importants (en particulier vers de nouvelles adresses), effectuez d’abord une petite transaction test (par exemple, une valeur inférieure à 1 $) et attendez la confirmation de réception.

  • Lorsque vous créez un nouveau compte, commencez par n’envoyer qu’une petite transaction test à la nouvelle adresse. Une fois que vous avez reçu la transaction de test, essayez de renvoyer à partir de ce compte. Il existe de nombreuses raisons pour lesquelles la création d’un compte peut mal tourner, et si cela a mal tourné, il vaut mieux le découvrir avec une petite perte. Si les tests fonctionnent, tout va bien.

  • Les explorateurs de blocs publics sont un moyen simple de voir indépendamment si une transaction a été acceptée par le réseau. Cependant, cette commodité a un impact négatif sur votre vie privée, car vous révélez vos adresses pour bloquer les explorateurs, qui peuvent vous suivre.

  • N’envoyez pas d’argent à l’une des adresses indiquées dans ce livre. Les clés privées sont répertoriées dans le livre et quelqu’un prendra immédiatement cet argent.

Maintenant que nous avons couvert quelques bonnes pratiques de base pour la gestion des clés et la sécurité, passons au travail avec MetaMask !

Premiers pas avec MetaMask

Ouvrez le navigateur Google Chrome et accédez à https://chrome.google.com/webstore/category/extensions.

Recherchez "MetaMask" et cliquez sur le logo d’un renard. Vous devriez voir quelque chose comme le résultat affiché dans La page de détail de l’extension MetaMask Chrome.

Page de détails du métamasque
Figure 1. La page de détail de l’extension MetaMask Chrome

Il est important de vérifier que vous téléchargez la véritable extension MetaMask, car parfois les gens sont capables de passer furtivement des extensions malveillantes au-delà des filtres de Google. Le vrai:

  • Affiche l’ID nkbihfbeogaeaoehlefnkodbefgpgknn dans la barre d’adresse

  • Est offert par https://metamask.io

  • A plus de 1 500 avis

  • A plus de 1 000 000 d’utilisateurs

Une fois que vous avez confirmé que vous recherchez la bonne extension, cliquez sur "Ajouter à Chrome" pour l’installer.

Création d’un portefeuille

Une fois MetaMask installé, vous devriez voir une nouvelle icône (la tête d’un renard) dans la barre d’outils de votre navigateur. Cliquez dessus pour commencer. Il vous sera demandé d’accepter les termes et conditions puis de créer votre nouveau portefeuille Ethereum en saisissant un mot de passe (voir La page de mot de passe de l’extension MetaMask Chrome).

Page de mot de passe du métamasque
Figure 2. La page de mot de passe de l’extension MetaMask Chrome
Tip

Le mot de passe contrôle l’accès à MetaMask, de sorte qu’il ne peut être utilisé par personne ayant accès à votre navigateur.

Une fois que vous avez défini un mot de passe, MetaMask générera un portefeuille pour vous et vous montrera un mnémonique de sauvegarde composé de 12 mots anglais (voir La sauvegarde mnémonique de votre portefeuille, créée par MetaMask). Ces mots peuvent être utilisés dans n’importe quel portefeuille compatible pour récupérer l’accès à vos fonds si quelque chose arrivait à MetaMask ou à votre ordinateur. Vous n’avez pas besoin du mot de passe pour cette récupération ; les 12 mots suffisent.

Tip

Sauvegardez votre mnémonique (12 mots) sur papier, deux fois. Conservez les deux sauvegardes papier dans deux emplacements sécurisés distincts, tels qu’un coffre-fort résistant au feu, un tiroir verrouillé ou un coffre-fort. Traitez les sauvegardes papier comme de l’argent de valeur équivalente à ce que vous stockez dans votre portefeuille Ethereum. Toute personne ayant accès à ces mots peut y accéder et voler votre argent.

Page Mnémonique MetaMask
Figure 3. La sauvegarde mnémonique de votre portefeuille, créée par MetaMask

Une fois que vous avez confirmé que vous avez stocké le mnémonique en toute sécurité, vous pourrez voir les détails de votre compte Ethereum, comme indiqué dans Votre compte Ethereum dans MetaMask.

Page de compte MetaMask
Figure 4. Votre compte Ethereum dans MetaMask

La page de votre compte affiche le nom de votre compte ("Account 1" par défaut), une adresse Ethereum (0x9E713... dans l’exemple), et une icône colorée pour vous aider à distinguer visuellement ce compte des autres comptes. En haut de la page du compte, vous pouvez voir sur quel réseau Ethereum vous travaillez actuellement ("Main Network" dans l’exemple).

Toutes nos félicitations ! Vous avez configuré votre premier portefeuille Ethereum.

Changer le réseau

Comme vous pouvez le voir sur la page du compte MetaMask, vous pouvez choisir entre plusieurs réseaux Ethereum. Par défaut, MetaMask essaiera de se connecter au réseau principal. Les autres choix sont des réseaux de test publics, tout nœud Ethereum de votre choix ou des nœuds exécutant des chaînes de blocs privées sur votre propre ordinateur (localhost) :

Réseau principal Ethereum

La principale chaîne de blocs publique Ethereum. Véritable ETH, valeur réelle et conséquences réelles.

Réseau de test Ropsten

Chaîne de blocs et réseau de test public Ethereum. l’ETH sur ce réseau n’a aucune valeur.

Réseau de test Kovan

Chaîne de blocs et réseau de test public Ethereum utilisant le protocole de consensus Aura avec preuve d’autorité (signature fédérée). ETH sur ce réseau n’a aucune valeur. Le réseau de test Kovan est uniquement pris en charge par Parity. D’autres clients Ethereum utilisent le protocole de consensus Clique, qui a été proposé plus tard, pour la preuve de la vérification basée sur l’autorité.

Réseau de test Rinkeby

Chaîne de blocs et réseau de test public Ethereum, utilisant le protocole de consensus Clique avec preuve d’autorité (signature fédérée). ETH sur ce réseau n’a aucune valeur.

Localhost 8545

Se connecte à un nœud exécuté sur le même ordinateur que le navigateur. Le nœud peut faire partie de n’importe quelle chaîne de blocs publique (principale ou testnet) ou d’un testnet privé.

RPC personnalisé

vous permet de connecter MetaMask à n’importe quel nœud avec une interface d’appel de procédure distante (RPC) compatible Geth. Le nœud peut faire partie de n’importe quelle chaîne de blocs publique ou privée.

Note

Votre portefeuille MetaMask utilise la même clé privée et la même adresse Ethereum sur tous les réseaux auxquels il se connecte. Cependant, votre solde d’adresses Ethereum sur chaque réseau Ethereum sera différent. Vos clés peuvent contrôler l’ether et les contrats sur Ropsten, par exemple, mais pas sur le réseau principal.

Obtenir de l’ether de test

Votre première tâche est de financer votre portefeuille. Vous ne ferez pas cela sur le réseau principal car l’ether réel coûte de l’argent et sa manipulation nécessite un peu plus d’expérience. Pour l’instant, vous allez charger votre portefeuille avec de l’ether testnet.

Passez MetaMask au Réseau test Ropsten. Cliquez sur Deposit, puis sur Ropsten Test Faucet. MetaMask ouvrira une nouvelle page Web, comme indiqué dans MetaMask et le robinet du réseau test Ropsten.

MetaMask et le robinet du réseau test Ropsten
Figure 5. MetaMask et le robinet du réseau test Ropsten

Vous remarquerez peut-être que la page Web contient déjà l’adresse Ethereum de votre portefeuille MetaMask. MetaMask intègre les pages Web compatibles Ethereum avec votre portefeuille MetaMask et peut "voir" les adresses Ethereum sur la page Web, vous permettant, par exemple, d’envoyer un paiement à une boutique en ligne affichant une adresse Ethereum. MetaMask peut également remplir la page Web avec l’adresse de votre propre portefeuille en tant qu’adresse de destinataire si la page Web le demande. Sur cette page, l’application du robinet demande à MetaMask une adresse de portefeuille à laquelle envoyer de l’ether de test.

Cliquez sur le bouton vert "demander 1 ether au robinet". Vous verrez un ID de transaction apparaître dans la partie inférieure de la page. L’application robinet ou "faucet" a créé une transaction, un paiement qui vous est destiné. L’ID de transaction ressemble à ceci :

0x7c7ad5aaea6474adccf6f5c5d6abed11b70a350fbc6f9590109e099568090c57

Dans quelques secondes, la nouvelle transaction sera exploitée par les mineurs de Ropsten et votre portefeuille MetaMask affichera un solde de 1 ETH. Cliquez sur l’ID de transaction et votre navigateur vous amènera à un explorateur de bloc, qui est un site Web qui vous permet de visualiser et d’explorer des blocs, des adresses et des transactions. MetaMask utilise Explorateur de blocs Etherscan, l’un des explorateurs de blocs Ethereum les plus populaires. La transaction contenant le paiement du Ropsten Test Faucet est affichée dans Explorateur de blocs Ropsten sur Etherscan.

Explorateur de blocs Ropsten sur Etherscan
Figure 6. Explorateur de blocs Ropsten sur Etherscan

La transaction a été enregistrée sur la chaîne de blocs Ropsten et peut être consultée à tout moment par n’importe qui, simplement en recherchant l’ID de transaction, ou en visitant le lien.

Essayez de visiter ce lien ou de saisir le hachage de la transaction sur le site Web ropsten.etherscan.io pour le voir par vous-même.

Envoi d’ether depuis MetaMask

Une fois que vous avez reçu votre premier test d’ether du Ropsten Test Faucet, vous pouvez expérimenter l’envoi d’ether en essayant d’en renvoyer au robinet. Comme vous pouvez le voir sur la page Ropsten Test Faucet, il existe une option pour "donner" 1 ETH au robinet. Cette option est disponible pour qu’une fois que vous avez terminé les tests, vous puissiez retourner le reste de votre ether de test, afin que quelqu’un d’autre puisse l’utiliser ensuite. Même si l’ether de test n’a aucune valeur, certaines personnes l’accumulent, ce qui rend difficile pour tout le monde d’utiliser les réseaux de test. L’accumulation de l’ether de test est mal vue !

Heureusement, nous ne sommes pas des accumulateurs d’ether de test. Cliquez sur le bouton orange "1 ether" pour dire à MetaMask de créer une transaction payant le robinet 1 ether. MetaMask préparera une transaction et ouvrira une fenêtre avec la confirmation, comme indiqué dans Envoi de 1 ether au robinet.

Envoi d'1 ether au robinet
Figure 7. Envoi de 1 ether au robinet

Oups ! Vous avez probablement remarqué que vous ne pouvez pas terminer la transaction - MetaMask indique que vous avez un solde insuffisant. À première vue, cela peut sembler déroutant : vous avez 1 ETH, vous voulez envoyer 1 ETH, alors pourquoi MetaMask dit-il que vous avez des fonds insuffisants ?

La réponse est à cause du coût de gaz. Chaque transaction Ethereum nécessite le paiement d’une redevance, qui est perçue par les mineurs pour valider la transaction. Les frais d’Ethereum sont facturés dans une monnaie virtuelle appelée gaz. Vous payez le gaz avec de l’ether, dans le cadre de la transaction.

Note

Des frais sont également exigés sur les réseaux de test. Sans frais, un réseau de test se comporterait différemment du réseau principal, ce qui en ferait une plate-forme de test inadéquate. Les frais protègent également les réseaux de test des attaques DoS et des contrats mal construits (par exemple, des boucles infinies), tout comme ils protègent le réseau principal.

Lorsque vous avez envoyé la transaction, MetaMask a calculé le prix moyen du gaz des récentes transactions réussies à 3 gwei, ce qui signifie gigawei. Wei est la plus petite subdivision de la monnaie ether, comme nous l’avons vu dans L’ether comme unité monétaire. La limite de gaz est fixée au prix de l’envoi d’une transaction de base, soit 21 000 unités de gaz. Par conséquent, le montant maximum d’ETH que vous dépenserez est de 3 * 21 000 gwei = 63 000 gwei = 0,000063 ETH. (Sachez que les prix moyens du gaz peuvent fluctuer, car ils sont principalement déterminés par les mineurs. Nous verrons dans un chapitre ultérieur comment vous pouvez augmenter/diminuer votre limite de gaz pour vous assurer que votre transaction a la priorité si nécessaire.)

Tout cela pour dire : faire une transaction à 1 ETH coûte 1,000063 ETH. MetaMask arrondit de manière confuse au plancher d'1 ETH lors de l’affichage du total, mais le montant réel dont vous avez besoin est de 1,000063 ETH et vous n’avez que 1 ETH. Cliquez sur Reject pour annuler cette transaction.

Prenons un peu plus d’ether de test ! Cliquez à nouveau sur le bouton vert "request 1 ether from the faucet" et attendez quelques secondes. Ne vous inquiétez pas, le robinet devrait avoir beaucoup d’ether et vous en donnera plus si vous le demandez.

Une fois que vous avez un solde de 2 ETH, vous pouvez réessayer. Cette fois, lorsque vous cliquez sur le bouton de don orange "1 ether", vous disposez d’un solde suffisant pour finaliser la transaction. Cliquez sur Soumettre lorsque MetaMask apparaît dans la fenêtre de paiement. Après tout cela, vous devriez voir un solde de 0,999937 ETH car vous avez envoyé 1 ETH au robinet avec 0,000063 ETH en gaz.

Explorer l’historique des transactions d’une adresse

Vous êtes maintenant devenu un expert dans l’utilisation de MetaMask pour envoyer et recevoir de l’ether de test. Votre portefeuille a reçu au moins deux paiements et en a envoyé au moins un. Vous pouvez afficher toutes ces transactions à l’aide de l’explorateur de blocs ropsten.etherscan.io. Vous pouvez soit copier l’adresse de votre portefeuille et la coller dans la zone de recherche de l’explorateur de blocs, soit demander à MetaMask d’ouvrir la page pour vous. À côté de l’icône de votre compte dans MetaMask, vous verrez un bouton affichant trois points. Cliquez dessus pour afficher un menu d’options liées au compte (voir Menu contextuel du compte MetaMask).

Menu contextuel du compte MetaMask
Figure 8. Menu contextuel du compte MetaMask

Sélectionnez "View account on Etherscan" pour ouvrir une page Web dans l’explorateur de blocs affichant l’historique des transactions de votre compte, comme indiqué dans Historique des transactions d’une adresse sur Etherscan.

Historique des transactions d’adresses sur Etherscan
Figure 9. Historique des transactions d’une adresse sur Etherscan

Ici, vous pouvez voir l’historique complet des transactions de votre adresse Ethereum. Il montre toutes les transactions enregistrées sur la chaîne de blocs Ropsten où votre adresse est l’expéditeur ou le destinataire. Cliquez sur quelques-unes de ces transactions pour voir plus de détails.

Vous pouvez explorer l’historique des transactions de n’importe quelle adresse. Jetez un œil à l’historique des transactions de l’adresse Ropsten Test Faucet (indice : il s’agit de l’adresse « expéditeur » répertoriée dans le plus ancien paiement à votre adresse). Vous pouvez voir tout l’ether de test envoyé par le robinet à vous et à d’autres adresses. Chaque transaction que vous voyez peut vous mener à plus d’adresses et plus de transactions. D’ici peu, vous serez perdu dans le labyrinthe de données interconnectées. Les chaînes de blocs publiques contiennent une énorme richesse d’informations, qui peuvent toutes être explorées par programmation, comme nous le verrons dans de futurs exemples.

Présentation de l’ordinateur mondial

Vous avez maintenant créé un portefeuille et envoyé et reçu de l’ether. Jusqu’à présent, nous avons traité Ethereum comme une cryptomonnaie. Mais Ethereum est bien plus que cela. En fait, la fonction de cryptomonnaie est subordonnée à la fonction d’Ethereum en tant qu’ordinateur mondial décentralisé. L’ether est destiné à être utilisé pour payer l’exécution de contrats intelligents aussi, qui sont des programmes informatiques qui s’exécutent sur un ordinateur émulé appelé Ethereum Virtual Machine (EVM).

L’EVM est un singleton global, ce qui signifie qu’il fonctionne comme s’il s’agissait d’un ordinateur global à instance unique, fonctionnant partout. Chaque nœud du réseau Ethereum exécute une copie locale de l’EVM pour valider l’exécution du contrat, tandis que la chaîne de blocs Ethereum enregistre l’état changeant de cet ordinateur mondial lorsqu’il traite les transactions et les contrats intelligents. Nous en discuterons plus en détail dans La machine virtuelle Ethereum.

Comptes détenus en externe (EOA ou Externally Owned Accounts) et contrats

Le type de compte que vous avez créé dans le portefeuille MetaMask est appelé un compte détenu par une personne externe (EOA ou Externally Owned Accounts). Les comptes détenus en externe sont ceux qui ont une clé privée ; avoir la clé privée signifie contrôler l’accès aux fonds ou aux contrats. Maintenant, vous devinez probablement qu’il existe un autre type de compte. Cet autre type de compte est un compte de contrat. Un compte de contrat a un code de contrat intelligent, ce qu’un simple EOA ne peut pas avoir. De plus, un compte contractuel ne possède pas de clé privée. Au lieu de cela, il est détenu (et contrôlé) par la logique de son code de contrat intelligent : le programme logiciel enregistré sur la chaîne de blocs Ethereum lors de la création du compte de contrat et exécuté par l’EVM.

Les contrats ont des adresses, tout comme les EOA. Les contrats peuvent également envoyer et recevoir de l’ether, tout comme les EOA. Cependant, lorsqu’une destination de transaction est une adresse de contrat, cela provoque l’exécution de ce contrat dans l’EVM, en utilisant la transaction et les données de la transaction comme entrée. En plus d’ether, les transactions peuvent contenir des données indiquant quelle fonction spécifique du contrat exécuter et quels paramètres transmettre à cette fonction. De cette façon, les transactions peuvent appeler des fonctions dans les contrats.

Notez qu’étant donné qu’un compte de contrat n’a pas de clé privée, il ne peut pas initier une transaction. Seuls les EOA peuvent initier des transactions, mais les contrats peuvent réagir aux transactions en appelant d’autres contrats, en créant des chemins d’exécution complexes. Une utilisation typique de ceci est un EOA envoyant une transaction de demande à un portefeuille de contrats intelligents multisignatures pour envoyer des ETH à une autre adresse. Un modèle de programmation DApp typique consiste à ce que le contrat A appelle le contrat B afin de maintenir un état partagé entre les utilisateurs du contrat A.

Dans les prochaines sections, nous rédigerons notre premier contrat. Vous apprendrez ensuite comment créer, financer et utiliser ce contrat avec votre portefeuille MetaMask et tester l’ether sur le réseau de test Ropsten.

Un contrat simple : un robinet test d’ether

Ethereum possède de nombreux langages de haut niveau différents, qui peuvent tous être utilisés pour écrire un contrat et produire un code intermédiaire ou bytecode EVM. Vous pouvez lire sur bon nombre des plus importantes et des plus intéressantes dans Introduction aux langages de haut niveau Ethereum. Un langage de haut niveau est de loin le choix dominant pour la programmation de contrats intelligents : Solidity. Solidity a été créé par le Dr Gavin Wood, le co-auteur de ce livre, et est devenu le langage le plus largement utilisé dans Ethereum (et au-delà). Nous utiliserons Solidity pour rédiger notre premier contrat.

Pour notre premier exemple (Faucet.sol: Un contrat Solidity mettant en place un robinet), nous allons écrire un contrat qui contrôle un robinet. Vous avez déjà utilisé un robinet pour obtenir de l’ether de test sur le réseau de test de Ropsten. Un robinet est une chose relativement simple: il donne de l’ether à toute adresse qui le demande et peut être rempli périodiquement. Vous pouvez implémenter un robinet comme un portefeuille contrôlé par un humain ou un serveur Web.

Example 1. Faucet.sol: Un contrat Solidity mettant en place un robinet
// Identifiant de licence SPDX : CC-BY-SA-4.0

// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity 0.6.4;

// Notre premier contrat est un faucet !
contract Faucet {
    // Accepte tout montant entrant
    receive() external payable {}

    // Donnez de l'éther à quiconque demande
    function withdraw(uint withdraw_amount) public {
        // Limiter le montant du retrait
        require(withdraw_amount <= 100000000000000000);

        // Envoie le montant à l'adresse qui l'a demandé
        msg.sender.transfer(withdraw_amount);
    }
}
Note

Vous trouverez tous les exemples de code pour ce livre dans le sous-répertoire code de le référentiel GitHub du livre. Concrètement, notre contrat Faucet.sol est en:

code/Solidity/Faucet.sol

Il s’agit d’un contrat très simple, à peu près aussi simple que possible. Il s’agit également d’un contrat faussé, démontrant un certain nombre de mauvaises pratiques et de failles de sécurité. Nous apprendrons en examinant tous ses défauts dans les sections suivantes. Mais pour l’instant, regardons ce que fait ce contrat et comment il fonctionne, ligne par ligne. Vous remarquerez rapidement que de nombreux éléments de Solidity sont similaires aux langages de programmation existants, tels que JavaScript, Java ou C++.

La première ligne est un commentaire :

// Notre premier contrat est un robinet (faucet) !

Les commentaires sont destinés à être lus par des humains et ne sont pas inclus dans le code intermédiaire exécutable de l’EVM. Nous les plaçons généralement sur la ligne avant le code que nous essayons d’expliquer, ou parfois sur la même ligne. Les commentaires commencent par deux barres obliques : //. Tout, depuis la première barre oblique jusqu’à la fin de cette ligne, est traité comme une ligne vide et ignoré.

La ligne suivante est l’endroit où notre contrat réel commence :

contract Faucet {

Cette ligne déclare un objet contract, similaire à une déclaration class dans d’autres langages orientés objet. La définition du contrat inclut toutes les lignes entre les accolades ({}), qui définissent une portée, un peu comme la façon dont les accolades sont utilisées dans de nombreux autres langages de programmation.

Ensuite, nous déclarons la première fonction du contrat Faucet :

function withdraw(uint withdraw_amount) public {

La fonction est nommée withdraw, et elle prend un argument entier non signé (uint)nommé withdraw_amount. Elle est déclarée fonction publique, c’est-à-dire qu’elle peut être appelée par d’autres contrats. La définition de la fonction suit, entre accolades. La première partie de la fonction retirer fixe une limite aux retraits :

require(withdraw_amount <= 100000000000000000);

Il utilise la fonction intégrée Solidity require pour tester une condition préalable, que le withdraw_amount est inférieur ou égal à 100 000 000 000 000 000 wei, qui est l’unité de base de l’ether (voir Dénominations d’ether et noms d’unités) et équivalent à 0,1 ether. Si la fonction withdraw est appelée avec un withdraw_amount supérieur à ce montant, la fonction require provoquera ici l’arrêt et l’échec de l’exécution du contrat avec une exception. Notez que les instructions doivent se terminer par un point-virgule dans Solidity.

Cette partie du contrat est la logique principale de notre robinet. Il contrôle le flux de fonds hors du contrat en imposant une limite aux retraits. C’est un contrôle très simple mais qui peut vous donner un aperçu de la puissance d’une chaîne de blocs programmable : un logiciel décentralisé contrôlant l’argent.

Vient ensuite le retrait proprement dit :

msg.sender.transfer(withdraw_amount);

Quelques choses intéressantes se passent ici. L’objet msg est l’une des entrées auxquelles tous les contrats peuvent accéder. Il représente la transaction qui a déclenché l’exécution de ce contrat. L’attribut sender est l’adresse de l’expéditeur de la transaction. La fonction transfer est une fonction intégrée qui transfère l’ether du contrat actuel à l’adresse de l’expéditeur. En le lisant à l’envers, cela signifie transfert vers l'expéditeur du msg qui a déclenché l’exécution de ce contrat. La fonction transfer prend un montant comme seul argument. Nous passons la valeur withdraw_amount qui était le paramètre à la fonction withdraw déclarée quelques lignes plus tôt.

La ligne suivante est l’accolade fermante, indiquant la fin de la définition de notre fonction withdraw.

Ensuite, nous déclarons une autre fonction :

function () external payable {}

Cette fonction est une fonction dite de secours ou de fallback (ou default), qui est appelée si la transaction qui a déclenché le contrat n’a nommé aucune des fonctions déclarées dans le contrat, ou aucune fonction du tout , ou ne contenait pas de données. Les contrats peuvent avoir une telle fonction par défaut (sans nom) et c’est généralement celle qui reçoit l’ether. C’est pourquoi il est défini comme une fonction externe et payante, ce qui signifie qu’il peut accepter de l’ether dans le contrat. Il ne fait rien d’autre que d’accepter l’ether, comme indiqué par la définition vide entre les accolades ({}). Si nous effectuons une transaction qui envoie de l’ether à l’adresse du contrat, comme s’il s’agissait d’un portefeuille, cette fonction s’en chargera.

Juste en dessous de notre fonction par défaut se trouve l’accolade fermante finale, qui ferme la définition du contrat Faucet. C’est tout !

Compilation du contrat de robinet

Maintenant que nous avons notre premier exemple de contrat, nous devons utiliser un compilateur Solidity pour convertir le code Solidity en code intermédiaire/bytecode EVM afin qu’il puisse être exécuté par l’EVM sur la chaîne de blocs elle-même .

Le compilateur Solidity se présente sous la forme d’un exécutable autonome, dans le cadre de divers frameworks, et regroupé dans des environnements de développement intégrés (IDE). Pour garder les choses simples, nous utiliserons l’un des IDE les plus populaires, appelé Remix.

Utilisez votre navigateur Chrome (avec le portefeuille MetaMask que vous avez installé précédemment) pour accéder à l’IDE Remix à l’adresse https://remix.ethereum.org.

Lorsque vous chargez Remix pour la première fois, il démarre avec un exemple de contrat appelé ballot.sol. Nous n’en avons pas besoin, alors fermez-le en cliquant sur le x dans le coin de l’onglet, comme indiqué dans Fermer l’onglet d’exemple par défaut.

Fermer l’onglet d’exemple par défaut
Figure 10. Fermer l’onglet d’exemple par défaut

Maintenant, ajoutez un nouvel onglet en cliquant sur le signe plus circulaire dans la barre d’outils en haut à gauche, comme indiqué dans Cliquez sur le signe plus pour ouvrir un nouvel onglet. Nommez le nouveau fichier Faucet.sol.

Cliquez sur le signe plus pour ouvrir un nouvel onglet
Figure 11. Cliquez sur le signe plus pour ouvrir un nouvel onglet

Une fois que vous avez ouvert le nouvel onglet, copiez et collez le code de notre exemple Faucet.sol, comme indiqué dans Copiez l’exemple de code Faucet dans le nouvel onglet.

Copiez l’exemple de code Faucet dans le nouvel onglet
Figure 12. Copiez l’exemple de code Faucet dans le nouvel onglet

Une fois que vous avez chargé le contrat Faucet.sol dans l’IDE Remix, l’IDE compilera automatiquement le code. Si tout se passe bien, vous verrez une boîte verte avec "Faucet" dedans apparaître à droite, sous l’onglet Compiler, confirmant la compilation réussie (voir Remix compile avec succès le contrat Faucet.sol).

Figure 13. Remix compile avec succès le contrat Faucet.sol

Si quelque chose ne va pas, le problème le plus probable est que l’IDE Remix utilise une version du compilateur Solidity différente de 0.5.12. Dans ce cas, notre directive pragma empêchera Faucet.sol de se compiler. Pour modifier la version du compilateur, accédez à l’onglet Paramètres, définissez la version sur 0.5.12 et réessayez.

Le compilateur Solidity a maintenant compilé notre Faucet.sol en code intermédiaire EVM. Si vous êtes curieux, le code intermédiaire ressemble à ceci :

PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH2 0xF JUMPI PUSH1 0x0 DUP1
REVERT JUMPDEST PUSH1 0xE5 DUP1 PUSH2 0x1D PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN
STOP PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3F JUMPI
PUSH1 0x0 CALLDATALOAD PUSH29
0x100000000000000000000000000000000000000000000000000000000
SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x2E1A7D4D EQ PUSH1 0x41 JUMPI
JUMPDEST STOP JUMPDEST CALLVALUE ISZERO PUSH1 0x4B JUMPI PUSH1 0x0 DUP1 REVERT
JUMPDEST PUSH1 0x5F PUSH1 0x4 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1
SWAP2 SWAP1 POP POP PUSH1 0x61 JUMP JUMPDEST STOP JUMPDEST PUSH8
0x16345785D8A0000 DUP2 GT ISZERO ISZERO ISZERO PUSH1 0x77 JUMPI PUSH1 0x0 DUP1
REVERT JUMPDEST CALLER PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND
PUSH2 0x8FC DUP3 SWAP1 DUP2 ISZERO MUL SWAP1 PUSH1 0x40 MLOAD PUSH1 0x0 PUSH1
0x40 MLOAD DUP1 DUP4 SUB DUP2 DUP6 DUP9 DUP9 CALL SWAP4 POP POP POP POP ISZERO
ISZERO PUSH1 0xB6 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP JUMP STOP LOG1 PUSH6
0x627A7A723058 KECCAK256 PUSH9 0x13D1EA839A4438EF75 GASLIMIT CALLVALUE LOG4 0x5f
PUSH24 0x7541F409787592C988A079407FB28B4AD000290000000000

N’êtes-vous pas content d’utiliser un langage de haut niveau comme Solidity au lieu de programmer directement en code intermédiaire EVM ? Moi aussi !

Création du contrat sur la chaîne de blocs

Donc, nous avons un contrat. Nous l’avons compilé en code intermédiaire. Maintenant, nous devons "enregistrer" le contrat sur la chaîne de blocs Ethereum. Nous utiliserons le testnet de Ropsten pour tester notre contrat, c’est donc la chaîne de blocs à laquelle nous voulons le soumettre.

L’enregistrement d’un contrat sur la chaîne de blocs implique la création d’une transaction spéciale dont la destination est l’adresse 0x00000000000000000000000000000000000000, également appelée zero address ou l'adresse zéro. L’adresse zéro est une adresse spéciale qui indique à la chaîne de blocs Ethereum que vous souhaitez enregistrer un contrat. Heureusement, l’IDE Remix s’occupera de tout cela pour vous et enverra la transaction à MetaMask.

Tout d’abord, passez à l’onglet Run et sélectionnez Injected Web3 dans la zone de sélection déroulante Environment. Cela connecte l’IDE Remix au portefeuille MetaMask et, via MetaMask, au réseau de test Ropsten. Une fois que vous faites cela, vous pouvez voir Ropsten sous Environment. De plus, dans la zone de sélection du compte, il indique l’adresse de votre portefeuille (voir Onglet Run du IDE Remix, avec l’environnement Injected Web3 sélectionné).

Onglet Run du IDE Remix, avec l’environnement Injected Web3 sélectionné
Figure 14. Onglet Run du IDE Remix, avec l’environnement Injected Web3 sélectionné

Juste en dessous des paramètres d’exécution que vous venez de confirmer se trouve le contrat Faucet, prêt à être créé. Cliquez sur le bouton Deploy affiché dans Onglet Run du IDE Remix, avec l’environnement Injected Web3 sélectionné.

Remix construira la transaction spéciale "création" et MetaMask vous demandera de l’approuver, comme indiqué dans MetaMask montrant la transaction de création de contrat. Vous remarquerez que la transaction de création de contrat ne contient pas d’ether, mais qu’elle contient 262 octets de données (le contrat compilé) et consommera 10 gwei en gaz. Cliquez sur Soumettre pour l’approuver.

MetaMask montrant la transaction de création de contrat
Figure 15. MetaMask montrant la transaction de création de contrat

Maintenant, vous devez attendre. Il faudra environ 15 à 30 secondes pour que le contrat soit miné sur Ropsten. Remix ne semblera pas faire grand-chose, mais soyez patient.

Une fois le contrat créé, il apparaît en bas de l’onglet Run (voir Le contrat Faucet est VIVANT !).

Le contrat Faucet est VIVANT !
Figure 16. Le contrat Faucet est VIVANT !

Notez que le contrat Faucet a maintenant sa propre adresse : Remix l’affiche comme "Faucet à 0x72e…​c7829" (bien que votre adresse, les lettres et les chiffres aléatoires, soient différents). Le petit symbole de presse-papiers à droite vous permet de copier l’adresse du contrat dans votre presse-papiers. Nous l’utiliserons dans la section suivante.

Interagir avec le contrat

Récapitulons ce que nous avons appris jusqu’à présent : les contrats Ethereum sont des programmes qui contrôler l’argent, qui s’exécutent à l’intérieur d’une machine virtuelle appelée EVM. Ils sont créés par une transaction spéciale qui soumet leur code intermédiaire à enregistrer sur la chaîne de blocs. Une fois créés sur la chaîne de blocs, ils ont une adresse Ethereum, tout comme les portefeuilles. Chaque fois que quelqu’un envoie une transaction à une adresse de contrat, le contrat s’exécute dans l’EVM, avec la transaction en entrée. Transactions envoyées pour les adresses de contrat peuvent contenir de l’ether ou des données ou les deux. S’ils contiennent de l’ether, il est "déposé" sur le solde du contrat. S’ils contiennent des données, les données peuvent spécifier une fonction nommée dans le contrat et l’appeler, en transmettant des arguments à la fonction.

Affichage de l’adresse du contrat dans un explorateur de blocs

Nous avons maintenant un contrat enregistré sur la chaîne de blocs, et nous pouvons voir qu’il a une adresse Ethereum. Vérifions-le dans l’explorateur de blocs ropsten.etherscan.io et voyons à quoi ressemble un contrat. Dans l’IDE Remix, copiez l’adresse du contrat en cliquant sur l’icône du presse-papiers à côté de son nom (voir Copiez l’adresse du contrat de Remix).

Copier l’adresse du contrat depuis Remix
Figure 17. Copiez l’adresse du contrat de Remix

Gardez Remix ouvert ; nous y reviendrons plus tard. Maintenant, naviguez dans votre navigateur jusqu’à ropsten.etherscan.io et collez l’adresse dans le champ de recherche. Vous devriez voir l’historique des adresses Ethereum du contrat, comme indiqué dans Afficher l’adresse du contrat Faucet dans l’explorateur de blocs Etherscan.

Afficher l’adresse du contrat Faucet dans l’explorateur de blocs etherscan
Figure 18. Afficher l’adresse du contrat Faucet dans l’explorateur de blocs Etherscan

Financement du contrat

Pour l’instant, le contrat n’a qu’une seule transaction dans son historique : l’opération de création de contrat. Comme vous pouvez le voir, le contrat n’a pas non plus d’ether (solde nul). C’est parce que nous n’avons pas envoyé d’ether au contrat dans la transaction de création, même si nous aurions pu le faire.

Notre robinet a besoin de fonds ! Notre premier projet sera d’utiliser MetaMask pour envoyer de l’ether au contrat. Vous devriez toujours avoir l’adresse du contrat dans votre presse-papiers (sinon, copiez-la à nouveau depuis Remix). Ouvrez MetaMask et envoyez-lui 1 ether, exactement comme vous le feriez à n’importe quelle autre adresse Ethereum (voir Envoyer 1 ether à l’adresse du contrat).

Figure 19. Envoyer 1 ether à l’adresse du contrat

Dans une minute, si vous rechargez l’explorateur de blocs Etherscan, il affichera une autre transaction à l’adresse du contrat et un solde mis à jour de 1 ether.

Vous souvenez-vous de la fonction de paiement externe par défaut sans nom dans notre code Faucet.sol ? Ça ressemblait à ça :

function () external payable {}

Lorsque vous avez envoyé une transaction à l’adresse du contrat, sans données spécifiant la fonction à appeler, elle a appelé cette fonction par défaut. Parce que nous l’avons déclaré comme payable, il a accepté et déposé le 1 ether dans le solde du compte du contrat. Votre transaction a entraîné l’exécution du contrat dans l’EVM, mettant à jour son solde. Vous avez financé votre robinet !

Faire un retrait sur notre contrat

Ensuite, retirons des fonds du robinet. Pour retirer, nous devons construire une transaction qui appelle la fonction withdraw et lui passe un argument withdraw_amount. Pour garder les choses simples pour le moment, Remix construira cette transaction pour nous et MetaMask la présentera pour notre approbation.

Revenez à l’onglet Remix et consultez le contrat dans l’onglet Run. Vous devriez voir une boîte orange intitulée wthdraw (retrait) avec une entrée de champ intitulée uint256 withdraw_amount (voir La fonction de retrait (withdraw) de Faucet.sol, dans Remix).

La fonction de retrait (withdraw) de Faucet.sol, dans Remix
Figure 20. La fonction de retrait (withdraw) de Faucet.sol, dans Remix

Il s’agit de l’interface Remix du contrat. Il nous permet de construire des transactions qui appellent les fonctions définies dans le contrat. Nous entrerons un withdraw_amount et cliquerons sur le bouton de retrait pour générer la transaction.

Tout d’abord, déterminons le withdraw_amount (montant de retrait). Nous voulons essayer de retirer 0,1 ether, qui est le montant maximum autorisé par notre contrat. N’oubliez pas que toutes les valeurs monétaires d’Ethereum sont libellées en wei en interne, et notre fonction withdraw s’attend à ce que le withdraw_amount soit également libellé en wei. La quantité que nous voulons est de 0,1 ether, soit 100 000 000 000 000 000 wei (un 1 suivi de 17 zéros).

Tip

En raison d’une limitation de JavaScript, un nombre aussi grand que 1017 ne peut pas être traité par Remix. Au lieu de cela, nous l’entourons de guillemets doubles, pour permettre à Remix de le recevoir sous forme de chaîne et de le manipuler comme un BigNumber. Si nous ne le mettons pas entre guillemets, l’IDE Remix ne parviendra pas à le traiter et affichera "Error encoding arguments: Error: Assertion failed."

Tapez "100000000000000000" (avec les guillemets) dans la case withdraw_amount et cliquez sur le bouton de retrait (voir Cliquez sur "withdraw" dans Remix pour créer une transaction de retrait).

Figure 21. Cliquez sur "withdraw" dans Remix pour créer une transaction de retrait

MetaMask fera apparaître une fenêtre de transaction que vous devrez approuver. Cliquez sur Confirmer pour envoyer votre appel de retrait au contrat (voir Transaction MetaMask pour appeler la fonction de retrait).

Transaction MetaMask pour appeler la fonction de retrait
Figure 22. Transaction MetaMask pour appeler la fonction de retrait

Attendez une minute, puis rechargez l’explorateur de blocs Etherscan pour voir la transaction reflétée dans l’historique des adresses de contrat + Faucet + (voir Etherscan montre la transaction appelant la fonction de retrait).

Etherscan affiche la transaction appelant la fonction de retrait
Figure 23. Etherscan montre la transaction appelant la fonction de retrait

Nous voyons maintenant une nouvelle transaction avec l’adresse du contrat comme destination et une valeur de 0 ether. Le solde du contrat a changé et est maintenant de 0,9 ether car il nous a envoyé 0,1 ether comme demandé. Mais nous ne voyons pas de transaction "OUT" dans l'historique des adresses de contrat.

Où est le retrait sortant ? Un nouvel onglet est apparu sur la page d’historique des adresses du contrat, nommé Transactions internes. Parce que le transfert d’ether 0.1 provient du code de contrat, il s’agit d’une transaction interne (également appelée message). Cliquez sur cet onglet pour le voir (voir Etherscan montre la transaction interne transférant l’ether du contrat).

Cette "transaction interne" a été envoyée par le contrat dans cette ligne de code (à partir de la fonction withdraw dans Faucet.sol) :

msg.sender.transfer(withdraw_amount);

Pour récapituler : vous avez envoyé une transaction depuis votre portefeuille MetaMask contenant des instructions de données pour appeler la fonction withdraw avec un argument withdraw_amount de 0,1 ether. Cette transaction a entraîné l’exécution du contrat à l’intérieur de l’EVM. Lorsque l’EVM a exécuté la fonction withdraw du contrat Faucet, il a d’abord appelé la fonction require et validé que le montant demandé était inférieur ou égal au retrait maximal autorisé de 0,1 ether. Ensuite, il a appelé la fonction transfer pour vous envoyer l’ether. L’exécution de la fonction transfer a généré une transaction interne qui a déposé 0,1 ether dans votre adresse de portefeuille, à partir du solde du contrat. C’est celui affiché sur l’onglet Internal Transactions dans Etherscan.

Etherscan montre la transaction interne transférant l’ether hors du contrat
Figure 24. Etherscan montre la transaction interne transférant l’ether du contrat

Conclusion

Dans ce chapitre, vous avez configuré un portefeuille à l’aide de MetaMask et l’avez financé à l’aide d’un robinet sur le réseau de test Ropsten. Vous avez reçu de l’ether dans l’adresse Ethereum de votre portefeuille, puis vous avez envoyé de l’ether à l’adresse du robinet Ethereum.

Ensuite, vous avez écrit un contrat de robinet dans Solidity. Vous avez utilisé l’IDE Remix pour compiler le contrat en code intermédiaire EVM, puis utilisé Remix pour former une transaction et créé le contrat Faucet sur la chaîne de blocs Ropsten. Une fois créé, le contrat Faucet avait une adresse Ethereum, et vous lui avez envoyé de l’ether. Enfin, vous avez construit une transaction pour appeler la fonction withdraw et demandé avec succès 0,1 ether. Le contrat a vérifié la demande et vous a envoyé 0,1 ether avec une transaction interne.

Cela peut ne pas sembler beaucoup, mais vous venez d’interagir avec succès avec un logiciel qui contrôle l’argent sur un ordinateur mondial décentralisé.

Nous ferons beaucoup plus de programmation de contrats intelligents dans Contrats intelligents et Solidity et découvrirons les meilleures pratiques et les considérations de sécurité dans Sécurité des contrats intelligents.

Clients Ethereum

Bannière Amazon du livre Maîtriser Ethereum

Un client Ethereum est une application logicielle qui implémente la spécification Ethereum et communique sur le réseau pair à pair avec d’autres clients Ethereum. Différents clients Ethereum interopèrent s’ils respectent la spécification de référence et les protocoles de communication standardisés. Bien que ces différents clients soient implémentés par des équipes différentes et dans des langages de programmation différents, ils « parlent » tous le même protocole et suivent les mêmes règles. En tant que tels, ils peuvent tous être utilisés pour fonctionner et interagir avec le même réseau Ethereum.

Ethereum est un projet à source libre, et le code source de tous les principaux clients est disponible sous des licences à source libre (par exemple, LGPL v3.0), téléchargeable et utilisable à n’importe quelle fin. Source libre signifie cependant plus que simplement libre d’utilisation. Cela signifie également qu’Ethereum est développé par une communauté ouverte de volontaires et peut être modifié par n’importe qui. Plus d’yeux signifie un code plus fiable.

Ethereum est défini par une spécification formelle appelée le "Papier jaune" (voir Lectures complémentaires).

Cela contraste avec, par exemple, Bitcoin, qui n’est pas défini de manière formelle. Là où la "spécification" de Bitcoin est l’implémentation de référence de Bitcoin Core, la spécification d’Ethereum est documentée dans un article qui combine une spécification anglaise et une spécification mathématique (formelle). Cette spécification formelle, en plus de diverses propositions d’amélioration Ethereum, définit le comportement standard d’un client Ethereum. Le livre jaune est périodiquement mis à jour au fur et à mesure que des modifications majeures sont apportées à Ethereum.

En raison de la spécification formelle claire d’Ethereum, il existe un certain nombre d’implémentations logicielles développées indépendamment, mais interopérables, d’un client Ethereum. Ethereum a une plus grande diversité d’implémentations fonctionnant sur le réseau que toute autre chaîne de blocs, ce qui est généralement considéré comme une bonne chose. En effet, il s’est avéré, par exemple, être un excellent moyen de se défendre contre les attaques sur le réseau, car l’exploitation de la stratégie de mise en œuvre d’un client particulier ne fait qu’embêter les développeurs pendant qu’ils corrigent l’exploit, tandis que d’autres clients maintiennent le réseau en marche presque sans être affecté.

Réseaux Ethereum

Il existe une variété de réseaux basés sur Ethereum qui sont largement conformes à la spécification formelle définie dans le livre jaune Ethereum, mais qui peuvent ou non interagir les uns avec les autres.

Parmi ces réseaux basés sur Ethereum figurent Ethereum, Ethereum Classic, Ella, Expanse, Ubiq, Musicoin et bien d’autres. Bien qu’ils soient généralement compatibles au niveau du protocole, ces réseaux ont souvent des fonctionnalités ou des attributs qui obligent les mainteneurs du logiciel client Ethereum à apporter de petites modifications afin de prendre en charge chaque réseau. Pour cette raison, toutes les versions du logiciel client Ethereum n’exécutent pas toutes les chaînes de blocs basées sur Ethereum.

Actuellement, il existe six implémentations principales du protocole Ethereum, écrites dans six langages différents :

  • Parity, écrite en Rust

  • Geth, écrit en Go

  • cpp-ethereum, écrit en C++

  • pyethereum, écrit en Python

  • Mantis, écrit en Scala

  • Harmony, écrit en Java

Dans cette section, nous examinerons les deux clients les plus courants, Parity et Geth. Nous montrerons comment configurer un nœud à l’aide de chaque client et explorerons certaines de leurs options de ligne de commande et interfaces de programmation d’application (API).

Dois-je exécuter un nœud complet ?

La santé, la résilience et la résistance à la censure des chaînes de blocs dépendent de leur nombre de nœuds complets exploités indépendamment et dispersés géographiquement. Chaque nœud complet peut aider d’autres nouveaux nœuds à obtenir les données de bloc pour amorcer leur fonctionnement, tout en offrant à l’opérateur une vérification faisant autorité et indépendante de toutes les transactions et contrats.

Cependant, l’exécution d’un nœud complet entraînera un coût en ressources matérielles et en bande passante. Un nœud complet doit télécharger 80 à 300 Go de données (à compter de janvier 2020, selon la configuration du client) et les stocker sur un disque dur local. Cette charge de données augmente assez rapidement chaque jour à mesure que de nouvelles transactions et de nouveaux blocs sont ajoutés. Nous abordons ce sujet plus en détail dans Configuration matérielle requise pour un nœud complet.

Un nœud complet fonctionnant sur un réseau mainnet en direct n’est pas nécessaire pour le développement d’Ethereum. Vous pouvez faire presque tout ce que vous devez faire avec un nœud testnet (qui vous connecte à l’une des plus petites chaînes de blocs de test publiques), avec une chaîne de blocs privée locale comme Ganache, ou avec un client Ethereum basé sur le cloud proposé par un fournisseur de services comme Infura.

Vous avez également la possibilité d’exécuter un client distant, qui ne stocke pas de copie locale de la chaîne de blocs ni ne valide les blocs et les transactions. Ces clients offrent la fonctionnalité d’un portefeuille et peuvent créer et diffuser des transactions. Les clients distants peuvent être utilisés pour se connecter à des réseaux existants, tels que votre propre nœud complet, une chaîne de blocs publique, un testnet public ou autorisé (preuve d’autorité), ou une chaîne de blocs locale privée. En pratique, vous utiliserez probablement un client distant tel que MetaMask, Emerald Wallet, MyEtherWallet ou MyCrypto comme moyen pratique de basculer entre toutes les différentes options de nœud.

Les termes "client distant" et "portefeuille" sont utilisés de manière interchangeable, bien qu’il existe certains différences. Habituellement, un client distant propose une API (telle que l’API web3.js) en plus de la fonctionnalité de transaction d’un portefeuille.

Ne confondez pas le concept de portefeuille distant dans Ethereum avec celui d’un client léger (qui est analogue à un client de vérification simplifiée des paiements dans Bitcoin). Les clients légers valident les en-têtes de bloc et utilisent les preuves Merkle pour valider l’inclusion des transactions dans la chaîne de blocs et déterminer leurs effets, leur donnant un niveau de sécurité similaire à un nœud complet. Inversement, les clients distants Ethereum ne valident pas les en-têtes de bloc ou les transactions. Ils font entièrement confiance à un client complet pour leur donner accès à la chaîne de blocs, et perdent ainsi d’importantes garanties de sécurité et d’anonymat. Vous pouvez atténuer ces problèmes en utilisant un client complet que vous exécutez vous-même.

Avantages et désavantages du nœud complet

Choisir d’exécuter un nœud complet facilite le fonctionnement des réseaux auxquels vous le connectez, mais entraîne également des coûts légers à modérés pour vous. Regardons quelques-uns des avantages et des inconvénients.

Avantages:

  • Prend en charge la résilience et la résistance à la censure des réseaux basés sur Ethereum

  • Valide avec autorité toutes les transactions

  • Peut interagir avec n’importe quel contrat sur la chaîne de blocs publique sans intermédiaire

  • Peut déployer directement des contrats dans la chaîne de blocs publique sans intermédiaire

  • Peut interroger (lecture seule) le statut de la chaîne de blocs (comptes, contrats, etc.) hors ligne

  • Peut interroger la chaîne de blocs sans laisser un tiers connaître les informations que vous lisez

Désavantages:

  • Nécessite des ressources matérielles et de bande passante importantes et croissantes

  • Peut nécessiter plusieurs jours pour une synchronisation complète lors du premier démarrage

  • Doit être maintenu, mis à jour et maintenu en ligne pour rester synchronisé

Avantages et désavantages du testnet public

Que vous choisissiez ou non d’exécuter un nœud complet, vous souhaiterez probablement exécuter un nœud testnet public. Examinons quelques-uns des avantages et des inconvénients de l’utilisation d’un testnet public.

Avantages:

  • Un nœud testnet doit synchroniser et stocker beaucoup moins de données - environ 45 Go selon le réseau.

  • Un nœud testnet peut se synchroniser complètement en quelques heures.

  • Le déploiement de contrats ou la réalisation de transactions nécessite un test d’ether, qui n’a aucune valeur et peut être acquis gratuitement à partir de plusieurs "robinets".

  • Les réseaux de test sont des chaînes de blocs publiques avec de nombreux autres utilisateurs et contrats, fonctionnant "en direct".

Désavantages:

  • Vous ne pouvez pas utiliser d’argent "réel" sur un testnet ; il fonctionne sur l’ether de test. Par conséquent, vous ne pouvez pas tester la sécurité contre de vrais adversaires, car il n’y a rien en jeu.

  • Il y a certains aspects d’une chaîne de blocs publique que vous ne pouvez pas tester de manière réaliste sur un testnet. Par exemple, les frais de transaction, bien que nécessaires pour envoyer des transactions, ne sont pas pris en compte sur un testnet, car le gaz est gratuit. De plus, les réseaux de test ne connaissent pas de congestion du réseau comme le fait parfois le réseau principal public.

Avantages et désavantages de la simulation chaîne de blocs locale

À de nombreuses fins de test, la meilleure option consiste à lancer une chaîne de blocs privée à instance unique. Ganache (anciennement nommé testrpc)est l’une des simulations de chaîne de blocs locales les plus populaires avec lesquelles vous pouvez interagir, sans aucun autre participant. Il partage de nombreux avantages et inconvénients du testnet public, mais présente également quelques différences.

Avantages:

  • Aucune synchronisation et presque aucune donnée sur le disque ; vous minez vous-même le premier bloc

  • Pas besoin d’obtenir de l’ether de test ; vous vous "attribuez" des récompenses minières que vous pouvez utiliser pour tester

  • Aucun autre utilisateur, juste vous

  • Aucun autre contrat, juste ceux que vous déployez après le lancement

Désavantages:

  • Ne pas avoir d’autres utilisateurs signifie qu’il ne se comporte pas de la même manière qu’une chaîne de blocs publique. Il n’y a pas de concurrence pour l’espace de transaction ou le séquençage des transactions.

  • Aucun mineur autre que vous signifie que l’exploitation minière est plus prévisible ; par conséquent, vous ne pouvez pas tester certains scénarios qui se produisent sur une chaîne de blocs publique.

  • L’absence d’autres contrats signifie que vous devez déployer tout ce que vous souhaitez tester, y compris les dépendances et les bibliothèques de contrats.

  • Vous ne pouvez pas recréer certains contrats publics et leurs adresses pour tester certains scénarios (par exemple, le contrat DAO).

Exécuter un client Ethereum

Si vous avez le temps et les ressources, vous devriez essayer d’exécuter un nœud complet, même si ce n’est que pour en savoir plus sur le processus. Dans cette section, nous expliquons comment télécharger, compiler et exécuter les clients Ethereum Parity et Geth. Cela nécessite une certaine familiarité avec l’utilisation de l’interface de ligne de commande sur votre système d’exploitation. Cela vaut la peine d’installer ces clients, que vous choisissiez de les exécuter en tant que nœuds complets, en tant que nœuds testnet ou en tant que clients d’une chaîne de blocs privée locale.

Configuration matérielle requise pour un nœud complet

Avant de commencer, vous devez vous assurer que vous disposez d’un ordinateur doté de ressources suffisantes pour fonctionner un nœud complet Ethereum. Vous aurez besoin d’au moins 300 Go d’espace disque pour stocker une copie complète de la chaîne de blocs Ethereum. Si vous souhaitez également exécuter un nœud complet sur le testnet Ethereum, vous aurez besoin d’au moins 45 Go supplémentaires. Le téléchargement de 345 Go de données de la chaîne de blocs peut prendre beaucoup de temps, il est donc recommandé de travailler sur une connexion Internet rapide.

La synchronisation de la chaîne de blocs Ethereum est très intensive en entrée/sortie (E/S). Il est préférable d’avoir un disque statique électronique (SSD). Si vous avez un disque dur mécanique (HDD), vous aurez besoin d’au moins 8 Go de RAM à utiliser comme cache. Sinon, vous découvrirez peut-être que votre système est trop lent pour suivre et synchroniser complètement.

Exigences minimales:

  • Processeur avec 2+ cœurs

  • Au moins 300 Go d’espace de stockage libre

  • 4 Go de RAM minimum avec un SSD, 8 Go+ si vous avez un HDD

  • Service Internet de téléchargement de 8 Mbit/s

Ce sont les exigences minimales pour synchroniser une copie complète (mais élaguée) d’une chaîne de blocs basée sur Ethereum.

Au moment de la rédaction, la base de code Parity est plus légère sur les ressources, donc si vous utilisez un matériel limité, vous obtiendrez probablement de meilleurs résultats en utilisant Parity.

Si vous souhaitez synchroniser dans un délai raisonnable et stocker tous les outils de développement, bibliothèques, clients et chaînes de blocs dont nous parlons dans ce livre, vous voudrez un ordinateur plus performant.

Spécifications recommandées :

  • Processeur rapide avec 4+ cœurs

  • 16 Go + RAM

  • SSD rapide avec au moins 500 Go d’espace libre

  • Service Internet de téléchargement de plus de 25 Mbit/s

Il est difficile de prédire à quelle vitesse la taille d’une chaîne de blocs augmentera et quand plus d’espace disque sera nécessaire, il est donc recommandé de vérifier la dernière taille de la chaîne de blocs avant de commencer la synchronisation.

Note

Les exigences de taille de disque répertoriées ici supposent que vous exécuterez un nœud avec les paramètres par défaut, où la chaîne de blocs est "élaguée" des anciennes données d’état. Si vous exécutez à la place un nœud "d’archivage" complet, où tout l’état est conservé sur le disque, il nécessitera probablement plus de 1 To d’espace disque.

Ces liens fournissent des estimations à jour de la taille de la chaîne de blocs :

Configuration logicielle requise pour créer et exécuter un client (nœud)

Cette section couvre les logiciels clients Parity et Geth. Il suppose également que vous utilisez un environnement de ligne de commande de type Unix. Les exemples montrent les commandes et la sortie telles qu’elles apparaissent sur un système d’exploitation Ubuntu GNU/Linux exécutant le shell bash (environnement d’exécution en ligne de commande).

En règle générale, chaque chaîne de blocs aura sa propre version de Geth, tandis que Parity prend en charge plusieurs chaînes de blocs basées sur Ethereum (Ethereum, Ethereum Classic, Ellaism, Expanse, Musicoin) avec le même client téléchargé.

Tip

Dans de nombreux exemples de ce chapitre , nous utiliserons l’interface de ligne de commande du système d’exploitation (également appelée "shell"), accessible via une application "terminal". Le shell affichera une invite ; vous tapez une commande et le shell répond avec du texte et une nouvelle invite pour votre prochaine commande. L’invite peut sembler différente sur votre système, mais dans les exemples suivants, elle est indiquée par un symbole $. Dans les exemples, lorsque vous voyez du texte après un symbole $, ne tapez pas le symbole $ mais tapez la commande qui le suit immédiatement (en gras), puis appuyez sur Entrée pour exécuter la commande. Dans les exemples, les lignes sous chaque commande sont les réponses du système d’exploitation à cette commande. Lorsque vous verrez le prochain préfixe $, vous saurez qu’il s’agit d’une nouvelle commande et vous devrez répéter le processus.

Avant de commencer, vous devrez peut-être installer certains logiciels. Si vous n’avez jamais fait de développement logiciel sur l’ordinateur que vous utilisez actuellement, vous devrez probablement installer quelques outils de base. Pour les exemples qui suivent, vous devrez installer git, le système de gestion du code source ; golang, le langage de programmation Go et les bibliothèques standard ; et Rust, un langage de programmation système.

Git peut être installé en suivant les instructions sur https://git-scm.com.

Go peut être installé en suivant les instructions sur https://golang.org, ou https://github.com/golang/ go/wiki/Ubuntu[] si vous utilisez Ubuntu.

Note

Les exigences de Geth varient, mais si vous vous en tenez à Go version 1.10 ou supérieure, vous devriez pouvoir compiler n’importe quelle version de Geth que vous souhaitez. Bien sûr, vous devriez toujours vous référer à la documentation de la version de Geth que vous avez choisie.

La version de golang installée sur votre système d’exploitation ou disponible à partir du gestionnaire de packages de votre système peut être bien antérieure à la 1.10. Si tel est le cas, supprimez-le et installez la dernière version à partir de https://golang.org/.

Rust peut être installé en suivant les instructions sur https://www.rustup.rs/.

Note

Parity nécessite Rust version 1.27 ou supérieure.

Parity nécessite également certaines bibliothèques logicielles, telles que OpenSSL et libudev. Pour les installer sur un système compatible Ubuntu ou Debian GNU/Linux, utilisez la commande suivante :

$ sudo apt-get install openssl libssl-dev libudev-dev cmake clang

Pour les autres systèmes d’exploitation, utilisez le gestionnaire d’applications de votre système d’exploitation ou suivez les https://github.com/paritytech/parity/wiki/Setup [instructions Wiki] pour installer les bibliothèques requises.

Maintenant que vous avez installé git, golang, Rust et les bibliothèques nécessaires, mettons-nous au travail !

Parity

Parity est une implémentation d’un client Ethereum à nœud complet et d’un navigateur DApp. Il a été écrit « à partir de zéro » en Rust, un langage de programmation système, dans le but de créer un client Ethereum modulaire, sécurisé et évolutif. Parity est développé par Parity Tech, une société britannique, et est publié sous la licence de logiciel libre GPLv3.

Note

Divulgation: L’un des auteurs de ce livre, le Dr Gavin Wood, est le fondateur de Parity Tech et a écrit une grande partie du client Parity. Parity représente environ 25% de la base des clients Ethereum installés.

Pour installer Parity, vous pouvez utiliser le gestionnaire d’applications Rust cargo ou télécharger le code source depuis GitHub. Le gestionnaire d’applications télécharge également le code source, il n’y a donc pas beaucoup de différence entre les deux options. Dans la section suivante, nous vous montrerons comment télécharger et compiler Parity vous-même.

Installation de Parity

Le Parity Wiki propose des instructions pour créer Parity dans différents environnements et conteneurs. Nous allons vous montrer comment créer Parity à partir de la source. Cela suppose que vous avez déjà installé Rust en utilisant rustup (voir Configuration logicielle requise pour créer et exécuter un client (nœud)).

Tout d’abord, récupérez le code source sur GitHub :

$ git clone https://github.com/paritytech/parity

Passez ensuite au répertoire parity et utilisez cargo pour créer l’exécutable :

$ cd parity
$ cargo install --path .

Si tout se passe bien, vous devriez voir quelque chose comme :

$ cargo install --path .
Installing parity-ethereum v2.7.0 (/root/parity)
Updating crates.io index
Updating git repository `https://github.com/paritytech/rust-ctrlc.git`
Updating git repository `https://github.com/paritytech/app-dirs-rs`   Updating git repository

[...]

Compiling parity-ethereum v2.7.0 (/root/parity)
Finished release [optimized] target(s) in 10m 16s
Installing /root/.cargo/bin/parity
Installed package `parity-ethereum v2.7.0 (/root/parity)` (executable `parity`)
$

Essayez d’exécuter parity pour voir s’il est installé, en invoquant l’option --version :

$ parity --version
Parity Ethereum Client.
  version Parity-Ethereum/v2.7.0-unstable-b69a33b3a-20200124/x86_64-unknown-linux-gnu/rustc1.40.0
Copyright 2015-2020 Parity Technologies (UK) Ltd.
License GPLv3+ : GNU GPL version 3 or later .
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

By Wood/Paronyan/Kotewicz/Drwięga/Volf/Greeff
   Habermeier/Czaban/Gotchac/Redman/Nikolsky
   Schoedon/Tang/Adolfsson/Silva/Palm/Hirsz et al.
$

Génial ! Maintenant que Parity est installé, vous pouvez synchroniser la chaîne de blocs et commencer avec quelques options de ligne de commande de base.

Go-Ethereum (Geth)

Geth est l’implémentation avec le langage Go activement développée par la Fondation Ethereum, elle est donc considérée comme l’implémentation "officielle" du client Ethereum. En règle générale, chaque chaîne de blocs basée sur Ethereum aura sa propre implémentation Geth. Si vous utilisez Geth, assurez-vous de récupérer la bonne version pour votre chaîne de blocs en utilisant l’un des liens de référentiel suivants :

Note

Vous pouvez également ignorer ces instructions et installer un binaire précompilé pour la plate-forme de votre choix. Les versions précompilées sont beaucoup plus faciles à installer et peuvent être trouvées dans la section "versions" de l’un des référentiels répertoriés ici. Cependant, vous pouvez en apprendre davantage en téléchargeant et en compilant le logiciel vous-même.

Cloner le référentiel

La première étape consiste à cloner le référentiel Git, pour obtenir une copie du code source.

Pour créer un clone local de votre référentiel choisi, utilisez la commande git comme suit, dans votre répertoire personnel ou sous n’importe quel répertoire que vous utilisez pour le développement :

$ git clone <Lien vers le référentiel>

Vous devriez voir un rapport de progression au fur et à mesure que le référentiel est copié sur votre système local :

Cloning into 'go-ethereum'...
remote: Enumerating objects: 86915, done.
remote: Total 86915 (delta 0), reused 0 (delta 0), pack-reused 86915
Receiving objects: 100% (86915/86915), 134.73 MiB | 29.30 MiB/s, done.
Resolving deltas: 100% (57590/57590), done.

Génial ! Maintenant que vous avez une copie locale de Geth, vous pouvez compiler un exécutable pour votre plate-forme.

Construire Geth à partir du code source

Pour compiler Geth, allez dans le répertoire où le code source a été téléchargé et utilisez la commande make :

$ cd go-ethereum
$ make geth

Si tout se passe bien, vous verrez le compilateur Go construire chaque composant jusqu’à ce qu’il produise l’exécutable geth :

build/env.sh go run build/ci.go install ./cmd/geth
>>> /usr/local/go/bin/go install -ldflags -X main.gitCommit=58a1e13e6dd7f52a1d...
github.com/ethereum/go-ethereum/common/hexutil
github.com/ethereum/go-ethereum/common/math
github.com/ethereum/go-ethereum/crypto/sha3
github.com/ethereum/go-ethereum/rlp
github.com/ethereum/go-ethereum/crypto/secp256k1
github.com/ethereum/go-ethereum/common
[...]
github.com/ethereum/go-ethereum/cmd/utils
github.com/ethereum/go-ethereum/cmd/geth
Done building.
Exécutez "build/bin/geth" pour exécuter geth.
$

Assurons-nous que geth fonctionne sans l’excuter :

$ ./build/bin/geth version

Geth
Version: 1.9.11-unstable
Git Commit: 0b284f6c6cfc6df452ca23f9454ee16a6330cb8e
Git Commit Date: 20200123
Architecture: amd64
Protocol Versions: [64 63]
Go Version: go1.13.4
Operating System: linux
[...]

Votre commande geth version peut afficher des informations légèrement différentes, mais vous devriez voir un rapport de version similaire à celui présenté ici.

Les sections suivantes expliquent le défi avec la synchronisation initiale de la chaîne de blocs d’Ethereum.

La première synchronisation des chaînes de blocs basées sur Ethereum

Traditionnellement, lors de la synchronisation d’une chaîne de blocs Ethereum, votre client téléchargerait et validerait chaque bloc et chaque transaction depuis le tout début, c’est-à-dire depuis le bloc de genèse.

Bien qu’il soit possible de synchroniser entièrement la chaîne de blocs de cette façon, ce type de synchronisation prendra très longtemps et nécessite beaucoup de ressources (il nécessitera beaucoup plus de RAM, et prendra en effet très longtemps si vous n’avez pas stockage).

De nombreuses chaînes de blocs basées sur Ethereum ont été victimes d’attaques par déni de service fin 2016. Les chaînes de blocs concernées auront tendance à se synchroniser lentement lors d’une synchronisation complète.

Par exemple, sur Ethereum, un nouveau client progressera rapidement jusqu’à atteindre le bloc 2 283 397. Ce bloc a été miné le 18 septembre 2016 et marque le début des attaques DoS. De ce bloc au bloc 2 700 031 (26 novembre 2016), la validation des transactions devient extrêmement lente, gourmande en mémoire et gourmande en E/S. Cela se traduit par des temps de validation supérieurs à 1 minute par bloc. Ethereum a mis en œuvre une série de mises à niveau, à l’aide d’embranchements divergents, pour remédier aux vulnérabilités sous-jacentes qui ont été exploitées dans les attaques DoS. Ces mises à niveau ont également nettoyé la chaîne de blocs en supprimant quelque 20 millions de comptes vides créés par des transactions de spam.

Si vous synchronisez avec une validation complète, votre client ralentira et peut prendre plusieurs jours, voire plus, pour valider les blocs affectés par les attaques DoS.

Heureusement, la plupart des clients Ethereum effectuent désormais par défaut une synchronisation "rapide" qui ignore la validation complète des transactions jusqu’à ce qu’elle soit synchronisée avec la pointe de la chaîne de blocs, puis reprend la validation complète.

Geth effectue une synchronisation rapide par défaut pour Ethereum. Vous devrez peut-être vous référer aux instructions spécifiques pour l’autre chaîne Ethereum choisie.

Parity effectue également une synchronisation rapide par défaut.

Note

Geth ne peut opérer une synchronisation rapide que lorsqu’il démarre avec une base de données de blocs vide. Si vous avez déjà commencé la synchronisation sans mode rapide, Geth ne peut pas basculer de mode. Il est plus rapide de supprimer le répertoire de données de la chaîne de blocs et de commencer la synchronisation rapide depuis le début que de continuer la synchronisation avec une validation complète. Veillez à ne supprimer aucun portefeuille lors de la suppression des données de la chaîne de blocs !

Exécuter Geth ou Parity

Maintenant que vous comprenez les défis de la "première synchronisation", vous êtes prêt à démarrer un client Ethereum et à synchroniser la chaîne de blocs. Pour Geth et Parity, vous pouvez utiliser l’option --help pour voir tous les paramètres de configuration. Les paramètres par défaut sont généralement judicieux et appropriés pour la plupart des utilisations. Choisissez comment configurer les paramètres facultatifs en fonction de vos besoins, puis démarrez Geth ou Parity pour synchroniser la chaîne. Puis attendre…​

Tip

La synchronisation de la chaîne de blocs Ethereum prendra entre une demi-journée sur un système très rapide avec beaucoup de RAM et plusieurs jours sur un système plus lent.

L’interface JSON-RPC

Les clients Ethereum offrent une interface de programmation d’application et un ensemble de commandes Remote Procedure Call (RPC), qui sont encodées en JavaScript Object Notation (JSON). Vous verrez cela appelé API JSON-RPC. Essentiellement, l’API JSON-RPC est une interface qui nous permet d’écrire des programmes qui utilisent un client Ethereum comme passerelle vers un réseau Ethereum et une chaîne de blocs.

Habituellement, l’interface RPC est proposée en tant que service HTTP sur le port 8545. Pour des raisons de sécurité, il est restreint, par défaut, de n’accepter que les connexions de localhost (l’adresse IP de votre propre ordinateur, qui est 127.0.0.1).

Pour accéder à l’API JSON-RPC, vous pouvez utiliser une bibliothèque spécialisée (écrite dans le langage de programmation de votre choix) qui fournit des appels de fonction "stub" correspondant à chaque commande RPC disponible, ou vous pouvez construire manuellement des requêtes HTTP et envoyer/recevoir des requêtes codées en JSON. Vous pouvez même utiliser un client HTTP de ligne de commande générique, comme curl, pour appeler l’interface RPC. Essayons ça. Tout d’abord, assurez-vous que Geth est opérationnel, configuré avec --rpc pour autoriser l’accès HTTP à l’interface RPC, puis passez à une nouvelle fenêtre de terminal (par exemple, avec Ctrl-Maj-N ou Ctrl-Maj-T dans un fenêtre de terminal) comme indiqué ici :

$ curl -X POST -H "Content-Type: application/json" --data \
  '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' \
  http://localhost:8545

{"jsonrpc":"2.0","id":1,
"result":"Geth/v1.9.11-unstable-0b284f6c-20200123/linux-amd64/go1.13.4"}

Dans cet exemple, nous utilisons curl pour établir une connexion HTTP à l’adresse http://localhost:8545. Nous exécutons déjà geth, qui propose l’API JSON-RPC en tant que service HTTP sur le port 8545. Nous demandons à curl d’utiliser la commande HTTP POST et d’identifier le contenu en tant que type application/json. Enfin, nous transmettons une requête encodée en JSON en tant que composant data de notre requête HTTP. La majeure partie de notre ligne de commande consiste simplement à configurer curl pour établir correctement la connexion HTTP. La partie intéressante est la commande JSON-RPC que nous émettons :

{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}

La requête JSON-RPC est formatée conformément à la https://www.jsonrpc.org/specification [spécification JSON-RPC 2.0]. Chaque demande contient quatre éléments :

jsonrpc

Version du protocole JSON-RPC. Cela DOIT être exactement "2.0".

method

Le nom de la méthode à invoquer.

params

Une valeur structurée qui contient les valeurs de paramètre à utiliser lors de l’invocation de la méthode. Ce membre PEUT être omis.

id

Un identifiant établi par le client qui DOIT contenir une valeur String, Number ou NULL si elle est incluse. Le serveur DOIT répondre avec la même valeur dans l’objet de réponse s’il est inclus. Ce membre est utilisé pour corréler le contexte entre les deux objets.

Tip

Le paramètre id est principalement utilisé lorsque vous effectuez plusieurs requêtes dans un seul appel JSON-RPC, une pratique appelée batching. Le traitement par lots est utilisé pour éviter la surcharge d’une nouvelle connexion HTTP et TCP pour chaque requête. Dans le contexte Ethereum, par exemple, nous utiliserions le traitement par lots si nous voulions récupérer des milliers de transactions sur une seule connexion HTTP. Lors du traitement par lots, vous définissez un id différent pour chaque demande, puis le faites correspondre au id dans chaque réponse du serveur JSON-RPC. Le moyen le plus simple d’implémenter ceci est de maintenir un compteur et d’incrémenter la valeur pour chaque demande.

La réponse que nous recevons est :

{"jsonrpc":"2.0","id":1,
"result":"Geth/v1.9.11-unstable-0b284f6c-20200123/linux-amd64/go1.13.4"}

Cela nous indique que l’API JSON-RPC est servie par la version 1.13.4 du client Geth.

Essayons quelque chose d’un peu plus intéressant. Dans l’exemple suivant, nous demandons à l’API JSON-RPC le prix actuel du gaz en wei :

$ curl -X POST -H "Content-Type: application/json" --data \
  '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":4213}' \
  http://localhost:8545

{"jsonrpc":"2.0","id":4213,"result":"0x430e23400"}

La réponse, 0x430e23400, nous indique que le prix actuel du gaz est de 18 gwei (gigawei ou milliard de wei). Si, comme nous, vous ne pensez pas en hexadécimal, vous pouvez le convertir en décimal sur la ligne de commande avec un petit bash-fu :

$ echo $((0x430e23400))

18000000000

L’API JSON-RPC complète peut être étudiée sur le wiki Ethereum.

Mode de compatibilité Geth de Parity

Parity a un "mode de compatibilité Geth" spécial, où il offre une API JSON-RPC identique à celle offerte par Geth. Pour exécuter Parity dans ce mode, utilisez le commutateur --geth:

$ parity --geth

Clients Ethereum distants

Les clients distants offrent un sous-ensemble des fonctionnalités d’un client complet. Ils ne stockent pas la chaîne de blocs Ethereum complète, ils sont donc plus rapides à configurer et nécessitent beaucoup moins de stockage de données.

Ces clients offrent généralement la possibilité d’effectuer une ou plusieurs des actions suivantes :

  • Gérer les clés privées et les adresses Ethereum dans un portefeuille.

  • Créer, signer et diffuser des transactions.

  • Interagir avec les contrats intelligents, en utilisant la charge utile des données.

  • Naviguer et interagir avec les DApps.

  • Offrir des liens vers des services externes tels que des explorateurs de blocs.

  • Convertir les unités d’ether et récupérer les taux de change à partir de sources externes.

  • Injecter une instance web3 dans le navigateur Web en tant qu’objet JavaScript.

  • Utiliser une instance web3 fournie/injectée dans le navigateur par un autre client.

  • Accéder aux services RPC sur un nœud Ethereum local ou distant.

Certains clients distants, par exemple les portefeuilles mobiles (smartphones), n’offrent que des fonctionnalités de portefeuille de base. Les autres clients distants sont des navigateurs DApp à part entière. Les clients distants offrent généralement certaines des fonctions d’un client Ethereum à nœud complet sans synchroniser une copie locale de la chaîne de blocs Ethereum en se connectant à un nœud complet exécuté ailleurs, par exemple, par vous localement sur votre machine ou sur un serveur Web, ou par un tiers sur leurs serveurs.

Examinons certains des clients distants les plus populaires et les fonctions qu’ils offrent.

Portefeuilles mobiles (Smartphone)

Tous les portefeuilles mobiles sont des clients distants, car les téléphones intelligents ne disposent pas des ressources adéquates pour exécuter un client Ethereum complet. Les clients légers sont en développement et ne sont pas généralement utilisés pour Ethereum. Dans le cas de Parity, le client léger est marqué "expérimental" et peut être utilisé en exécutant parity avec l’option --light.

Les portefeuilles mobiles populaires incluent les éléments suivants (nous les énumérons simplement à titre d’exemples ; il ne s’agit pas d’une approbation ou d’une indication de la sécurité ou de la fonctionnalité de ces portefeuilles) :

Jaxx

Un portefeuille mobile multidevises basé sur les valeurs mnémoniques BIP-39, avec prise en charge de Bitcoin, Litecoin, Ethereum, Ethereum Classic, ZCash, une variété de jetons ERC20 et de nombreuses autres devises. Jaxx est disponible sur Android et iOS, en tant que portefeuille de plug-in de navigateur et en tant que portefeuille de bureau pour une variété de systèmes d’exploitation.

Status

Un portefeuille mobile et un navigateur DApp, avec prise en charge d’une variété de jetons et de DApps populaires. Disponible pour iOS et Android.

Trust Wallet

Un portefeuille mobile multi-devises qui prend en charge Ethereum et Ethereum Classic ainsi que les jetons ERC20 et ERC223. Trust Wallet est disponible pour iOS et Android.

Cipher Browser

Un navigateur et un portefeuille DApp mobile complet compatible avec Ethereum qui permet l’intégration avec les applications et les jetons Ethereum. Disponible pour iOS et Android.

Portefeuilles de navigateur

Une variété de portefeuilles et de navigateurs DApp sont disponibles en tant que plug-ins ou extensions de navigateurs Web tels que Chrome et Firefox. Ce sont des clients distants qui s’exécutent dans votre navigateur.

Certains des plus populaires sont MetaMask, Jaxx, MyEtherWallet et MyCrypto.

MetaMask

MetaMask, introduit dans Les bases d’Ethereum, est un portefeuille polyvalent basé sur un navigateur, un client RPC et un explorateur de contrats de base. Il est disponible sur Chrome, Firefox, Opera et Brave Browser.

Contrairement aux autres portefeuilles de navigateur, MetaMask injecte une instance web3 dans le contexte JavaScript du navigateur, agissant comme un client RPC qui se connecte à une variété de chaînes de blocs Ethereum (mainnet, Ropsten testnet, Kovan testnet, nœud RPC local, etc.). La possibilité d’injecter une instance web3 et d’agir comme une passerelle vers des services RPC externes fait de MetaMask un outil très puissant pour les développeurs et les utilisateurs. Il peut être combiné, par exemple, avec MyEtherWallet ou MyCrypto, agissant comme un fournisseur web3 et une passerelle RPC pour ces outils.

Jaxx

Jaxx, qui a été présenté comme un portefeuille mobile dans la section précédente, est également disponible en tant qu’extension Chrome et Firefox et en tant que portefeuille de bureau.

MyEtherWallet (MEW)

MyEtherWallet est un client distant JavaScript basé sur un navigateur qui offre :

  • Un pont vers les portefeuilles matériels populaires tels que Trezor et Ledger

  • Une interface web3 pouvant se connecter à une instance web3 injectée par un autre client (par exemple, MetaMask)

  • Un client RPC pouvant se connecter à un client complet Ethereum

  • Une interface de base qui peut interagir avec des contrats intelligents, étant donné l’adresse d’un contrat et l’interface binaire d’application (ABI)

  • Une application mobile, MEWConnect, qui permet d’utiliser un appareil Android ou iOS compatible pour stocker des fonds, de la même manière qu’un portefeuille matériel.

  • Un portefeuille logiciel fonctionnant en JavaScript

Warning

Vous devez être très prudent lorsque vous accédez à MyEtherWallet et à d’autres portefeuilles JavaScript basés sur un navigateur, car ils sont des cibles fréquentes pour le phishing. Utilisez toujours un signet et non un moteur de recherche ou un lien pour accéder à l’URL Web correcte.

MyCrypto

Début 2018, le projet MyEtherWallet s’est scindé en deux implémentations concurrentes, guidées par deux équipes de développement indépendantes : une "fourche" ou "fork", comme on l’appelle dans le développement à source libre. Les deux projets s’appellent MyEtherWallet (la marque originale) et MyCrypto. MyCrypto offre des fonctionnalités presque identiques à MyEtherWallet, mais au lieu d’utiliser MEWConnect, il offre une connexion à l’application mobile Parity Signer. Comme MEWConnect, Parity Signer stocke les clés sur le téléphone et s’interface avec MyCrypto de la même manière qu’un portefeuille matériel.

Mist (obsolète)

Mist était le premier navigateur compatible avec Ethereum, construit par la Fondation Ethereum. Il contenait un portefeuille basé sur un navigateur qui était la première implémentation de la norme de jeton ERC20 (Fabian Vogelsteller, auteur d’ERC20, était également le principal développeur de Mist). Mist a également été le premier portefeuille à introduire la somme de contrôle camelCase (EIP-55). Depuis mars 2019, Mist est obsolète et ne doit plus être utilisé.

Conclusion

Dans ce chapitre, nous avons exploré les clients Ethereum. Vous avez téléchargé, installé et synchronisé un client, devenant un participant au réseau Ethereum et contribuant à la santé et à la stabilité du système en répliquant la chaîne de blocs sur votre propre ordinateur.

Cryptographie

Bannière Amazon du livre Maîtriser Ethereum

L’une des technologies fondamentales d’Ethereum est la cryptographie, qui est une branche des mathématiques largement utilisées en sécurité informatique. La cryptographie signifie "écriture secrète" en grec, mais l’étude de la cryptographie englobe plus que la simple écriture secrète, appelée cryptage. La cryptographie peut, par exemple, également être utilisée pour prouver la connaissance d’un secret sans le révéler (par exemple, avec une signature numérique), ou pour prouver l’authenticité des données (par exemple, avec des empreintes digitales, également appelées "hachages"). Ces types de preuves cryptographiques sont des outils mathématiques essentiels au fonctionnement de la plate-forme Ethereum (et, en fait, de tous les systèmes de chaîne de blocs), et sont également largement utilisés dans les applications Ethereum.

Notez qu’au moment de la publication, aucune partie du protocole Ethereum n’implique de cryptage ; c’est-à-dire que toutes les communications avec la plateforme Ethereum et entre les nœuds (y compris les données de transaction) sont non cryptées et peuvent (nécessairement) être lues par n’importe qui. C’est ainsi que tout le monde peut vérifier l’exactitude des mises à jour d’état et qu’un consensus peut être atteint. À l’avenir, des outils cryptographiques avancés, tels que les preuves à zéro connaissance et le cryptage homomorphe, seront disponibles, ce qui permettra d’enregistrer certains calculs cryptés sur la chaîne de blocs tout en permettant le consensus ; cependant, bien que des dispositions aient été prises à leur intention, elles n’ont pas encore été déployées.

Dans ce chapitre, nous présenterons une partie de la cryptographie utilisée dans Ethereum : à savoir la cryptographie à clé publique (PKC), qui est utilisée pour contrôler la propriété des fonds, sous la forme de clés privées et d’adresses.

Clés et adresses

Comme nous l’avons vu précédemment dans le livre, Ethereum a deux types de comptes différents : les comptes détenus par des tiers ou en externe (Externally Owned Accounts ou EOA) et les contrats. La propriété de l’ether par les EOAs est établie par des clés privées numériques, des adresses Ethereum et des signatures numériques. Les clés privées sont au cœur de toutes les interactions des utilisateurs avec Ethereum. En fait, les adresses de compte sont directement dérivées des clés privées : une clé privée détermine de manière unique une seule adresse Ethereum, également appelée compte.

Les clés privées ne sont en aucun cas utilisées directement dans le système Ethereum ; ils ne sont jamais transmis ou stockés sur Ethereum. C’est-à-dire que les clés privées doivent rester privées et ne jamais apparaître dans les messages transmis au réseau, ni être stockées en chaîne ; seules les adresses de compte et les signatures numériques sont transmises et stockées sur le système Ethereum. Pour plus d’informations sur la manière de conserver les clés privées en toute sécurité, consultez Contrôle et responsabilité et Portefeuilles.

L’accès et le contrôle des fonds sont réalisés avec des signatures numériques, qui sont également créées à l’aide de la clé privée. Les transactions Ethereum nécessitent une signature numérique valide pour être incluses dans la chaîne de blocs. Toute personne disposant d’une copie d’une clé privée a le contrôle du compte correspondant et de tout ether qu’il détient. En supposant qu’un utilisateur garde sa clé privée en sécurité, les signatures numériques dans les transactions Ethereum prouvent le véritable propriétaire des fonds, car elles prouvent la propriété de la clé privée.

Dans les systèmes basés sur la cryptographie à clé publique, tels que ceux utilisés par Ethereum, les clés sont fournies par paires composées d’une clé privée (secrète) et d’une clé publique. Considérez la clé publique comme similaire à un numéro de compte bancaire et la clé privée comme similaire au code PIN secret ; c’est ce dernier qui fournit le contrôle sur le compte, et le premier qui l’identifie aux autres. Les clés privées elles-mêmes sont très rarement vues par les utilisateurs d’Ethereum ; pour la plupart, ils sont stockés (sous forme cryptée) dans des fichiers spéciaux et gérés par le logiciel de portefeuille Ethereum.

Dans la partie paiement d’une transaction Ethereum, le destinataire prévu est représenté par une adresse Ethereum, qui est utilisée de la même manière que les détails du compte bénéficiaire d’un virement bancaire. Comme nous le verrons plus en détail sous peu, une adresse Ethereum pour un EOA est générée à partir de la partie clé publique d’une paire de clés. Cependant, toutes les adresses Ethereum ne représentent pas des paires de clés publiques-privées ; ils peuvent aussi représenter des contrats, qui, comme nous le verrons dans Contrats intelligents et Solidity, ne sont pas soutenus par des clés privées.

Dans le reste de ce chapitre, nous allons d’abord explorer la cryptographie de base un peu plus en détail et expliquer les mathématiques utilisées dans Ethereum. Ensuite, nous verrons comment les clés sont générées, stockées et gérées. Enfin, nous passerons en revue les différents formats d’encodage utilisés pour représenter les clés privées, les clés publiques et les adresses.

Cryptographie à clé publique et cryptomonnaie

La cryptographie à clé publique (également appelée "cryptographie asymétrique") est un élément central de la sécurité de l’information moderne. Le protocole d’échange de clé, d’abord publié dans les années 1970 par Martin Hellman, Whitfield Diffie et Ralph Merkle, a été une percée monumentale qui a suscité la première grande vague d’intérêt public dans le domaine de la cryptographie. Avant les années 1970, de solides connaissances cryptographiques étaient gardées secrètes par les gouvernements.

La cryptographie à clé publique utilise des clés uniques pour sécuriser les informations. Ces clés sont basées sur des fonctions mathématiques qui ont une propriété particulière : il est facile de les calculer, mais difficile de calculer leur inverse. Sur la base de ces fonctions, la cryptographie permet la création de secrets numériques et de signatures numériques infalsifiables, qui sont sécurisées par les lois des mathématiques.

Par exemple, multiplier deux grands nombres premiers ensemble est trivial. Mais étant donné le produit de deux grands nombres premiers, il est très difficile de trouver les facteurs premiers (un problème appelé factorisation primaire ou première). Disons que nous présentons le nombre 8 018 009 et que nous vous disons qu’il est le produit de deux nombres premiers. Trouver ces deux nombres premiers est beaucoup plus difficile pour vous que pour moi de les multiplier pour produire 8 018 009.

Certaines de ces fonctions mathématiques peuvent être facilement inversées si vous connaissez des informations secrètes. Dans l’exemple précédent, si je vous dis que l’un des facteurs premiers est 2 003, vous pouvez trivialement trouver l’autre avec une simple division : 8 018 009 ÷ 2 003 = 4 003. Ces fonctions sont souvent appelées fonctions de trappe car elles sont très difficiles à inverser à moins que vous ne receviez une information secrète qui peut être utilisée comme raccourci pour inverser la fonction.

Une catégorie plus avancée de fonctions mathématiques utiles en cryptographie est basée sur des opérations arithmétiques sur une courbe elliptique. Dans l’arithmétique des courbes elliptiques, la multiplication modulo d’un nombre premier est simple mais la division (l’inverse) est pratiquement impossible. C’est ce qu’on appelle le problème de logarithme discret et il n’y a actuellement aucune trappe connue. La cryptographie à courbe elliptique est largement utilisée dans les systèmes informatiques modernes et constitue la base de l’utilisation par Ethereum (et d’autres cryptomonnaies) des clés privées et des signatures numériques.

Note

Consultez les ressources suivantes si vous souhaitez en savoir plus sur la cryptographie et les fonctions mathématiques utilisées dans la cryptographie moderne :

Dans Ethereum, nous utilisons la cryptographie à clé publique (également connue sous le nom de cryptographie asymétrique) pour créer la paire de clés publique-privée dont nous avons parlé dans ce chapitre. Ils sont considérés comme une "paire" car la clé publique est dérivée de la clé privée. Ensemble, ils représentent un compte Ethereum en fournissant, respectivement, un identifiant de compte accessible au public (l’adresse) et un contrôle privé sur l’accès à tout ether du compte et sur toute authentification dont le compte a besoin lors de l’utilisation de contrats intelligents. La clé privée contrôle l’accès en étant l’élément d’information unique nécessaire pour créer des signatures numériques, qui sont nécessaires pour signer des transactions afin de dépenser des fonds sur le compte. Les signatures numériques sont également utilisées pour authentifier les propriétaires ou les utilisateurs de contrats, comme nous le verrons dans Contrats intelligents et Solidity.

Tip

Dans la plupart des implémentations de portefeuille, les clés privées et publiques sont stockées ensemble sous la forme d’une paire de clés pour plus de commodité. Cependant, la clé publique peut être trivialement calculée à partir de la clé privée, de sorte qu’il est également possible de ne stocker que la clé privée.

Une signature numérique peut être créée pour signer n’importe quel message. Pour les transactions Ethereum, les détails de la transaction elle-même sont utilisés comme message. Les mathématiques de la cryptographie - dans ce cas, la cryptographie à courbe elliptique - permettent de combiner le message (c’est-à-dire les détails de la transaction) avec la clé privée pour créer un code qui ne peut être produit qu’avec la connaissance de la clé privée. Ce code s’appelle la signature numérique. Notez qu’une transaction Ethereum est essentiellement une demande d’accès à un compte particulier avec une adresse Ethereum particulière. Lorsqu’une transaction est envoyée au réseau Ethereum afin de déplacer des fonds ou d’interagir avec des contrats intelligents, elle doit être envoyée avec une signature numérique créée avec la clé privée correspondant à l’adresse Ethereum en question. Les mathématiques de la courbe elliptique signifient que n’importe qui peut vérifier qu’une transaction est valide, en vérifiant que la signature numérique correspond aux détails de la transaction et à l’adresse Ethereum à laquelle l’accès est demandé. La vérification n’implique pas du tout la clé privée ; qui reste privé. Cependant, le processus de vérification détermine sans aucun doute que la transaction ne peut provenir que de quelqu’un avec la clé privée qui correspond à la clé publique derrière l’adresse Ethereum. C’est la « magie » de la cryptographie à clé publique.

Tip

Il n’y a pas de cryptage dans le cadre du protocole Ethereum - tous les messages envoyés dans le cadre du fonctionnement du réseau Ethereum peuvent (nécessairement) être lus par tout le monde. En tant que telles, les clés privées ne sont utilisées que pour créer des signatures numériques pour l’authentification des transactions.

Clés privées

Une clé privée est simplement un nombre, choisi au hasard. La propriété et le contrôle de la clé privée sont à la base du contrôle de l’utilisateur sur tous les fonds associés à l’adresse Ethereum correspondante, ainsi que de l’accès aux contrats qui autorisent cette adresse. La clé privée est utilisée pour créer les signatures nécessaires pour dépenser de l’ether en prouvant la propriété des fonds utilisés dans une transaction. La clé privée doit rester secrète à tout moment, car la révéler à des tiers équivaut à leur donner le contrôle de l’ether et des contrats sécurisés par cette clé privée. La clé privée doit également être sauvegardée et protégée contre toute perte accidentelle. S’il est perdu, il ne peut pas être récupéré et les fonds garantis par celui-ci sont également perdus à jamais.

Tip

La clé privée Ethereum n’est qu’un chiffre. Une façon de choisir vos clés privées au hasard consiste simplement à utiliser une pièce de monnaie, un crayon et du papier : lancez une pièce 256 fois et vous obtenez les chiffres binaires d’une clé privée aléatoire que vous pouvez utiliser dans un portefeuille Ethereum (probablement - voir la section suivante ). La clé publique et l’adresse peuvent ensuite être générées à partir de la clé privée.

Générer une clé privée à partir d’un nombre aléatoire

Le premier et l’étape la plus importante dans la génération de clés consiste à trouver une source sécurisée d’entropie ou de caractère aléatoire. La création d’une clé privée Ethereum consiste essentiellement à choisir un nombre compris entre 1 et 2256. La méthode exacte que vous utilisez pour choisir ce nombre n’a pas d’importance tant qu’elle n’est pas prévisible ou déterministe. Le logiciel Ethereum utilise le générateur de nombres aléatoires du système d’exploitation sous-jacent pour produire 256 bits aléatoires. Habituellement, le générateur de nombres aléatoires du système d’exploitation est initialisé par une source humaine d’aléatoire, c’est pourquoi il peut vous être demandé de remuer votre souris pendant quelques secondes ou d’appuyer sur des touches aléatoires de votre clavier. Une alternative pourrait être le bruit de rayonnement cosmique sur le canal du microphone de l’ordinateur.

Plus précisément, une clé privée peut être n’importe quel nombre différent de zéro jusqu’à un très grand nombre légèrement inférieur à 2256 - un énorme nombre à 78 chiffres, environ 1,158 * 1077. Le nombre exact partage les 38 premiers chiffres avec 2256 et est défini comme l’ordre de la courbe elliptique utilisée dans Ethereum (voir La cryptographie à courbe elliptique expliquée). Pour créer une clé privée, nous choisissons au hasard un nombre de 256 bits et vérifions qu’il se trouve dans la plage valide. En termes de programmation, cela est généralement réalisé en introduisant une chaîne encore plus grande de bits aléatoires (collectés à partir d’une source aléatoire sécurisée par cryptographie) dans un algorithme de hachage de 256 bits tel que Keccak-256 ou SHA-256, qui produiront commodément un nombre de 256 bits. Si le résultat est dans la plage valide, nous avons une clé privée appropriée. Sinon, nous réessayons simplement avec un autre nombre aléatoire.

Tip

2256 - la taille de l’espace de clé privée d’Ethereum - est un nombre insondable. Il est d’environ 1077 en décimal ; c’est-à-dire un nombre à 77 chiffres. À titre de comparaison, on estime que l’univers visible contient 1080 atomes. Ainsi, il y a presque assez de clés privées pour donner à chaque atome de l’univers un compte Ethereum. Si vous choisissez une clé privée au hasard, il est impossible que quelqu’un la devine ou la choisisse lui-même.

Notez que le processus de génération de clé privée est hors ligne ; il ne nécessite aucune communication avec le réseau Ethereum, ni même aucune communication avec qui que ce soit. En tant que tel, afin de choisir un nombre que personne d’autre ne choisira jamais, il doit être vraiment aléatoire. Si vous choisissez vous-même le numéro, la probabilité que quelqu’un d’autre l’essaie (et s’enfuie ensuite avec votre ether) est trop élevée. Utiliser un mauvais générateur de nombres aléatoires (comme la fonction pseudo-aléatoire rand dans la plupart des langages de programmation) est encore pire, car il est encore plus évident et encore plus facile à reproduire. Tout comme pour les mots de passe des comptes en ligne, la clé privée doit être impossible à deviner. Heureusement, vous n’avez jamais besoin de vous souvenir de votre clé privée, vous pouvez donc adopter la meilleure approche possible pour la choisir : à savoir, le vrai hasard.

Warning

N’écrivez pas votre propre code pour créer un nombre aléatoire ou n’utilisez pas un "simple" générateur de nombres aléatoires proposé par votre langage de programmation. Il est essentiel que vous utilisiez un générateur de nombres pseudo-aléatoires cryptographiquement sécurisé (tel que CSPRNG) avec une valeur d’amorçage provenant d’une source d’entropie suffisante. Étudiez la documentation de la bibliothèque de générateurs de nombres aléatoires que vous choisissez pour vous assurer qu’elle est cryptographiquement sécurisée. La mise en œuvre correcte de la bibliothèque CSPRNG est essentielle à la sécurité des clés.

Ce qui suit est une clé privée générée aléatoirement affichée au format hexadécimal (256 bits affichés sous la forme de 64 chiffres hexadécimaux, chacun de 4 bits) :

f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315

Clés publiques

Une clé publique Ethereum est un point sur une courbe elliptique, ce qui signifie qu’il s’agit d’un ensemble de coordonnées x et y qui satisfont l’équation de la courbe elliptique.

En termes plus simples, une clé publique Ethereum est constituée de deux nombres réunis. Ces chiffres sont produits à partir de la clé privée par un calcul qui ne peut aller que dans un sens. Cela signifie qu’il est trivial de calculer une clé publique si vous avez la clé privée, mais vous ne pouvez pas calculer la clé privée à partir de la clé publique.

Warning

Des MATHÉMATIQUES sont sur le point d’arriver ! Ne pas paniquer. Si vous commencez à vous perdre à n’importe quel moment dans les paragraphes suivants, vous pouvez ignorer les quelques sections suivantes. Il existe de nombreux outils et bibliothèques qui feront le calcul pour vous.

La clé publique est calculée à partir de la clé privée en utilisant la multiplication par courbe elliptique, qui est pratiquement irréversible : K = k * G, où k est la clé privée, G est un point constant appelé le point générateur, K est la clé publique résultante et * est l’opérateur spécial de "multiplication" de la courbe elliptique. Notez que la multiplication de courbe elliptique n’est pas comme la multiplication normale. Il partage des attributs fonctionnels avec la multiplication normale, mais c’est à peu près tout. Par exemple, l’opération inverse (qui serait une division pour des nombres normaux), connue sous le nom de "trouver le logarithme discret" - c’est-à-dire calculer k si vous connaissez K - est aussi difficile que d’essayer toutes les valeurs possibles de k (une recherche par force brute cela prendra probablement plus de temps que cet univers ne le permettra).

En termes plus simples : l’arithmétique sur la courbe elliptique est différente de l’arithmétique entière "régulière". Un point (G) peut être multiplié par un entier (k) pour produire un autre point (K). Mais la division n’existe pas, il n’est donc pas possible de simplement "diviser" la clé publique K par le point G pour calculer la clé privée k. Il s’agit de la fonction mathématique à sens unique décrite dans Cryptographie à clé publique et cryptomonnaie.

Note

La multiplication de courbe elliptique est un type de fonction que les cryptographes appellent une fonction "à sens unique" : elle est facile à faire dans un sens (multiplication) et impossible à faire dans le sens inverse (division). Le propriétaire de la clé privée peut facilement créer la clé publique puis la partager avec le monde, sachant que personne ne peut inverser la fonction et calculer la clé privée à partir de la clé publique. Cette astuce mathématique devient la base de signatures numériques infalsifiables et sécurisées qui prouvent la propriété des fonds Ethereum et le contrôle des contrats.

Avant de montrer comment générer une clé publique à partir d’une clé privée, examinons un peu plus en détail la cryptographie à courbe elliptique.

La cryptographie à courbe elliptique expliquée

La courbe elliptique cryptographique est un type de cryptographie asymétrique ou cryptographie à clé publique basée sur le problème du logarithme discret exprimé par addition et multiplication sur les points d’une courbe elliptique.

Une visualisation d’une courbe elliptique est un exemple de courbe elliptique, similaire à celle utilisée par Ethereum.

Note

Ethereum utilise exactement la même courbe elliptique, appelée secp256k1, que Bitcoin. Cela permet de réutiliser de nombreuses bibliothèques et outils de courbes elliptiques de Bitcoin.

ecc-curve
Figure 25. Une visualisation d’une courbe elliptique

Ethereum utilise une courbe elliptique spécifique et un ensemble de constantes mathématiques, telles que définies dans une norme appelée secp256k1, établie par le National Institute of Standards and Technology (NIST) des États-Unis. La courbe secp256k1 est définie par la fonction suivante, qui produit une courbe elliptique :

y2 = (x3 + 7) over (𝔽 p)

ou alors:

y2 mod p = (x3 + 7) mod p

Le mod p (modulo nombre premier p) indique que cette courbe est sur un corps fini d’ordre premier p, également écrit comme (𝔽 p), où p = 2256 – 232 – 29 – 28 – 27 – 26 – 24 – 1, qui est un très grand nombre premier.

Parce que cette courbe est définie sur un champ fini d’ordre premier au lieu de sur les nombres réels, elle ressemble à un motif de points dispersés en deux dimensions, ce qui la rend difficile à visualiser. Cependant, le calcul est identique à celui d’une courbe elliptique sur des nombres réels. A titre d’exemple, Cryptographie sur courbe elliptique: visualisation d’une courbe elliptique sur F(p), avec p=17 montre la même courbe elliptique sur un champ fini beaucoup plus petit d’ordre premier 17, montrant un motif de points sur une grille. La courbe elliptique secp256k1 Ethereum peut être considérée comme un motif beaucoup plus complexe de points sur une grille insondable.

ecc-over-F17-math
Figure 26. Cryptographie sur courbe elliptique: visualisation d’une courbe elliptique sur F(p), avec p=17

Ainsi, par exemple, ce qui suit est un point Q avec des coordonnées (x,y) qui est un point sur la courbe secp256k1 :

Q =
(49790390825249384486033144355916864607616083520101638681403973749255924539515,
59574132161899900045862086493921015780032175291755807399284007721050341297360)

[example_1] montre comment vous pouvez vérifier cela vous-même en utilisant Python. Les variables x et y sont les coordonnées du point Q, comme dans l’exemple précédent. La variable p est l’ordre premier de la courbe elliptique (le premier qui est utilisé pour toutes les opérations modulo). La dernière ligne de Python est l’équation de la courbe elliptique (l’opérateur % en Python est l’opérateur modulo). Si x et y sont bien les coordonnées d’un point sur la courbe elliptique, alors elles satisfont l’équation et le résultat est zéro (0L est un entier long valant zéro). Essayez vous-même, en tapant python sur une ligne de commande et en copiant chaque ligne (après l’invite + >>> +)de la liste.

Utiliser Python pour confirmer que ce point est sur la courbe elliptique
Python 3.4.0 (default, Mar 30 2014, 19:23:13)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> p = 115792089237316195423570985008687907853269984665640564039457584007908834 \
671663
>>> x = 49790390825249384486033144355916864607616083520101638681403973749255924539515
>>> y = 59574132161899900045862086493921015780032175291755807399284007721050341297360
>>> (x ** 3 + 7 - y**2) % p
0L

Opérations arithmétiques sur les courbes elliptiques

Beaucoup de mathématiques sur les courbes elliptiques ressemblent beaucoup à l’arithmétique des nombres entiers que nous avons apprise à l’école. Plus précisément, nous pouvons définir un opérateur d’addition qui, au lieu de sauter le long de la droite numérique, saute vers d’autres points de la courbe. Une fois que nous avons l’opérateur d’addition, nous pouvons également définir la multiplication d’un point et d’un nombre entier, ce qui équivaut à une addition répétée.

L’addition de courbe elliptique est définie de telle sorte que, étant donné deux points P1 et P2 sur la courbe elliptique, il existe un troisième point P3 = P1 + P2, également sur la courbe elliptique.

Géométriquement, ce troisième point P3 est calculé en traçant une ligne entre P1 et P2. Cette ligne coupera la courbe elliptique exactement à un endroit supplémentaire (étonnamment). Appelez ce point P3' = (x, y). Réfléchissez ensuite sur l’axe des x pour obtenir P3 = (x, –y).

Si P1 et P2 sont au même point, la ligne "entre" P1 et P2 devrait s’étendre pour être la tangente à la courbe en ce point P1. Cette tangente coupera la courbe exactement en un nouveau point. Vous pouvez utiliser des techniques de calcul pour déterminer la pente de la ligne tangente. Curieusement, ces techniques fonctionnent, même si nous restreignons notre intérêt aux points de la courbe à deux coordonnées entières !

Dans les mathématiques des courbes elliptiques, il existe également un point appelé "point à l’infini", qui correspond à peu près au rôle du nombre zéro en addition. Sur les ordinateurs, il est parfois représenté par x = y = 0 (ce qui ne satisfait pas l’équation de la courbe elliptique, mais c’est un cas séparé facile qui peut être vérifié). Il y a quelques cas particuliers qui expliquent la nécessité du point à l’infini.

Dans certains cas (par exemple, si P1 et P2 ont les mêmes valeurs x mais des valeurs y différentes), la ligne sera exactement verticale, auquel cas P3 = le point à l’infini.

Si P1 est le point à l’infini, alors P1 + P2 = P2. De même, si P2 est le point à l’infini, alors P1 + P2 = P1. Cela montre comment le point à l’infini joue le rôle que joue zéro dans l’arithmétique "normale".

Il s’avère que + est associatif, ce qui signifie que (A + B) + C = A + (B + C). Cela signifie que nous pouvons écrire A + B + C (sans parenthèses) sans ambiguïté.

Maintenant que nous avons défini l’addition, nous pouvons définir la multiplication de la manière standard qui étend l’addition. Pour un point P sur la courbe elliptique, si k est un nombre entier, alors k * P = P + P + P + …​ + P (k fois). Notez que k est parfois (peut-être de manière déroutante) appelé un "exposant" dans ce cas.

Génération d’une clé publique

Commencer avec une clé privée sous la forme d’un nombre k généré aléatoirement, nous le multiplions par un point prédéterminé sur la courbe appelé point générateur G pour produire un autre point ailleurs sur la courbe, qui est la clé publique correspondante K :

K = k * G

Le point générateur est spécifié dans le cadre de la norme secp256k1 ; c’est la même chose pour toutes les implémentations de secp256k1, et toutes les clés dérivées de cette courbe utilisent le même point G. Comme le point générateur est toujours le même pour tous les utilisateurs d’Ethereum, une clé privée k multipliée par G donnera toujours la même clé publique K. La relation entre k et K est fixe, mais ne peut être calculée que dans un sens, de k vers K. C’est pourquoi une adresse Ethereum (dérivée de K) peut être partagée avec n’importe qui et ne révèle pas la clé privée de l’utilisateur (k).

Comme nous l’avons décrit dans la section précédente, la multiplication de k * G est équivalente à une addition répétée, donc G + G + G + …​ + G, répété k fois. En résumé, pour produire une clé publique K à partir d’une clé privée k, on ajoute le point générateur G à lui-même, k fois.

Tip

Une clé privée peut être convertie en clé publique, mais une clé publique ne peut pas être reconvertie en clé privée, car le calcul ne fonctionne que dans un sens.

Appliquons ce calcul pour trouver la clé publique de la clé privée spécifique que nous vous avons montrée dans Clés privées :

Exemple de calcul de clé privée à clé publique
K = f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315 * G

Une bibliothèque cryptographique peut nous aider à calculer K, en utilisant la multiplication de courbes elliptiques. La clé publique résultante K est définie comme le point :

K = (x, y)

où :

x = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b
y = 83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0

Dans Ethereum, vous pouvez voir des clés publiques représentées par une sérialisation de 130 caractères hexadécimaux (65 octets ). Ceci est adopté à partir d’un format de sérialisation standard proposé par le consortium industriel Standards for Efficient Cryptography Group (SECG), documenté dans http://www.secg.org/sec1-v2.pdf [Standards for Efficient Cryptography (SEC1)]. La norme définit quatre préfixes possibles pouvant être utilisés pour identifier des points sur une courbe elliptique, répertoriés dans Préfixes de clé publique EC sérialisés.

Table 2. Préfixes de clé publique EC sérialisés
Préfixe Signification Longueur (octets comptant le préfixe)

0x00

Pointe à l’infini

1

0x04

Point non compressé

65

0x02

Point compressé avec pair y

33

0x03

Point compressé avec impair y

33

Ethereum utilise uniquement des clés publiques non compressées ; par conséquent, le seul préfixe pertinent est (hex) 04. La sérialisation concatène les coordonnées x et y de la clé publique :

04 + coordonnée x (32 octets/64 hex) + coordonnée y (32 octets/64 hex)

Par conséquent, la clé publique que nous avons calculée précédemment est sérialisée comme suit :

046e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0 \
c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0

Bibliothèques de courbes elliptiques

Il existe quelques implémentations de la courbe elliptique secp256k1 qui sont utilisées dans les projets liés à la cryptomonnaie :

OpenSSL

La bibliothèque OpenSSL offre un ensemble complet de primitives cryptographiques, y compris une implémentation complète de secp256k1. Par exemple, pour dériver la clé publique, la fonction EC_POINT_mul peut être utilisée.

libsecp256k1

libsecp256k1 de Bitcoin Core est une implémentation en langage C de la courbe elliptique secp256k1 et d’autres primitives cryptographiques. Il a été écrit à partir de zéro pour remplacer OpenSSL dans le logiciel Bitcoin Core et est considéré comme supérieur en termes de performances et de sécurité.

Fonctions de hachage cryptographique

Les fonctions de hachage cryptographiques sont utilisées dans Ethereum. En fait, les fonctions de hachage sont largement utilisées dans presque tous les systèmes cryptographiques - un fait capturé par cryptographeur Bruce Schneier, qui a dit , "Bien plus que des algorithmes de chiffrement, les fonctions de hachage unidirectionnelles sont les bêtes de somme de la cryptographie moderne."

Dans cette section, nous discuterons des fonctions de hachage, explorerons leurs propriétés de base et verrons comment ces propriétés les rendent si utiles dans de nombreux domaines de la cryptographie moderne. Nous abordons ici les fonctions de hachage car elles font partie de la transformation des clés publiques Ethereum en adresses. Ils peuvent également être utilisés pour créer des empreintes digitales, qui facilitent la vérification des données.

En termes simples, une fonction de hachage est "toute fonction qui peut être utilisée pour mapper des données de taille arbitraire à des données de taille fixe. ” L’entrée d’une fonction de hachage est appelée une pré-image, le message ou simplement les données d’entrée. La sortie est appelée hash ou résultat de hachage ou hachage en français. Les fonctions de hachage cryptographiques sont une sous-catégorie spéciale qui possède des propriétés spécifiques utiles pour sécuriser les plates-formes, telles qu’Ethereum.

Une fonction de hachage cryptographique est une fonction de hachage unidirectionnelle qui associe des données de taille arbitraire à une chaîne de bits de taille fixe. La nature "unidirectionnelle" signifie qu’il est impossible de recréer les données d’entrée si l’on ne connaît que le hachage de sortie. La seule façon de déterminer une entrée possible est de mener une recherche par force brute, en vérifiant chaque candidat pour une sortie correspondante ; étant donné que l’espace de recherche est virtuellement infini, il est aisé de comprendre l’impossibilité pratique de la tâche. Même si vous trouvez des données d’entrée qui créent un hachage correspondant, il se peut qu’il ne s’agisse pas des données d’entrée d’origine : les fonctions de hachage sont des fonctions "plusieurs vers un". Trouver deux ensembles de données d’entrée qui hachent la même sortie s’appelle trouver une collision de hachage. En gros, plus la fonction de hachage est bonne, plus les collisions de hachage sont rares. Pour Ethereum, ils sont effectivement impossibles.

Regardons de plus près les principales propriétés des fonctions de hachage cryptographiques. Ceux-ci inclus:

Déterministe

Un message d’entrée donné produit toujours la même sortie de hachage.

Vérifiable

Le calcul du hachage d’un message est efficace (complexité linéaire).

Non-corrélationnaire

Une petite modification du message (par exemple, un changement de 1 bit) devrait modifier la sortie de hachage si largement qu’elle ne peut pas être corrélée au hachage du message d’origine.

Irréversibile

Le calcul du message à partir de son hachage est irréalisable, ce qui équivaut à une recherche par force brute dans tous les messages possibles.

Protection contre les collisions

Il devrait être impossible de calculer deux messages différents qui produisent la même sortie de hachage.

La résistance aux collisions de hachage est particulièrement importante pour éviter la falsification de signature numérique dans Ethereum.

La combinaison de ces propriétés rend les fonctions de hachage cryptographiques utiles pour un large éventail d’applications de sécurité, notamment :

  • Empreinte des données

  • Intégrité des messages (détection d’erreur)

  • Preuve de travail

  • Authentification (hachage de mot de passe et étirement de clé)

  • Générateurs de nombres pseudo-aléatoires

  • Engagement de message (mécanismes de validation-révélation)

  • Identifiants uniques

Nous en trouverons beaucoup dans Ethereum au fur et à mesure que nous progressons dans les différentes couches du système.

Fonction de hachage cryptographique d’Ethereum : Keccak-256

Ethereum utilise le hachage cryptographique Keccak-256 dans de nombreux endroits. Keccak-256 a été conçu comme candidat pour le SHA-3 Cryptographic Hash Function Competition organisé en 2007 par le National Institute of Science and Technology (NIST). Keccak était l’algorithme gagnant, qui a été normalisé en tant que Federal Information Processing Standard (FIPS) 202 en 2015.

Cependant, pendant la période de développement d’Ethereum, la normalisation du NIST n’était pas encore finalisée. Le NIST a ajusté certains des paramètres de Keccak après l’achèvement du processus de normalisation, prétendument pour améliorer son efficacité. Cela se produisait en même temps que le dénonciateur héroïque Edward Snowden a révélé des documents qui impliquent que le NIST pourrait avoir été indûment influencé par la National Security Agency (NSA) pour affaiblir intentionnellement la norme de générateur de nombres aléatoires Dual_EC_DRBG, plaçant effectivement une porte dérobée dans le générateur de nombres aléatoires standard. Le résultat de cette controverse a été une réaction violente contre les modifications proposées et un retard important dans la normalisation de SHA-3. À l’époque, la Fondation Ethereum a décidé d’implémenter l’algorithme Keccak original, tel que proposé par ses inventeurs, plutôt que la norme SHA-3 telle que modifiée par le NIST.

Warning

Bien que vous puissiez voir "SHA-3" mentionné dans les documents et le code Ethereum, beaucoup, sinon toutes, de ces instances font en fait référence à Keccak-256, et non à la norme FIPS-202 SHA-3 finalisée. Les différences d’implémentation sont légères, liées aux paramètres de remplissage, mais elles sont importantes dans la mesure où Keccak-256 produit des sorties de hachage différentes de FIPS-202 SHA-3 pour la même entrée.

Quelle fonction de hachage suis-je en train d’utiliser ?

Comment pouvez-vous savoir si la bibliothèque de logiciels que vous utilisez implémente FIPS-202 SHA-3 ou Keccak-256, si les deux pouvaient s’appeler "SHA-3" ?

Un moyen simple de le dire est d’utiliser un vecteur de test, une sortie attendue pour une entrée donnée. Le test le plus couramment utilisé pour une fonction de hachage est l' entrée vide. Si vous exécutez la fonction de hachage avec une chaîne vide en entrée, vous devriez voir les résultats suivants :

Keccak256("") =
  c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470

SHA3("") =
  a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a

Quel que soit le nom de la fonction, vous pouvez la tester pour voir s’il s’agit du Keccak-256 d’origine ou de la norme NIST finale FIPS-202 SHA-3 en exécutant ce test simple. N’oubliez pas qu’Ethereum utilise Keccak-256, même s’il est souvent appelé SHA-3 dans le code.

Note

En raison de la confusion créée par la différence entre la fonction de hachage utilisée dans Ethereum (Keccak-256) et la norme finalisée (FIP-202 SHA-3), des efforts sont en cours pour renommer toutes les instances de sha3 dans tous les codes, opcodes, et des bibliothèques à keccak256. Voir ERC59 pour plus de détails.

Examinons ensuite la première application de Keccak-256 dans Ethereum, qui consiste à produire des adresses Ethereum à partir de clés publiques.

Adresses Ethereum

Les adresses Ethereum sont des identifiants uniques dérivés de clés publiques ou de contrats utilisant la fonction de hachage unidirectionnelle Keccak-256.

Dans nos exemples précédents, nous avons commencé avec une clé privée et avons utilisé la multiplication par courbe elliptique pour dériver une clé publique :

Clé privée k :

k = f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315

Clé publique K (coordonnées x et y concaténées et affichées en hexadécimal) :

K = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e...
Note

Il est à noter que la clé publique n’est pas formatée avec le préfixe (hex) 04 lors du calcul de l’adresse.

Nous utilisons Keccak-256 pour calculer le hachage de cette clé publique :

Keccak256(K) = 2a5bc342ed616b5ba5732269001d3f1ef827552ae1114027bd3ecf1f086ba0f9

Ensuite, nous ne gardons que les 20 derniers octets (octets les moins significatifs), qui sont notre adresse Ethereum :

001d3f1ef827552ae1114027bd3ecf1f086ba0f9

Le plus souvent, vous verrez des adresses Ethereum avec le préfixe 0x qui indique qu’elles sont codées en hexadécimal, comme ceci :

0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9

Formats d’adresse Ethereum

Les adresses Ethereum sont des nombres hexadécimaux, des identifiants dérivés des 20 derniers octets du hachage Keccak-256 de la clé publique.

("somme de contrôle","dans les formats d’adresses Ethereum"Contrairement aux adresses Bitcoin, qui sont encodées dans l’interface utilisateur de tous les clients pour inclure une somme de contrôle intégrée pour se protéger contre les adresses mal saisies, les adresses Ethereum sont présentées sous forme hexadécimale brute sans aucune somme de contrôle.

La justification de cette décision était que les adresses Ethereum seraient éventuellement cachées derrière des abstractions (telles que les services de noms) aux couches supérieures du système et que des sommes de contrôle devraient être ajoutées aux couches supérieures si nécessaire.

En réalité, ces couches supérieures ont été développées trop lentement et ce choix de conception a entraîné un certain nombre de problèmes dans les premiers jours de l’écosystème, notamment la perte de fonds en raison d’adresses mal saisies et d’erreurs de validation des entrées. De plus, comme les services de noms Ethereum ont été développés plus lentement que prévu initialement, les encodages alternatifs ont été adoptés très lentement par les développeurs de portefeuilles. Nous examinerons ensuite quelques-unes des options d’encodage.

Inter Exchange Client Address Protocol (ICAP)

Le client Inter exchange Address Protocol (ICAP) est un codage d’adresse Ethereum partiellement compatible avec le International Bank Account Number (IBAN), offrant un encodage polyvalent, à somme de contrôle et interopérable pour les adresses Ethereum. Les adresses ICAP peuvent encoder des adresses Ethereum ou des noms communs enregistrés auprès d’un registre de noms Ethereum. Vous pouvez en savoir plus sur ICAP sur Ethereum Wiki.

L’IBAN est une norme internationale d’identification des numéros de compte bancaire, principalement utilisée pour les virements électroniques. Il est largement adopté dans l’espace européen unique de paiements en euros (SEPA) et au-delà. L’IBAN est un service centralisé et fortement réglementé. ICAP est une implémentation décentralisée mais compatible pour les adresses Ethereum.

Un IBAN consiste en une chaîne de 34 caractères alphanumériques au maximum (insensible à la casse) comprenant un code pays, une somme de contrôle et un identifiant de compte bancaire (qui est spécifique au pays).

ICAP utilise la même structure en introduisant un code de pays non standard, "XE", qui signifie "Ethereum", suivi d’une somme de contrôle à deux caractères et de trois variantes possibles d’un identifiant de compte :

Direct

Un entier gros-boutiste base-36 comprenant jusqu’à 30 caractères alphanumériques, représentant les 155 bits les moins significatifs d’une adresse Ethereum. Étant donné que cet encodage correspond à moins que les 160 bits complets d’une adresse Ethereum générale, il ne fonctionne que pour les adresses Ethereum qui commencent par un ou plusieurs octets à zéro. L’avantage est qu’il est compatible avec l’IBAN, en termes de longueur de champ et de somme de contrôle. Exemple : XE60HAMICDXSV5QXVJA7TJW47Q9CHWKJD (33 caractères).

Basic

Identique à l’encodage Direct, sauf qu’il comporte 31 caractères. Cela lui permet d’encoder n’importe quelle adresse Ethereum, mais la rend incompatible avec la validation du champ IBAN. Exemple : XE18CHDJBPLTBCJ03FE9O2NS0BPOJVQCU2P (35 caractères).

Indirect

Encode un identifiant qui se résout en une adresse Ethereum via un fournisseur de registre de noms. Il utilise 16 caractères alphanumériques, comprenant un identifiant d’actif (par exemple, ETH), un service de nom (par exemple, XREG) et un nom lisible par l’homme à 9 caractères (par exemple, KITTYCATS). Exemple : XE##ETHXREGKITTYCATS (longueur de 20 caractères), où le ## doit être remplacé par les deux caractères de la somme de contrôle calculée.

Nous pouvons utiliser l’outil de ligne de commande helpeth pour créer des adresses ICAP. Vous pouvez obtenir de l’aide en l’installant avec :

$ npm install -g helpeth

Si vous n’avez pas npm, vous devrez peut-être d’abord installer nodeJS, ce que vous pouvez faire en suivant les instructions sur https://nodeJS.org.

Maintenant que nous avons helpeth, essayons de créer une adresse ICAP avec notre exemple de clé privée (préfixée par 0x et passée en paramètre à helpeth).

$ helpeth keyDetails \
  -p 0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315

Address: 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9
ICAP: XE60 HAMI CDXS V5QX VJA7 TJW4 7Q9C HWKJ D
Public key: 0x6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b...

La commande helpeth construit une adresse Ethereum hexadécimale ainsi qu’une adresse ICAP pour nous. L’adresse ICAP de notre exemple de clé est :

XE60HAMICDXSV5QXVJA7TJW47Q9CHWKJD

Étant donné que notre exemple d’adresse Ethereum commence par un octet zéro, il peut être encodé à l’aide de la méthode d’encodage Direct ICAP qui est valide au format IBAN. Vous pouvez le dire car il comporte 33 caractères.

Si notre adresse ne commençait pas par un zéro, elle serait encodée avec l’encodage Basic, qui comporterait 35 caractères et serait invalide en tant qu’IBAN.

Tip

Les chances qu’une adresse Ethereum commence par un octet zéro sont de 1 sur 256. Pour en générer une comme celle-là, il faudra en moyenne 256 tentatives avec 256 clés privées aléatoires différentes avant d’en trouver une qui fonctionne comme un type "Direct" compatible IBAN encodé comme adresse ICAP.

À l’heure actuelle, ICAP n’est malheureusement pris en charge que par quelques portefeuilles.

Encodage hexadécimal avec somme de contrôle en majuscules (EIP-55)

En raison de la lenteur du déploiement d’ICAP et des services de noms, une norme a été proposée par la Proposition d’amélioration d’Ethereum 55 (EIP-55). EIP-55 offre une somme de contrôle rétrocompatible pour les adresses Ethereum en modifiant la capitalisation de l’adresse hexadécimale. L’idée est que les adresses Ethereum sont insensibles à la casse et que tous les portefeuilles sont censés accepter les adresses Ethereum exprimées en majuscules ou en minuscules, sans aucune différence d’interprétation.

En modifiant la capitalisation des caractères alphabétiques dans l’adresse, nous pouvons transmettre une somme de contrôle qui peut être utilisée pour protéger l’intégrité de l’adresse contre les erreurs de frappe ou de lecture. Les portefeuilles qui ne prennent pas en charge les sommes de contrôle EIP-55 ignorent simplement le fait que l’adresse contient des majuscules mixtes, mais ceux qui la prennent en charge peuvent la valider et détecter les erreurs avec une précision de 99,986 %.

L’encodage en majuscules mixtes est subtil et vous ne le remarquerez peut-être pas au début. Notre exemple d’adresse est :

0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9

Avec une somme de contrôle à capitalisation mixte EIP-55, cela devient :

0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9

Pouvez-vous faire la différence ? Certains des caractères alphabétiques (A à F) de l’alphabet de codage hexadécimal sont désormais en majuscules, tandis que d’autres sont en minuscules.

EIP-55 est assez simple à mettre en œuvre. Nous prenons le hachage Keccak-256 de l’adresse hexadécimale minuscule. Ce hachage agit comme une empreinte numérique de l’adresse, nous donnant une somme de contrôle pratique. Tout petit changement dans l’entrée (l’adresse) devrait entraîner un grand changement dans le hachage résultant (la somme de contrôle), nous permettant de détecter efficacement les erreurs. Le hachage de notre adresse est ensuite encodé dans la capitalisation de l’adresse elle-même. Décomposons-le, étape par étape :

  1. Hachez l’adresse en minuscules, sans le préfixe 0x :

Keccak256("001d3f1ef827552ae1114027bd3ecf1f086ba0f9") =
23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1
  1. Mettez en majuscule chaque caractère d’adresse alphabétique si le chiffre hexadécimal correspondant du hachage est supérieur ou égal à 0x8. C’est plus facile à montrer si nous alignons l’adresse et le hachage :

Adresse : 001d3f1ef827552ae1114027bd3ecf1f086ba0f9
Hachage : 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9...

Notre adresse contient un caractère alphabétique d en quatrième position. Le quatrième caractère du hachage est 6, qui est inférieur à 8. Donc, nous laissons le d minuscule. Le prochain caractère alphabétique de notre adresse est f, en sixième position. Le sixième caractère du hachage hexadécimal est c, qui est supérieur à 8. Par conséquent, nous capitalisons le F dans l’adresse, et ainsi de suite. Comme vous pouvez le voir, nous n’utilisons que les 20 premiers octets (40 caractères hexadécimaux) du hachage comme somme de contrôle, car nous n’avons que 20 octets (40 caractères hexadécimaux) dans l’adresse à capitaliser de manière appropriée.

Vérifiez vous-même l’adresse en majuscules mixtes résultante et voyez si vous pouvez dire quels caractères ont été en majuscules et à quels caractères ils correspondent dans le hachage de l’adresse :

Adresse : 001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
Hachage : 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9...
Détection d’une erreur dans une adresse encodée EIP-55

Maintenant, regardons comment les adresses EIP-55 nous aideront à trouver une erreur. Supposons que nous ayons imprimé une adresse Ethereum, qui est codée EIP-55 :

0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9

Faisons maintenant une erreur fondamentale en lisant cette adresse. Le caractère avant le dernier est un F majuscule. Pour cet exemple, supposons que nous interprétions cela comme un E majuscule et que nous tapions l’adresse suivante (incorrecte) dans notre portefeuille :

0x001d3F1ef827552Ae1114027BD3ECF1f086bA0E9

Heureusement, notre portefeuille est conforme à la norme EIP-55 ! Il remarque les majuscules mixtes et tente de valider l’adresse. Il le convertit en minuscules et calcule le hachage de la somme de contrôle :

Keccak256("001d3f1ef827552ae1114027bd3ecf1f086ba0e9") =
5429b5d9460122fb4b11af9cb88b7bb76d8928862e0a57d46dd18dd8e08a6927

Comme vous pouvez le voir, même si l’adresse n’a changé que d’un caractère (en fait, un seul bit, car e et f sont séparés d’un bit), le hachage de l’adresse a radicalement changé. C’est la propriété des fonctions de hachage qui les rend si utiles pour les sommes de contrôle !

Maintenant, alignons les deux et vérifions la capitalisation :

001d3F1ef827552Ae1114027BD3ECF1f086bA0E9
5429b5d9460122fb4b11af9cb88b7bb76d892886...

C’est tout faux ! Plusieurs des caractères alphabétiques sont mal capitalisés. N’oubliez pas que la capitalisation est l’encodage de la somme de contrôle correcte.

La casse de l’adresse que nous avons saisie ne correspond pas à la somme de contrôle que nous venons de calculer, ce qui signifie que quelque chose a changé dans l’adresse et qu’une erreur a été introduite.

Conclusion

Dans ce chapitre, nous avons fourni un bref aperçu de la cryptographie à clé publique et nous nous sommes concentrés sur l’utilisation des clés publiques et privées dans Ethereum et l’utilisation d’outils cryptographiques, tels que les fonctions de hachage, dans la création et la vérification des adresses Ethereum. Nous avons également examiné les signatures numériques et comment elles peuvent démontrer la propriété d’une clé privée sans révéler cette clé privée. Dans Portefeuilles, nous allons rassembler ces idées et voir comment les portefeuilles peuvent être utilisés pour gérer les collections de clés.

Portefeuilles

Bannière Amazon du livre Maîtriser Ethereum

Le mot "portefeuille" est utilisé pour décrire quelques choses différentes dans Ethereum.

À un niveau élevé, un portefeuille est une application logicielle qui sert d’interface utilisateur principale à Ethereum. Le portefeuille contrôle l’accès à l’argent d’un utilisateur, gère les clés et les adresses, suit le solde et crée et signe des transactions. De plus, certains portefeuilles Ethereum peuvent également interagir avec des contrats, tels que les jetons ERC20.

Plus étroitement, du point de vue d’un programmeur, le mot wallet ou portefeuille fait référence au système utilisé pour stocker et gérer les clés d’un utilisateur. Chaque portefeuille a un composant de gestion des clés. Pour certains portefeuilles, c’est tout ce qu’il y a. D’autres portefeuilles font partie d’une catégorie beaucoup plus large, celle des navigateurs, qui sont des interfaces vers des applications décentralisées basées sur Ethereum, ou DApps, que nous examinerons plus en détail dans Applications décentralisées (DApps). Il n’y a pas de lignes de distinction claires entre les différentes catégories qui sont regroupées sous le terme portefeuille.

Dans ce chapitre, nous examinerons les portefeuilles en tant que conteneurs de clés privées et en tant que systèmes de gestion de ces clés.

Présentation de la technologie de portefeuille

Dans cette section, nous résumons les différentes technologies utilisées pour construire des portefeuilles Ethereum conviviaux, sécurisés et flexibles.

L’une des principales considérations dans la conception des portefeuilles est l’équilibre entre commodité et confidentialité. Le portefeuille Ethereum le plus pratique est celui avec une clé privée unique et une adresse que vous réutilisez pour tout. Malheureusement, une telle solution est un cauchemar pour la confidentialité, car n’importe qui peut facilement suivre et corréler toutes vos transactions. L’utilisation d’une nouvelle clé pour chaque transaction est préférable pour la confidentialité, mais devient très difficile à gérer. Le bon équilibre est difficile à atteindre, mais c’est pourquoi une bonne conception de portefeuille est primordiale.

Une idée fausse courante à propos d’Ethereum est que les portefeuilles Ethereum contiennent de l’ether ou des jetons. En fait, très strictement parlant, le portefeuille ne contient que des clés. L’ether ou d’autres jetons sont enregistrés sur la chaîne de blocs Ethereum. Les utilisateurs contrôlent les jetons sur le réseau en signant des transactions avec les clés dans leurs portefeuilles. Dans un sens, un portefeuille Ethereum est un porte-clés. Cela dit, étant donné que les clés détenues par le portefeuille sont les seules choses nécessaires pour transférer de l’ether ou des jetons à d’autres, en pratique, cette distinction est assez peu pertinente. Là où la différence est importante, c’est dans le fait de changer d’état d’esprit et de ne plus avoir affaire au système bancaire conventionnel centralisé (où seuls vous et la banque pouvez voir l’argent sur votre compte, et vous n’avez qu’à convaincre la banque que vous voulez transférer des fonds pour faire une transaction) vers un système décentralisé des plates-formes chaîne de blocs (où tout le monde peut voir le solde d’un compte, bien qu’il ne connaisse probablement pas le propriétaire du compte, et tout le monde doit être convaincu que le propriétaire veut déplacer des fonds pour une transaction édictée). En pratique, cela signifie qu’il existe un moyen indépendant de vérifier le solde d’un compte, sans avoir besoin de son portefeuille. De plus, vous pouvez déplacer la gestion de votre compte de votre portefeuille actuel vers un autre portefeuille, si vous n’aimez plus l’application de portefeuille que vous avez commencé à utiliser.

Note

Les portefeuilles Ethereum contiennent des clés, pas d’ether ni de jetons. Les portefeuilles sont comme des porte-clés contenant des paires de clés privées et publiques. Les utilisateurs signent des transactions avec les clés privées, prouvant ainsi qu’ils possèdent l’ether. L’ether est stocké sur la chaîne de blocs.

Il existe deux principaux types de portefeuilles, qui se distinguent par le fait que les clés qu’ils contiennent sont liées ou non.

Le premier type est un portefeuille non déterministe, où chaque clé est générée indépendamment à partir d’un nombre aléatoire différent . Les clés ne sont pas liées les unes aux autres. Ce type de portefeuille est également connu sous le nom de portefeuille JBOK, de l’expression "Just a Bunch Of Keys" ou "Juste un tas de clés".

Le deuxième type de portefeuille est un portefeuille déterministe, où toutes les clés sont dérivées d’une seule clé maîtresse, connue sous le nom de seed ou graine ou valeur d’amorçage. Toutes les clés de ce type de portefeuille sont liées les unes aux autres et peuvent être générées à nouveau si l’on dispose de la valeur d’amorçage d’origine. Il existe un certain nombre de méthodes différentes de dérivation de clé utilisées dans les portefeuilles déterministes. La méthode de dérivation la plus couramment utilisée utilise une structure arborescente, comme décrit dans Portefeuilles déterministes hiérarchiques (BIP-32/BIP-44).

Pour rendre les portefeuilles déterministes légèrement plus sûrs contre les accidents de perte de données, comme le vol de votre téléphone ou sa chute dans les toilettes, les valeurs d’amorçages sont souvent encodées sous la forme d’une liste de mots (en anglais ou dans une autre langue) à noter et à utiliser en cas d’accident. Ceux-ci sont connus sous le nom de mots de code mnémoniques du portefeuille. Bien sûr, si quelqu’un met la main sur vos mots de code mnémoniques, il peut également recréer votre portefeuille et ainsi accéder à vos contrats intelligents et votre ether. En tant que tel, soyez très, très prudent avec votre liste de mots de récupération ! Ne les stockez jamais électroniquement, dans un fichier, sur votre ordinateur ou votre téléphone. Écrivez-le sur papier et rangez-le dans un endroit sûr et sécurisé.

Les prochaines sections présentent chacune de ces technologies à un niveau élevé.

Portefeuilles non déterministes (aléatoires)

Dans le premier portefeuille Ethereum (produit pour la prévente Ethereum), chaque fichier de portefeuille stockait une seule clé privée générée aléatoirement. Ces portefeuilles sont remplacés par des portefeuilles déterministes car ces portefeuilles "à l’ancienne" sont à bien des égards inférieurs. Par exemple, il est considéré comme une bonne pratique d’éviter la réutilisation de l’adresse Ethereum dans le cadre de la maximisation de votre vie privée lors de l’utilisation d’Ethereum, c’est-à-dire d’utiliser une nouvelle adresse (qui nécessite une nouvelle clé privée) chaque fois que vous recevez des fonds. Vous pouvez aller plus loin et utiliser une nouvelle adresse pour chaque transaction, bien que cela puisse coûter cher si vous traitez beaucoup de jetons. Pour suivre cette pratique, un portefeuille non déterministe devra régulièrement augmenter sa liste de clés, ce qui signifie que vous devrez effectuer des sauvegardes régulières. Si jamais vous perdez vos données (panne de disque, accident d’abus de boisson, vol de téléphone) avant d’avoir réussi à sauvegarder votre portefeuille, vous perdrez l’accès à vos fonds et à vos contrats intelligents. Les portefeuilles non déterministes de "type 0" sont les plus difficiles à gérer, car ils créent un nouveau fichier de portefeuille pour chaque nouvelle adresse de manière "juste à temps".

Néanmoins, de nombreux clients Ethereum (y compris geth)utilisent un fichier keystore ou magasin de clés, qui est un fichier codé JSON contenant une clé privée unique (générée de manière aléatoire), chiffrée par une phrase secrète pour plus de sécurité . Le contenu du fichier JSON ressemble à ceci :

{
    "address": "001d3f1ef827552ae1114027bd3ecf1f086ba0f9",
    "crypto": {
        "cipher": "aes-128-ctr",
        "ciphertext":
            "233a9f4d236ed0c13394b504b6da5df02587c8bf1ad8946f6f2b58f055507ece",
        "cipherparams": {
            "iv": "d10c6ec5bae81b6cb9144de81037fa15"
        },
        "kdf": "scrypt",
        "kdfparams": {
            "dklen": 32,
            "n": 262144,
            "p": 1,
            "r": 8,
            "salt":
                "99d37a47c7c9429c66976f643f386a61b78b97f3246adca89abe4245d2788407"
        },
        "mac": "594c8df1c8ee0ded8255a50caf07e8c12061fd859f4b7c76ab704b17c957e842"
    },
    "id": "4fcb2ba4-ccdb-424f-89d5-26cce304bf9c",
    "version": 3
}

Le format de magasin de clés utilise une fonction de dérivation de clé (KDF ou key derivation function), également connue sous le nom d’algorithme d’étirement de mot de passe, qui protège contre les attaques par force, par dictionnaire et par table arc-en-ciel. En termes simples, la clé privée n’est pas chiffrée directement par la phrase secrète. Au lieu de cela, la phrase secrète est étirée, en la hachant à plusieurs reprises. La fonction de hachage est répétée pendant 262 144 tours, ce qui peut être vu dans le magasin de clés JSON sous la forme du paramètre crypto.kdfparams.n. Un attaquant essayant de forcer brutalement la phrase secrète devrait appliquer 262 144 cycles de hachage pour chaque phrase secrète tentée, ce qui ralentit suffisamment l’attaque pour la rendre impossible pour les phrases secrètes d’une complexité et d’une longueur suffisantes.

Il existe un certain nombre de bibliothèques logicielles qui peuvent lire et écrire le format keystore, comme la bibliothèque JavaScript keythereum.

Tip

L’utilisation de portefeuilles non déterministes est déconseillée pour autre chose que de simples tests. Ils sont trop encombrants pour être sauvegardés et utilisés pour autre chose que les situations les plus élémentaires. Utilisez plutôt un portefeuille HD standard avec une valeur d’amorçage mnémonique pour la sauvegarde.

Portefeuilles déterministes (par valeur d’amorçage)

Les portefeuilles déterministes ou "ensemencés par valeurs d’amorçages" sont des portefeuilles qui contiennent des clés privées qui sont toutes dérivées d’une seule clé maîtresse ou valeur d’amorçage. La valeur d’amorçage est un nombre généré aléatoirement qui est combiné avec d’autres données, telles qu’un numéro d’index ou un "code de chaîne" (voir Clés publiques et privées étendues), pour dériver n’importe quel nombre de clés privées. Dans un portefeuille déterministe, la valeur d’amorçage est suffisante pour récupérer toutes les clés dérivées, et donc une seule sauvegarde, au moment de la création, est suffisante pour sécuriser tous les fonds et contrats intelligents dans le portefeuille. La valeur d’amorçage est également suffisante pour une exportation ou une importation de portefeuille, permettant une migration facile de toutes les clés entre différentes implémentations de portefeuille.

Cette conception rend la sécurité de la valeur d’amorçage d’une plus haute importance, car seule la valeur d’amorçage est nécessaire pour accéder à l’ensemble du portefeuille. D’un autre côté, le fait de pouvoir concentrer les efforts de sécurité sur une seule donnée peut être considéré comme un avantage.

Portefeuilles déterministes hiérarchiques (BIP-32/BIP-44)

Les portefeuilles déterministes ont été développés pour faciliter la dérivation de nombreuses clés à partir d’une seule valeur d’amorçage. Actuellement, la forme la plus avancée de portefeuille déterministe est le portefeuille hiérarchique déterministe (HD ou Hierarchical Deterministic) défini par le standard BIP-32 de Bitcoin. Les portefeuilles HD contiennent des clés dérivées dans une structure arborescente, de sorte qu’une clé parent peut dériver une séquence de clés enfants, chacune pouvant dériver une séquence de clés petits-enfants, et ainsi de suite. Cette arborescence est illustrée dans Portefeuille HD : un arbre de clés généré à partir d’une seule valeur d’amorçage.

Portefeuille HD
Figure 27. Portefeuille HD : un arbre de clés généré à partir d’une seule valeur d’amorçage

Les portefeuilles HD offrent quelques avantages clés par rapport aux portefeuilles déterministes plus simples. Tout d’abord, la structure arborescente peut être utilisée pour exprimer une signification organisationnelle supplémentaire, par exemple lorsqu’une branche spécifique de sous-clés est utilisée pour recevoir des paiements entrants et qu’une branche différente est utilisée pour recevoir la monnaie des paiements sortants. Les branches de clés peuvent également être utilisées dans les paramètres de l’entreprise, en attribuant différentes branches à des départements, des filiales, des fonctions spécifiques ou des catégories comptables.

Le deuxième avantage des portefeuilles HD est que les utilisateurs peuvent créer une séquence de clés publiques sans avoir accès aux clés privées correspondantes. Cela permet aux portefeuilles HD d’être utilisés sur un serveur non sécurisé ou dans une capacité de surveillance ou de réception uniquement, où le portefeuille n’a pas les clés privées qui peuvent dépenser les fonds.

Valeurs d’amorçage et codes mnémoniques (BIP-39)

Il y a beaucoup de manières d’encoder une clé privée pour une sauvegarde et une récupération sécurisées. La méthode actuellement préférée consiste à utiliser une séquence de mots qui, lorsqu’ils sont pris ensemble dans le bon ordre, peuvent recréer de manière unique la clé privée. Ceci est parfois connu sous le nom de mnémonique, et l’approche a été normalisée par BIP-39. Aujourd’hui, de nombreux portefeuilles Ethereum (ainsi que des portefeuilles pour d’autres crypto-monnaies) utilisent cette norme et peuvent importer et exporter des valeurs d’amorçage pour la sauvegarde et la récupération à l’aide de mnémoniques interopérables.

Pour comprendre pourquoi cette approche est devenue populaire, examinons un exemple :

Une valeur d’amorçage pour un portefeuille déterministe, en hexadécimal
FCCF1AB3329FD5DA3DA9577511F8F137
Une valeur d’amorçage pour un portefeuille déterministe, à partir d’un mnémonique de 12 mots
wolf juice proud gown wool unfair
wall cliff insect more detail hub

En termes pratiques, le risque d’erreur lors de l’écriture de la séquence hexadécimale est inacceptablement élevé. En revanche, la liste des mots connus est assez facile à gérer, principalement parce qu’il y a une forte redondance dans l’écriture des mots (surtout des mots anglais). Si « inzect » avait été enregistré par accident, il pourrait être rapidement déterminé, lors de la récupération du portefeuille, que « inzect » n’est pas un mot anglais valide et que « insect » devrait être utilisé à la place. Nous parlons d’écrire une représentation de la valeur d’amorçage car c’est une bonne pratique lors de la gestion des portefeuilles HD : la valeur d’amorçage est nécessaire pour récupérer un portefeuille en cas de perte de données (que ce soit par accident ou par vol), il est donc très prudent de conserver une sauvegarde. Cependant, la valeur d’amorçage doit rester extrêmement privée, les sauvegardes numériques doivent donc être soigneusement évitées ; d’où le conseil précédent de sauvegarder avec un stylo et du papier.

En résumé, l’utilisation d’une liste de mots de récupération pour coder la valeur d’amorçage d’un portefeuille HD constitue le moyen le plus simple d’exporter, de transcrire, d’enregistrer sur papier en toute sécurité, de lire sans erreur et d’importer un ensemble de clés privées dans un autre portefeuille.

Meilleures pratiques de portefeuille

Alors que la technologie des portefeuilles de cryptomonnaie a mûri, certaines normes industrielles communes ont émergé qui rendent les portefeuilles largement interopérables, faciles à utiliser, sécurisé et flexible. Ces normes permettent également aux portefeuilles de dériver des clés pour plusieurs cryptomonnaies différentes, toutes à partir d’un seul mnémonique. Ces normes communes sont :

  • Mots de code mnémonique, basés sur BIP-39

  • Portefeuilles HD, basés sur BIP-32

  • Structure de portefeuille HD polyvalente, basée sur BIP-43

  • Portefeuilles multidevises et multicomptes, basés sur BIP-44

Ces normes peuvent changer ou être obsolètes par les développements futurs, mais pour l’instant, elles forment un ensemble de technologies imbriquées qui sont devenues la norme de facto de portefeuille pour la plupart des plateformes de chaîne de blocs et leurs cryptomonnaies.

Les normes ont été adoptées par une large gamme de portefeuilles logiciels et matériels, rendant tous ces portefeuilles interopérables. Un utilisateur peut exporter un mnémonique généré dans l’un de ces portefeuilles et l’importer dans un autre portefeuille, en récupérant toutes les clés et adresses.

Quelques exemples de portefeuilles logiciels prenant en charge ces normes incluent (classés par ordre alphabétique) Jaxx, MetaMask, MyCrypto et MyEtherWallet (MEW). Les exemples de portefeuilles matériels prenant en charge ces normes incluent Keepkey, Ledger et Trezor.

Les sections suivantes examinent chacune de ces technologies en détail.

Tip

Si vous implémentez un portefeuille Ethereum, il doit être construit comme un portefeuille HD, avec une valeur d’amorçage encodée sous forme de code mnémonique pour la sauvegarde, conformément aux normes BIP-32, BIP-39, BIP-43 et BIP-44, comme décrit dans les rubriques suivantes.

Mots de code mnémonique (BIP-39)

Les mots de code mnémoniques sont des séquences de mots qui encodent un nombre aléatoire utilisé comme valeur d’amorçage pour dériver un portefeuille déterministe. La séquence de mots est suffisante pour recréer la valeur d’amorçage, et à partir de là recréer le portefeuille et toutes les clés dérivées. Une application de portefeuille qui implémente des portefeuilles déterministes avec des mots mnémoniques montrera à l’utilisateur une séquence de 12 à 24 mots lors de la première création d’un portefeuille. Cette séquence de mots est la sauvegarde du portefeuille et peut être utilisée pour récupérer et recréer toutes les clés dans la même application de portefeuille ou dans n’importe quelle application de portefeuille compatible. Comme nous l’avons expliqué précédemment, les listes de mots mnémoniques facilitent la sauvegarde des portefeuilles par les utilisateurs, car elles sont faciles à lire et correctement transcrire.

Note

Les mots mnémoniques sont souvent confondus avec les "brainwallets". Ils ne sont pas les mêmes. La principale différence est qu’un brainwallet se compose de mots choisis par l’utilisateur, tandis que les mots mnémoniques sont créés de manière aléatoire par le portefeuille et présentés à l’utilisateur. Cette différence importante rend les mots mnémoniques beaucoup plus sûrs, car les humains sont de très mauvaises sources d’aléatoire. Peut-être plus important encore, l’utilisation du terme "brainwallet" suggère que les mots doivent être mémorisés, ce qui est une idée terrible et une recette pour ne pas avoir votre sauvegarde lorsque vous en avez besoin.

Les codes mnémoniques sont définis dans le BIP-39. Notez que BIP-39 est une implémentation d’une norme de code mnémonique. Il existe une norme différente, avec un ensemble de mots différent, utilisée par le portefeuille Electrum Bitcoin et antérieure à BIP-39. BIP-39 a été proposé par la société à l’origine du portefeuille matériel Trezor et est incompatible avec la mise en œuvre d’Electrum. Cependant, BIP-39 a maintenant obtenu un large soutien de l’industrie à travers des dizaines d’implémentations interopérables et devrait être considéré comme la norme de facto industrielle. De plus, BIP-39 peut être utilisé pour produire des portefeuilles multidevises prenant en charge Ethereum, contrairement aux valeurs d’amorçage Electrum.

La BIP-39 définit la création d’un code mnémonique et d’une valeur d’amorçage, que nous décrivons ici en neuf étapes. Pour plus de clarté, le processus est divisé en deux parties : les étapes 1 à 6 sont présentées dans Génération de mots mnémoniques et les étapes 7 à 9 sont illustrées dans Du mnémonique à la valeur d’amorçage.

Génération de mots mnémoniques

Les mots mnémoniques sont générés automatiquement par le portefeuille en utilisant le processus standardisé défini dans BIP-39. Le portefeuille part d’une source d’entropie, ajoute une somme de contrôle, puis mappe l’entropie sur une liste de mots :

  1. Créer une séquence cryptographiquement aléatoire S de 128 à 256 bits.

  2. Créez une somme de contrôle de S en prenant la première longueur de S ÷ 32 bits du hachage SHA-256 de S.

  3. Ajoutez la somme de contrôle à la fin de la séquence aléatoire S.

  4. Divisez la concaténation de séquence et de somme de contrôle en sections de 11 bits.

  5. Associez chaque valeur 11 bits à un mot du dictionnaire prédéfini de 2 048 mots.

  6. Créez le code mnémonique à partir de la séquence de mots en respectant l’ordre.

Génération d’entropie et encodage sous forme de mots mnémoniques montre comment l’entropie est utilisée pour générer des mots mnémoniques.

Codes mnémoniques : entropie et longueur des mots montre la relation entre la taille des données d’entropie et la longueur des codes mnémoniques en mots.

Table 3. Codes mnémoniques : entropie et longueur des mots
Entropie (bits) Somme de contrôle (bits) Somme de contrôle d’entropie + (bits) Longueur mnémonique (mots)

128

4

132

12

160

5

165

15

192

6

198

18

224

7

231

21

256

8

264

24

Génération d’entropie et encodage sous forme de mots mnémoniques
Figure 28. Génération d’entropie et encodage sous forme de mots mnémoniques
Du mnémonique à la valeur d’amorçage

Les mots mnémoniques représentent l’entropie d’une longueur de 128 à 256 bits . L’entropie est ensuite utilisée pour dériver une valeur d’amorçage plus longue (512 bits) grâce à l’utilisation de la fonction d’étirement de clé PBKDF2. La valeur d’amorçage produite est utilisée pour construire un portefeuille déterministe et en dériver ses clés.

La fonction d’étirement de clé prend deux paramètres : le mnémonique et un sel. Le but d’un sel dans une fonction d’étirement de clé est de rendre difficile la construction d’une table de recherche permettant une attaque par force brute. Dans la norme BIP-39, le sel a un autre objectif : il permet l’introduction d’une phrase secrète qui sert de facteur de sécurité supplémentaire protégeant la valeur d’amorçage, comme nous le décrirons plus en détail dans Phrase secrète facultative dans BIP-39.

Le processus décrit aux étapes 7 à 9 continue à partir du processus décrit dans la section précédente :

  1. Le premier paramètre de la fonction d’étirement de clé PBKDF2 est le mnémonique produit à l’étape 6.

  2. Le deuxième paramètre de la fonction d’étirement de clé PBKDF2 est un sel. Le sel est composé de la constante de chaîne "mnémonique" concaténée avec une phrase secrète facultative fournie par l’utilisateur.

  3. PBKDF2 étend les paramètres mnémoniques et de sel en utilisant 2 048 cycles de hachage avec l’algorithme HMAC-SHA512, produisant une valeur de 512 bits comme sortie finale. Cette valeur de 512 bits est la valeur d’amorçage.

Du mnémonique à la valeur d’amorçage montre comment un mnémonique est utilisé pour générer une valeur d’amorçage.

Du mnémonique à la valeur d’amorçage
Figure 29. Du mnémonique à la valeur d’amorçage
Note

La fonction d’étirement de clé, avec ses 2 048 cycles de hachage, est une protection assez efficace contre les attaques par force brute contre le mnémonique ou la phrase secrète. Cela rend coûteux (en calcul) d’essayer plus de quelques milliers de combinaisons de mots de passe et de mnémoniques, alors que le nombre de valeurs d’amorçage dérivées possibles est vaste (2512, soit environ 10154) - bien plus grand que le nombre d’atomes dans l’univers visible (environ 1080).

Les tables #mnemonic_128_no_pass, #mnemonic_128_w_pass et #mnemonic_256_no_pass montrent quelques exemples de codes mnémoniques et les valeurs d’amorçage qu’ils produisent.

Table 4. Code mnémonique d’entropie 128 bits, sans phrase secrète, valeur d’amorçage résultante

Entrée d’entropie (128 bits)

0c1e24e5917779d297e14d45f14e1a1a

Mnémonique (12 mots)

army van defense carry jealous true garbage claim echo media make crunch

Phrase secrète

(rien)

Valeur d’amorçage (512 bits)

5b56c417303faa3fcba7e57400e120a0ca83ec5a4fc9ffba757fbe63fbd77a89a1a3be4c67196f57c39 a88b76373733891bfaba16ed27a813ceed498804c0570

Table 5. Code mnémonique d’entropie 128 bits, avec phrase secrète, valeur d’amorçage résultante

Entrée d’entropie (128 bits)

0c1e24e5917779d297e14d45f14e1a1a

Mnémonique (12 mots)

army van defense carry jealous true garbage claim echo media make crunch

Phrase secrète

SuperDuperSecret

Valeur d’amorçage (512 bits)

3b5df16df2157104cfdd22830162a5e170c0161653e3afe6c88defeefb0818c793dbb28ab3ab091897d0 715861dc8a18358f80b79d49acf64142ae57037d1d54

Table 6. Code mnémonique d’entropie 256 bits, sans phrase secrète, valeur d’amorçage résultante

Entrée d’entropie (256 bits)

2041546864449caff939d32d574753fe684d3c947c3346713dd8423e74abcf8c

Mnémonique (24 mots)

cake apple borrow silk endorse fitness top denial coil riot stay wolf luggage oxygen faint major edit measure invite love trap field dilemma oblige

Phrase secrète

(rien)

Valeur d’amorçage (512 bits)

3269bce2674acbd188d4f120072b13b088a0ecf87c6e4cae41657a0bb78f5315b33b3a04356e53d062e5 5f1e0deaa082df8d487381379df848a6ad7e98798404

Phrase secrète facultative dans BIP-39

La norme BIP-39 permet l’utilisation d’une phrase secrète facultative dans la dérivation de la valeur d’amorçage. Si aucune phrase secrète n’est utilisée, le mnémonique est étiré avec un sel constitué de la chaîne constante "mnémonique", produisant une valeur d’amorçage spécifique de 512 bits à partir de n’importe quel mnémonique donné. Si une phrase secrète est utilisée, la fonction d’étirement produit une valeur d’amorçage différente à partir de ce même mnémonique. En fait, étant donné un seul mnémonique, chaque phrase secrète possible conduit à une valeur d’amorçage différente. Essentiellement, il n’y a pas de "mauvaise" phrase secrète. Toutes les phrases secrètes sont valides et mènent toutes à des valeur d’amorçage différentes, formant un vaste ensemble de portefeuilles non initialisés possibles. L’ensemble des portefeuilles possibles est si grand (2512) qu’il n’y a aucune possibilité pratique de forcer brutalement ou de deviner accidentellement celui qui est utilisé, tant que la phrase secrète a une complexité et une longueur suffisantes.

Tip

Il n’y a pas de "mauvaises" phrases secrète dans BIP-39. Chaque phrase secrète mène à un portefeuille qui, à moins qu’il n’ait été utilisé auparavant, sera vide.

La phrase secrète facultative crée deux fonctionnalités importantes :

  • Un deuxième facteur (quelque chose de mémorisé) qui rend un mnémonique inutile par lui-même, protégeant les sauvegardes mnémoniques de la compromission par un voleur.

  • Une forme de déni plausible ou "portefeuille sous contrainte", où une phrase secrète choisie mène à un portefeuille avec une petite quantité de fonds , utilisé pour distraire un attaquant du "vrai" portefeuille qui contient la majorité des fonds.

Cependant, il est important de noter que l’utilisation d’une phrase secrète introduit également un risque de perte :

  • Si le propriétaire du portefeuille est incapacité ou décédé et que personne d’autre ne connaît la phrase secrète, la valeur d’amorçage est inutile et tous les fonds stockés dans le portefeuille sont perdus à jamais.

  • À l’inverse, si le propriétaire sauvegarde la phrase secrète au même endroit que la valeur d’amorçage, cela va à l’encontre de l’objectif d’un deuxième facteur.

Bien que les phrases secrètes soient très utiles, elles ne doivent être utilisées qu’en combinaison avec un processus soigneusement planifié de sauvegarde et de récupération, compte tenu de la possibilité que des héritiers survivant au propriétaire puissent récupérer la cryptomonnaie.

Travailler avec des codes mnémoniques

BIP-39 est implémenté comme une bibliothèque dans de nombreux langages de programmation différents. Par example:

python-mnemonic

L’implémentation de référence de la norme par l’équipe SatoshiLabs qui a proposé BIP-39, en Python

ConsenSys/eth-lightwallet

Portefeuille léger JS Ethereum pour nœuds et navigateur (avec BIP-39)

npm/bip39

Implémentation JavaScript de Bitcoin BIP-39 : Code mnémonique pour générer des clés déterministes

Il existe également un générateur BIP-39 implémenté dans une page Web autonome (Un générateur BIP-39 en tant que page Web autonome), ce qui est extrêmement utile pour les tests et l’expérimentation. Le Mnemonic Code Converter génère des mnémoniques, des valeurs d’amorçage et des clés privées étendues. Il peut être utilisé hors ligne dans un navigateur ou accessible en ligne.

Page Web du générateur BIP-39
Figure 30. Un générateur BIP-39 en tant que page Web autonome

Créer un portefeuille HD à partir de la valeur d’amorçage

Les portefeuilles HD sont créés à partir d’une seule valeur d’amorçage racine, qui est un nombre aléatoire de 128, 256 ou 512 bits. Le plus souvent, cette valeur d’amorçage est générée à partir d’un mnémonique comme détaillé dans la section précédente.

Chaque clé du portefeuille HD est dérivée de manière déterministe de cette valeur d’amorçage racine, ce qui permet de recréer l’intégralité du portefeuille HD à partir de cette valeur d’amorçage dans n’importe quel portefeuille HD compatible. Cela facilite l’exportation, la sauvegarde, la restauration et l’importation de portefeuilles HD contenant des milliers, voire des millions de clés en transférant uniquement le mnémonique à partir duquel la valeur d’amorçage racine est dérivée.

Portefeuilles HD (BIP-32) et Chemins (BIP-43/44)

La plupart des portefeuilles HD suivent le Standard BIP-32, qui est devenu un standard industriel de facto pour la génération de clé déterministe.

Nous ne discuterons pas de tous les détails du BIP-32 ici, seulement des composants nécessaires pour comprendre comment il est utilisé dans les portefeuilles. Le principal aspect important est les relations hiérarchiques arborescentes qu’il est possible que les clés dérivées aient, comme vous pouvez le voir dans Portefeuille HD : un arbre de clés généré à partir d’une seule valeur d’amorçage. Il est également important de comprendre les notions de clés étendues et clés renforcées, qui sont expliquées dans les sections suivantes.

Il existe des dizaines d’implémentations interopérables de BIP-32 proposées dans de nombreuses bibliothèques de logiciels. Ceux-ci sont principalement conçus pour les portefeuilles Bitcoin, qui implémentent les adresses d’une manière différente, mais partagent la même implémentation de dérivation de clé que les portefeuilles compatibles BIP-32 d’Ethereum. Utilisez-en un https://github.com/ConsenSys/eth-lightwallet [conçu pour Ethereum], ou adaptez-en un à partir de Bitcoin en ajoutant une bibliothèque d’encodage d’adresses Ethereum.

Il existe également un générateur BIP-32 implémenté sous la forme d’une page Web autonome qui est très utile pour tester et expérimenter avec BIP-32.

Warning

Le générateur BIP-32 autonome n’est pas un site HTTPS. C’est pour vous rappeler que l’utilisation de cet outil n’est pas sécurisée. C’est uniquement pour tester. Vous ne devez pas utiliser les clés produites par ce site avec des fonds réels.

Clés publiques et privées étendues

Dans la terminologie BIP-32, les clés peuvent être "étendues". Avec les bonnes opérations mathématiques, ces clés "parentes" étendues peuvent être utilisées pour dériver des clés "enfant", produisant ainsi la hiérarchie des clés et des adresses décrites précédemment. Une clé parent n’a pas besoin d’être au sommet de l’arborescence. peut être choisi n’importe où dans l’arborescence. L’extension d’une clé implique de prendre la clé elle-même et d’y ajouter un code de chaîne spécial. Un code de chaîne est une chaîne binaire de 256 bits qui est mélangé avec chaque clé pour produire des clés enfants.

Si la clé est une clé privée, elle devient une clé privée étendue qui se distingue par le préfixe xprv :

xprv9s21ZrQH143K2JF8RafpqtKiTbsbaxEeUaMnNHsm5o6wCW3z8ySyH4UxFVSfZ8n7ESu7fgir8i...

Une clé publique étendue se distingue par le préfixe xpub :

xpub661MyMwAqRbcEnKbXcCqD2GT1di5zQxVqoHPAgHNe8dv5JP8gWmDproS6kFHJnLZd23tWevhdn...

Une caractéristique très utile des portefeuilles HD est la possibilité de dériver les clés publiques enfants des clés publiques parents, sans avoir les clés privées. Cela nous donne deux façons de dériver une clé publique enfant : soit directement à partir de la clé privée enfant, soit à partir de la clé publique parent.

Une clé publique étendue peut donc être utilisée pour dériver toutes les clés publiques (et uniquement les clés publiques) dans cette branche de la structure du portefeuille HD.

Ce raccourci peut être utilisé pour créer des déploiements très sécurisés à clé publique uniquement, où un serveur ou une application possède une copie d’une clé publique étendue, mais aucune clé privée. Ce type de déploiement peut produire un nombre infini de clés publiques et d’adresses Ethereum, mais ne peut pas dépenser l’argent envoyé à ces adresses. Pendant ce temps, sur un autre serveur plus sécurisé, la clé privée étendue peut dériver toutes les clés privées correspondantes pour signer des transactions et dépenser de l’argent.

Une application courante de cette méthode consiste à installer une clé publique étendue sur un serveur Web qui sert une application de commerce électronique. Le serveur Web peut utiliser la fonction de dérivation de clé publique pour créer une nouvelle adresse Ethereum pour chaque transaction (par exemple, pour le panier d’un client) et n’aura aucune clé privée vulnérable au vol. Sans les portefeuilles HD, la seule façon d’y parvenir est de générer des milliers d’adresses Ethereum sur un serveur sécurisé séparé, puis de les précharger sur le serveur de commerce électronique. Cette approche est lourde et nécessite une maintenance constante pour s’assurer que le serveur ne manque pas de clés, d’où la préférence d’utiliser des clés publiques étendues à partir de portefeuilles HD.

Une autre application courante de cette solution est pour le stockage à froid ou pour les portefeuilles matériels. Dans ce scénario, la clé privée étendue peut être stockée dans un portefeuille matériel, tandis que la clé publique étendue peut être conservée en ligne. L’utilisateur peut créer des adresses "de réception" à volonté, tandis que les clés privées sont stockées en toute sécurité hors ligne. Pour dépenser les fonds, l’utilisateur peut utiliser la clé privée étendue dans un client Ethereum de signature hors ligne ou signer des transactions sur le périphérique de portefeuille matériel.

Dérivation de clé enfant renforcée

La possibilité de dériver une branche de clés publiques à partir d’une clé publique étendue, ou xpub, est très utile, mais elle comporte un risque potentiel. L’accès à une xpub ne donne pas accès aux clés privées enfants. Cependant, étant donné que le xpub contient le code de chaîne (utilisé pour dériver les clés publiques enfants de la clé publique parent), si une clé privée enfant est connue ou divulguée d’une manière ou d’une autre, elle peut être utilisée avec le code de chaîne pour dériver tous les autres enfants privés. clés. Une seule clé privée enfant divulguée, associée à un code de chaîne parent, révèle toutes les clés privées de tous les enfants. Pire encore, la clé privée enfant associée à un code de chaîne parent peut être utilisée pour déduire la clé privée parent.

Pour contrer ce risque, les portefeuilles HD utilisent une fonction de dérivation alternative appelée dérivation renforcée, qui "casse" la relation entre la clé publique parent et le code de chaîne enfant. La fonction de dérivation renforcée utilise la clé privée parent pour dériver le code de chaîne enfant, au lieu de la clé publique parent. Cela crée un "pare-feu" dans la séquence parent/enfant, avec un code de chaîne qui ne peut pas être utilisé pour compromettre une clé privée parent ou sœur.

En termes simples, si vous souhaitez utiliser la commodité d’un xpub pour dériver des branches de clés publiques sans vous exposer au risque d’une fuite de code de chaîne, vous devez le dériver d’un parent renforcé, plutôt que d’un parent normal. La meilleure pratique consiste à toujours dériver les enfants de niveau 1 des clés principales par dérivation renforcée, afin d’éviter toute compromission des clés principales.

Numéros d’index pour dérivation normale et durcie

Il est clairement souhaitable de pouvoir dériver plus d’une clé enfant à partir d’une clé parent donnée. Pour gérer cela, un numéro d’index est utilisé. Chaque numéro d’index, lorsqu’il est combiné avec une clé parent à l’aide de la fonction spéciale de dérivation d’enfant, donne une clé enfant différente. Le numéro d’index utilisé dans la fonction de dérivation parent-enfant BIP-32 est un entier de 32 bits. Pour distinguer facilement les clés dérivées via la fonction de dérivation normale (non renforcée) des clés dérivées via la dérivation renforcée, ce numéro d’index est divisé en deux plages. Les numéros d’index entre 0 et 231–1 (0x0 à 0x7FFFFFFF)sont utilisés uniquement pour la dérivation normale. Les numéros d’index entre 231 et 232–1 (0x80000000 à 0xFFFFFFFF)sont utilisés uniquement pour la dérivation renforcée. Ainsi, si l’indice est inférieur à 231, l’enfant est normal, alors que si l’indice est égal ou supérieur à 231, l’enfant est endurci.

Pour faciliter la lecture et l’affichage des numéros d’index, les numéros d’index pour les enfants endurcis sont affichés à partir de zéro, mais avec un symbole prime. La première clé enfant normale est donc affichée sous la forme 0, tandis que la première clé enfant renforcée (index 0x80000000)est affichée sous la forme 0'. Dans l’ordre, alors, la deuxième clé renforcée aurait un index de 0x80000001 et serait affichée comme 1', et ainsi de suite. Lorsque vous voyez un index de portefeuille HD i', cela signifie 231 + i.

Identifiant de clé de portefeuille HD (chemin)

Les clés d’un portefeuille HD sont identifiées à l’aide d’un " chemin", avec chaque niveau de l’arborescence séparé par une barre oblique (/) (voir [hd_path_table]). Les clés privées dérivées de la clé privée principale commencent par m. Les clés publiques dérivées de la clé publique principale commencent par M. Par conséquent, la première clé privée enfant de la clé privée principale est m/0. La première clé publique enfant est M/0. Le deuxième petit-enfant du premier enfant est m/0/1, et ainsi de suite.

L'"ascendance" d’une clé se lit de droite à gauche, jusqu’à atteindre la clé maîtresse dont elle est issue. Par exemple, l’identifiant m/x/y/z décrit la clé qui est le z-ème enfant de la clé m/x/y, qui est le y-ème enfant de la clé m/x, qui est le x-ième enfant de m.

Exemples de chemin de portefeuille .HD

Chemin HD Clé décrite

m/0

La première clé privée enfant (0)de la clé privée principale (m)

m/0/0

La clé privée du premier petit-enfant du premier enfant (m/0)

m/0'/0

Le premier petit-enfant normal du premier enfant renforcé (m/0')

m/1/0

La clé privée du premier petit-enfant du deuxième enfant (m/1)

M/23/17/0/0

La clé publique du premier arrière-arrière-petit-enfant du premier arrière-petit-enfant du 18e petit-enfant du 24e enfant

La structure arborescente du portefeuille HD est extrêmement flexible. Le revers de la médaille est qu’il permet également une complexité illimitée : chaque clé étendue parent peut avoir 4 milliards d’enfants : 2 milliards d’enfants normaux et 2 milliards d’enfants renforcés. Chacun de ces enfants peut avoir 4 milliards d’enfants supplémentaires, et ainsi de suite. L’arbre peut être aussi profond que vous le souhaitez, avec un nombre potentiellement infini de générations. Avec tout ce potentiel, il peut devenir assez difficile de naviguer dans ces très grands arbres.

Deux BIP offrent un moyen de gérer cette complexité potentielle en créant des normes pour la structure des arborescences de portefeuille HD. BIP-43 propose l’utilisation du premier index enfant renforcé comme identifiant spécial qui signifie le "but" de la structure arborescente. Basé sur BIP-43, un portefeuille HD ne devrait utiliser qu’une seule branche de niveau 1 de l’arborescence, le numéro d’index définissant l’objectif du portefeuille en identifiant la structure et l’espace de noms du reste de l’arborescence. Plus précisément, un portefeuille HD utilisant uniquement la branche m/i'/... est destiné à signifier un objectif spécifique et cet objectif est identifié par le numéro d’index i.

Étendant cette spécification, BIP-44 propose une structure multicompte multidevise signifiée en définissant le numéro de "but" à 44'. Tous les portefeuilles HD suivant la structure BIP-44 sont identifiés par le fait qu’ils n’utilisent qu’une seule branche de l’arbre : m/44'/*.

BIP-44 spécifie la structure comme étant composée de cinq niveaux d’arborescence prédéfinis :

m / but' / type_de_monnaie' / compte' / change / index_adresse

Le premier niveau, but', est toujours réglé sur 44'. Le deuxième niveau, type_de_monnaie', spécifie le type de pièce de cryptomonnaie, permettant des portefeuilles HD multidevises où chaque devise a son propre sous-arbre sous le deuxième niveau. Il existe plusieurs devises définies dans un document standard appelé SLIP0044 ; par exemple, Ethereum vaut m/44'/60', Ethereum Classic vaut m/44'/61', Bitcoin vaut m/44'/0' et Testnet pour tous devises est m/44'/1'.

Le troisième niveau de l’arborescence est compte', qui permet aux utilisateurs de subdiviser leurs portefeuilles en sous-comptes logiques distincts à des fins comptables ou organisationnelles. Par exemple, un portefeuille HD peut contenir deux "comptes" Ethereum : m/44'/60'/0' et m/44'/60'/1'. Chaque compte est la racine de sa propre sous-arborescence.

Parce que BIP-44 a été créé à l’origine pour Bitcoin, il contient une "bizarrerie" qui n’est pas pertinente dans le monde Ethereum. Au quatrième niveau du chemin, change, un portefeuille HD a deux sous-arbres : un pour créer des adresses de réception et un pour créer des adresses de change (retour du change de la transaction). Seul le chemin "réception" est utilisé dans Ethereum, car il n’est pas nécessaire de changer d’adresse comme c’est le cas dans Bitcoin. Notez qu’alors que les niveaux précédents utilisaient une dérivation renforcée, ce niveau utilise une dérivation normale. Cela permet au niveau du compte de l’arborescence d’exporter des clés publiques étendues pour une utilisation dans un environnement non sécurisé. Les adresses utilisables sont dérivées par le portefeuille HD en tant qu’enfants du quatrième niveau, faisant du cinquième niveau de l’arborescence le index_adresse. Par exemple, la troisième adresse de réception des paiements Ethereum dans le compte principal serait M/44'/60'/0'/0/2. [bip44_path_examples] montre quelques exemples supplémentaires.

Exemples de structure de portefeuille .BIP-44 HD

Chemin HD Clé décrite

M/44'/60'/0'/0/2+

La troisième clé publique de réception pour le compte Ethereum principal

M/44'/0'/3'/1/14+

La 15ème clé publique d’adresse de change pour le 4ème compte Bitcoin

m/44'/2'/0'/0/1

La deuxième clé privée du compte principal Litecoin, pour la signature des transactions

Conclusion

Les portefeuilles sont la base de toute application chaîne de blocs destinée aux utilisateurs. Ils permettent aux utilisateurs de gérer des collections de clés et d’adresses. Les portefeuilles permettent également aux utilisateurs de démontrer leur propriété d’ether et d’autoriser les transactions, en appliquant des signatures numériques, comme nous le verrons dans Transactions.

Transactions

Bannière Amazon du livre Maîtriser Ethereum

Les transactions sont des messages signés provenant d’un compte externe, transmis par le réseau Ethereum et enregistrés sur la chaîne de blocs Ethereum. Cette définition de base cache bien des détails surprenants et fascinants. Une autre façon de voir les transactions est qu’elles sont les seules choses qui peuvent déclencher un changement d’état ou provoquer l’exécution d’un contrat dans l’EVM. Ethereum est une machine à états singleton globale, et les transactions sont ce qui fait que cette machine à états "fonctionner", qui change son état. Les contrats ne fonctionnent pas seuls. Ethereum ne fonctionne pas de manière autonome. Tout commence par une transaction.

Dans ce chapitre, nous allons disséquer les transactions, montrer leur fonctionnement et examiner les détails. Notez qu’une grande partie de ce chapitre s’adresse à ceux qui souhaitent gérer leurs propres transactions à un niveau inférieur, peut-être parce qu’ils écrivent une application de portefeuille ; vous n’avez pas à vous en soucier si vous êtes satisfait des applications de portefeuille existantes, bien que vous puissiez trouver les détails intéressants !

La structure d’une transaction

Regardons d’abord la structure de base d’une transaction, telle qu’elle est sérialisée et transmise sur le réseau Ethereum. Chaque client et application qui reçoit une transaction sérialisée la stocke en mémoire en utilisant sa propre structure de données interne, éventuellement agrémentée de métadonnées qui n’existent pas dans la transaction sérialisée du réseau elle-même. La sérialisation réseau est la seule forme standard d’une transaction.

Une transaction est un message binaire sérialisé qui contient les données suivantes :

Nonce

Un numéro de séquence, émis par l’EOA d’origine, utilisé pour empêcher la relecture du message

Prix du gaz

Le prix du gaz (en wei) que le donneur d’ordre est prêt à payer

Limite de gaz

La quantité maximale de gaz que l’initiateur est prêt à acheter pour cette transaction

Destinataire

L’adresse Ethereum de destination

Valeur

La quantité d’ether à envoyer à la destination

Données

La charge utile de données binaires de longueur variable

v,r,s

Les trois composants d’une signature numérique ECDSA de l’EOA d’origine

La structure du message de transaction est sérialisée à l’aide du schéma de codage Recursive Length Prefix (RLP) (ou Préfixe de longueur récursif), qui a été créé spécifiquement pour sérialisation de données simple et parfaite dans Ethereum. Tous les nombres dans Ethereum sont codés sous forme d’entiers gros-boutiste, de longueurs multiples de 8 bits.

Notez que les étiquettes de champ (to, gas limit, etc.) sont affichées ici pour plus de clarté, mais ne font pas partie des données sérialisées de la transaction, qui contiennent les valeurs de champ codées RLP. En général, RLP ne contient aucun délimiteur de champ ou étiquette. Le préfixe de longueur de RLP est utilisé pour identifier la longueur de chaque champ. Tout ce qui dépasse la longueur définie appartient au champ suivant dans la structure.

Bien qu’il s’agisse de la structure de transaction réelle transmise, la plupart des représentations internes et des visualisations d’interface utilisateur l’agrémentent d’informations supplémentaires, dérivées de la transaction ou de la chaîne de blocs.

Par exemple, vous remarquerez peut-être qu’il n’y a pas de données "from" dans l’adresse identifiant l’EOA d’origine. En effet, la clé publique de l’EOA peut être dérivée des composants v,r,s de la signature ECDSA. L’adresse peut, à son tour, être dérivée de la clé publique. Lorsque vous voyez une transaction affichant un champ "from", qui a été ajouté par le logiciel utilisé pour visualiser la transaction. D’autres métadonnées fréquemment ajoutées à la transaction par le logiciel client incluent le numéro de bloc (une fois extrait et inclus dans la chaîne de blocs) et un ID de transaction (hachage calculé). Encore une fois, ces données sont dérivées de la transaction et ne font pas partie du message de transaction lui-même.

Le Nonce de transaction

Le nonce est l’un des composants les plus importants et les moins compris d’une transaction. La définition du Livre jaune (voir Lectures complémentaires) lit :

nonce : Une valeur scalaire égale au nombre de transactions émises depuis cette adresse ou, dans le cas de comptes avec code associé, au nombre de créations de contrats effectuées par ce compte.

À proprement parler, le nonce est un attribut de l’adresse d’origine ; c’est-à-dire qu’il n’a de sens que dans le contexte de l’adresse d’envoi. Cependant, le nonce n’est pas stocké explicitement dans le cadre de l’état d’un compte sur la chaîne de blocs. Au lieu de cela, il est calculé dynamiquement, en comptant le nombre de transactions confirmées provenant d’une adresse.

Il existe deux scénarios dans lesquels l’existence d’un nonce de comptage des transactions est importante : la fonctionnalité d’utilisabilité des transactions incluses dans l’ordre de création et la fonctionnalité vitale de la protection contre la duplication des transactions. Regardons un exemple de scénario pour chacun d’entre eux :

  1. Imaginez que vous souhaitiez effectuer deux transactions. Vous avez un paiement important à effectuer de 6 ethers, et aussi un autre paiement de 8 ethers. Vous signez et diffusez d’abord la transaction à 6 ethers, car c’est la plus importante, puis vous signez et diffusez la deuxième transaction à 8 ethers. Malheureusement, vous avez oublié le fait que votre compte ne contient que 10 ethers, donc le réseau ne peut pas accepter les deux transactions : l’une d’entre elles échouera. Parce que vous avez envoyé le 6-ether le plus important en premier, vous vous attendez naturellement à ce que celui-ci passe et que le 8-ether soit rejeté. Cependant, dans un système décentralisé comme Ethereum, les nœuds peuvent recevoir les transactions dans n’importe quel ordre ; il n’y a aucune garantie qu’un nœud particulier aura une transaction qui lui sera propagée avant l’autre. En tant que tel, il sera presque certainement le cas que certains nœuds reçoivent la transaction à 6 ethers en premier et que d’autres reçoivent la transaction à 8 ethers en premier. Sans le nonce, il serait aléatoire de savoir lequel est accepté et lequel est rejeté. Cependant, avec le nonce inclus, la première transaction que vous avez envoyée aura un nonce de, disons, 3, tandis que la transaction 8-ether a la valeur de nonce suivante (c’est-à-dire 4). Ainsi, cette transaction sera ignorée jusqu’à ce que les transactions avec des nonces de 0 à 3 aient été traitées, même si elle est reçue en premier. Phew!

  2. Imaginez maintenant que vous avez un compte avec 100 ethers. Fantastique ! Vous trouvez quelqu’un en ligne qui acceptera le paiement en ether pour un mcguffin-widget que vous voulez vraiment acheter. Vous leur envoyez 2 ethers et ils vous envoient le widget mcguffin. Beau. Pour effectuer ce paiement à 2 ethers, vous avez signé une transaction en envoyant 2 ethers de votre compte à leur compte, puis vous les avez diffusés sur le réseau Ethereum pour qu’ils soient vérifiés et inclus dans la chaîne de blocs. Maintenant, sans valeur nonce dans la transaction, une seconde transaction envoyant 2 ethers à la même adresse une seconde fois aura exactement la même apparence que la première transaction. Cela signifie que toute personne qui voit votre transaction sur le réseau Ethereum (ce qui signifie que tout le monde, y compris le destinataire ou vos ennemis) peut "rejouer" la transaction encore et encore et encore jusqu’à ce que tout votre ether soit parti simplement en copiant et collant votre transaction d’origine et le renvoyer sur le réseau. Cependant, avec la valeur nonce incluse dans les données de transaction, chaque transaction est unique, même lors de l’envoi de la même quantité d’ether à la même adresse de destinataire plusieurs fois. Ainsi, en ayant le nonce incrémenté dans le cadre de la transaction, il n’est tout simplement pas possible pour quiconque de "dupliquer" un paiement que vous avez effectué.

En résumé, il est important de noter que l’utilisation du nonce est en fait vitale pour un protocole basé sur le compte, contrairement au mécanisme "Unspent Transaction Output" (UTXO) du protocole Bitcoin.

Garder une trace des Nonces

En termes pratiques, le nonce est un décompte à jour du nombre de transactions confirmées (c’est-à-dire en chaîne) provenant d’un compte. Pour savoir ce qu’est le nonce, vous pouvez interroger la chaîne de blocs, par exemple via l’interface web3. Ouvrez une console JavaScript dans Geth (ou votre interface web3 préférée) sur Ropsten testnet, puis tapez :

> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f")
40

Tip

Le nonce est un compteur basé sur zéro, ce qui signifie que la première transaction a le nonce 0. Dans cet exemple, nous avons un nombre de transactions de 40, ce qui signifie que les nonces 0 à 39 ont été vus. Le nonce de la prochaine transaction devra être de 40.

Votre portefeuille gardera une trace des nonces pour chaque adresse qu’il gère. C’est assez simple à faire, tant que vous n’effectuez des transactions qu’à partir d’un seul point. Disons que vous écrivez votre propre logiciel de portefeuille ou une autre application qui génère des transactions. Comment suivre les nonces ?

Lorsque vous créez une nouvelle transaction, vous affectez le nonce suivant dans la séquence. Mais jusqu’à ce qu’il soit confirmé, il ne comptera pas dans le total getTransactionCount.

Warning

Soyez prudent lorsque vous utilisez la fonction getTransactionCount pour compter les transactions en attente, car vous pourriez rencontrer des problèmes si vous envoyez quelques transactions à la suite.

Regardons un exemple :

> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
40
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41

Tip

Si vous essayez de recréer vous-même ces exemples de code dans la console javascript de Geth, vous devez utiliser web3.toWei() au lieu de web3.utils.toWei(). En effet, Geth utilise une ancienne version de la bibliothèque web3.

Comme vous pouvez le voir, la première transaction que nous avons envoyée a augmenté le nombre de transactions à 41, indiquant la transaction en attente. Mais lorsque nous avons envoyé trois autres transactions en succession rapide, l’appel getTransactionCount ne les a pas comptées. Il n’en comptait qu’un, même si vous vous attendiez à ce qu’il y en ait trois en attente dans le mempool. Si nous attendons quelques secondes pour permettre aux communications réseau de s’établir, l’appel getTransactionCount renverra le nombre attendu. Mais dans l’intervalle, bien qu’il y ait plus d’une transaction en attente, cela pourrait ne pas nous aider.

Lorsque vous construisez une application qui construit des transactions, elle ne peut pas s’appuyer sur getTransactionCount pour les transactions en attente. Ce n’est que lorsque les nombres en attente et confirmés sont égaux (toutes les transactions en attente sont confirmées) que vous pouvez faire confiance à la sortie de getTransactionCount pour démarrer votre compteur de nonce. Par la suite, gardez une trace du nonce dans votre application jusqu’à ce que chaque transaction soit confirmée.

L’interface JSON RPC de Parity propose la fonction parity_nextNonce, qui renvoie le prochain nonce à utiliser dans une transaction. La fonction parity_nextNonce compte correctement les nonces, même si vous construisez plusieurs transactions en succession rapide sans les confirmer :

$ curl --data '{"method":"parity_nextNonce", \
  "params":["0x9e713963a92c02317a681b9bb3065a8249de124f"],\
  "id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST \
  localhost:8545

{"jsonrpc":"2.0","result":"0x32","id":1}

Tip

Parity dispose d’une console Web pour accéder à l’interface JSON RPC, mais ici nous utilisons un client HTTP en ligne de commande pour y accéder.

Lacunes dans les nonces, les nonces en double et la confirmation

Il est important de garder une trace des nonces si vous créez des transactions par programmation, surtout si vous le faites à partir de plusieurs processus indépendants simultanément.

Le réseau Ethereum traite les transactions de manière séquentielle, sur la base du nonce. Cela signifie que si vous transmettez une transaction avec nonce 0, puis transmettez une transaction avec nonce 2, la deuxième transaction ne sera incluse dans aucun bloc. Il sera stocké dans le mempool, pendant que le réseau Ethereum attend que le nonce manquant apparaisse. Tous les nœuds supposeront que le nonce manquant a simplement été retardé et que la transaction avec le nonce 2 a été reçue dans le désordre.

Si vous transmettez ensuite une transaction avec le nonce 1 manquant, les deux transactions (nonces 1 et 2) seront traitées et incluses (si elles sont valides, bien sûr). Une fois que vous avez comblé le vide, le réseau peut exploiter la transaction hors séquence qu’il a conservée dans le mempool.

Cela signifie que si vous créez plusieurs transactions en séquence et que l’une d’entre elles n’est officiellement incluse dans aucun bloc, toutes les transactions suivantes seront "bloquées", en attendant le nonce manquant. Une transaction peut créer un "écart" par inadvertance dans la séquence nonce car elle n’est pas valide ou n’a pas suffisamment de gaz. Pour que les choses bougent à nouveau, vous devez transmettre une transaction valide avec le nonce manquant. Vous devez également garder à l’esprit qu’une fois qu’une transaction avec le nonce "manquant" est validée par le réseau, toutes les transactions de diffusion avec les nonces suivants deviendront progressivement valides ; il n’est pas possible de "rappeler" une transaction !

Si, en revanche, vous dupliquez accidentellement un nonce, par exemple en transmettant deux transactions avec le même nonce mais différents destinataires ou valeurs, l’un d’entre eux sera confirmé et l’autre sera rejeté. Celui qui est confirmé sera déterminé par la séquence dans laquelle ils arrivent au premier nœud de validation qui les reçoit, c’est-à-dire que ce sera assez aléatoire.

Comme vous pouvez le constater, le suivi des nonces est nécessaire, et si votre application ne gère pas correctement ce processus, vous rencontrerez des problèmes. Malheureusement, les choses deviennent encore plus difficiles si vous essayez de le faire simultanément, comme nous le verrons dans la section suivante.

Concurrence, origine des transactions et nonces

La concurrence est un aspect complexe de l’informatique, et il apparaît parfois de manière inattendue, en particulier dans les systèmes en temps réel décentralisés et distribués comme Ethereum.

En termes simples, la concurrence est lorsque vous avez un calcul simultané par plusieurs systèmes indépendants. Ceux-ci peuvent se trouver dans le même programme (par exemple, multithreading), sur le même processeur (par exemple, multitraitement) ou sur différents ordinateurs (c’est-à-dire, systèmes distribués). Ethereum, par définition, est un système qui permet la simultanéité des opérations (nœuds, clients, DApps) mais applique un état singleton par consensus.

Maintenant, imaginez que vous ayez plusieurs applications de portefeuille indépendantes qui génèrent des transactions à partir de la ou des mêmes adresses. Un exemple d’une telle situation serait un échange traitant des retraits du portefeuille chaud de l’échange (un portefeuille dont les clés sont stockées en ligne, contrairement à un portefeuille froid où les clés ne sont jamais en ligne). Idéalement, vous voudriez avoir plus d’un ordinateur traitant les retraits, afin qu’il ne devienne pas un goulot d’étranglement ou un point de défaillance unique. Cependant, cela devient rapidement problématique, car le fait d’avoir plus d’un ordinateur produisant des retraits entraînera des problèmes épineux de concurrence, dont le moindre n’est pas la sélection des nonces. Comment plusieurs ordinateurs générant, signant et diffusant des transactions à partir du même compte du portefeuille chaud (Hot Wallet) se coordonnent-ils ?

Vous pouvez utiliser un seul ordinateur pour attribuer des nonces, selon le principe du premier arrivé, premier servi, aux ordinateurs signant des transactions. Cependant, cet ordinateur est maintenant un point de défaillance unique. Pire encore, si plusieurs nonces sont attribués et que l’un d’entre eux n’est jamais utilisé (à cause d’une défaillance de l’ordinateur traitant la transaction avec ce nonce), toutes les transactions suivantes restent bloquées.

Une autre approche serait de générer les transactions, mais de ne pas leur attribuer de nonce (et donc de les laisser non signées - rappelez-vous que le nonce fait partie intégrante des données de transaction et doit donc être inclus dans la signature numérique qui authentifie la transaction) . Vous pouvez ensuite les mettre en file d’attente sur un seul nœud qui les signe et garde également une trace des nonces. Encore une fois, cependant, ce serait un point d’étranglement dans le processus : la signature et le suivi des nonces est la partie de votre opération qui est susceptible d’être encombrée sous charge, alors que la génération de la transaction non signée est la partie que vous n’avez pas vraiment besoin de paralléliser. Vous auriez une certaine concurrence, mais il y aurait un manque dans une partie essentielle du processus.

En fin de compte, ces problèmes de simultanéité, en plus de la difficulté de suivre les soldes des comptes et les confirmations de transaction dans des processus indépendants, obligent la plupart des implémentations à éviter la simultanéité et à créer des goulots d’étranglement tels qu’un processus unique gérant toutes les transactions de retrait dans un échange, ou la mise en place de plusieurs des portefeuilles chauds qui peuvent fonctionner de manière totalement indépendante pour les retraits et ne doivent être rééquilibrés que par intermittence.

Gaz de transaction

Nous avons un peu parlé du gaz dans les chapitres précédents, et nous en discutons plus en détail dans Gaz. Cependant, couvrons quelques notions de base sur le rôle des composants gasPrice et gasLimit d’une transaction.

Le gaz est le carburant d’Ethereum. Le gaz n’est pas de l’ether, c’est une monnaie virtuelle distincte avec son propre taux de change par rapport à l’ether. Ethereum utilise du gaz pour contrôler la quantité de ressources qu’une transaction peut utiliser, car elle sera traitée sur des milliers d’ordinateurs à travers le monde. Le modèle de calcul ouvert (Turing-complet) nécessite une certaine forme de mesure afin d’éviter les attaques par déni de service ou les transactions gourmandes en ressources par inadvertance.

Le gaz est séparé de l’ether afin de protéger le système de la volatilité qui pourrait survenir avec les changements rapides de la valeur de l’ether, et aussi comme moyen de gérer les rapports importants et sensibles entre les coûts des diverses ressources que le gaz paie. (à savoir, calcul, mémoire et stockage).

Le champ gasPrice dans une transaction permet à l’initiateur de la transaction de définir le prix qu’il est prêt à payer en échange de gaz. Le prix est mesuré en wei par unité de gaz. Par exemple, dans l’exemple de transaction dans Les bases d’Ethereum votre portefeuille fixe le gasPrice à 3 gwei (3 gigawei ou 3 milliards de wei).

Tip

Le site populaire ETH Gas Station fournit des informations sur les prix actuels du gaz et d’autres paramètres de gaz pertinents pour le réseau principal Ethereum.

Les portefeuilles peuvent ajuster le gasPrice dans les transactions qu’ils génèrent pour obtenir une confirmation plus rapide des transactions. Plus le gasPrice est élevé, plus la transaction est susceptible d’être confirmée rapidement. À l’inverse, les transactions moins prioritaires peuvent avoir un prix réduit, ce qui entraîne une confirmation plus lente. La valeur minimale à laquelle gasPrice peut être défini est zéro, ce qui signifie une transaction sans frais. Pendant les périodes de faible demande d’espace dans un bloc, de telles transactions pourraient très bien être minées.

Note

Le gasPrice minimum acceptable est zéro. Cela signifie que les portefeuilles peuvent générer des transactions totalement gratuites. Selon la capacité, celles-ci peuvent ne jamais être confirmées, mais rien dans le protocole n’interdit les transactions gratuites. Vous pouvez trouver plusieurs exemples de telles transactions incluses avec succès sur la chaîne de blocs Ethereum.

L’interface web3 propose une suggestion gasPrice, en calculant un prix médian sur plusieurs blocs (nous pouvons utiliser la console truffle ou n’importe quelle console web3 JavaScript pour le faire) :

> web3.eth.getGasPrice(console.log)
> null BigNumber { s: 1, e: 10, c: [ 10000000000 ] }

Le deuxième champ important lié au gaz est gasLimit. En termes simples, gasLimit donne le nombre maximum d’unités de gaz que l’initiateur de la transaction est prêt à acheter pour finaliser la transaction. Pour les paiements simples, c’est-à-dire les transactions qui transfèrent de l’ether d’un EOA à un autre EOA, la quantité de gaz nécessaire est fixée à 21 000 unités de gaz. Pour calculer combien d’ether cela coûtera, vous multipliez 21 000 par le gasPrice que vous êtes prêt à payer. Par example:

> web3.eth.getGasPrice(function(err, res) {console.log(res*21000)} )
> 210000000000000

Si l’adresse de destination de votre transaction est un contrat, la quantité de gaz nécessaire peut être estimée mais ne peut pas être déterminée avec précision. En effet, un contrat peut évaluer différentes conditions qui conduisent à différentes voies d’exécution, avec différents coûts totaux du gaz. Le contrat peut n’exécuter qu’un calcul simple ou un calcul plus complexe, selon des conditions indépendantes de votre volonté et imprévisibles. Pour le démontrer, regardons un exemple : nous pouvons écrire un contrat intelligent qui incrémente un compteur à chaque fois qu’il est appelé et exécute une boucle particulière un nombre de fois égal au nombre d’appels. Peut-être qu’au 100e appel, il distribue un prix spécial, comme une loterie, mais doit faire des calculs supplémentaires pour calculer le prix. Si vous appelez le contrat 99 fois, une chose se produit, mais au 100e appel, quelque chose de très différent se produit. La quantité de gaz que vous paieriez pour cela dépend du nombre d’autres transactions qui ont appelé cette fonction avant que votre transaction ne soit incluse dans un bloc. Peut-être que votre estimation est basée sur la 99e transaction, mais juste avant que votre transaction ne soit confirmée, quelqu’un d’autre appelle le contrat pour la 99e fois. Vous êtes maintenant la 100e transaction à appeler, et l’effort de calcul (et le coût du gaz) est beaucoup plus élevé.

Pour emprunter une analogie courante utilisée dans Ethereum, vous pouvez considérer gasLimit comme la capacité du réservoir de carburant de votre voiture (votre voiture est la transaction). Vous remplissez le réservoir avec autant d’essence que vous pensez qu’il en aura besoin pour le trajet (le calcul nécessaire pour valider votre transaction). Vous pouvez estimer le montant dans une certaine mesure, mais il peut y avoir des changements inattendus dans votre trajet, comme une déviation (un chemin d’exécution plus complexe), qui augmentent la consommation de carburant.

L’analogie avec un réservoir de carburant est cependant quelque peu trompeuse. Il s’agit en fait plus d’un compte de crédit pour une entreprise de station-service, où vous payez une fois le voyage terminé, en fonction de la quantité d’essence que vous avez réellement utilisée. Lorsque vous transmettez votre transaction, l’une des premières étapes de validation consiste à vérifier que le compte dont elle provient dispose de suffisamment d’ether pour payer la limite gasPrice * gaz. Mais le montant n’est pas réellement déduit de votre compte tant que la transaction n’est pas terminée. Vous n’êtes facturé que pour le gaz réellement consommé par votre transaction, mais vous devez disposer d’un solde suffisant pour le montant maximum que vous êtes prêt à payer avant d’envoyer votre transaction.

Destinataire de la transaction

Le destinataire d’une transaction est spécifié dans le champ to. Celui-ci contient une adresse Ethereum de 20 octets. L’adresse peut être une EOA ou une adresse de contrat.

Ethereum ne valide plus ce champ. Toute valeur de 20 octets est considérée comme valide. Si la valeur de 20 octets correspond à une adresse sans clé privée correspondante, ou sans contrat correspondant, la transaction est toujours valide. Ethereum n’a aucun moyen de savoir si une adresse a été correctement dérivée d’une clé publique (et donc d’une clé privée) existante.

Warning

Le protocole Ethereum ne valide pas les adresses des destinataires dans les transactions. Vous pouvez envoyer à une adresse qui n’a pas de clé privée ou de contrat correspondant, "brûlant" ainsi l’ether, le rendant à jamais inutilisable. La validation doit être effectuée au niveau de l’interface utilisateur.

L’envoi d’une transaction à la mauvaise adresse va probablement brûler l’ether envoyé, le rendant à jamais inaccessible (inutilisable), puisque la plupart des adresses n’ont pas de clé privée connue et donc aucune signature ne peut être générée pour le dépenser. On suppose que la validation de l’adresse se produit au niveau de l’interface utilisateur (voir Encodage hexadécimal avec somme de contrôle en majuscules (EIP-55)). En fait, il existe un certain nombre de raisons valables pour brûler de l’ether - par exemple, pour dissuader de tricher dans les canaux de paiement et autres contrats intelligents - et puisque la quantité d’ether est finie, brûler de l’ether distribue efficacement la valeur brûlée à tous les détenteurs d’ether. (proportionnellement à la quantité d’ether qu’ils contiennent).

Valeur et données de la transaction

La "charge utile" principale d’une transaction est contenue dans deux champs : value et data. Les transactions peuvent avoir à la fois une valeur et des données, uniquement une valeur, uniquement des données ou ni valeur ni données. Les quatre combinaisons sont valides.

Une transaction avec seulement une valeur est un paiement. Une transaction avec uniquement des données est une invocation. Une transaction comportant à la fois de la valeur et des données est à la fois un paiement et une invocation. Une transaction sans valeur ni données - eh bien, ce n’est probablement qu’un gaspillage de gaz ! Mais c’est encore possible.

Essayons toutes ces combinaisons. Nous allons d’abord définir les adresses source et de destination de notre portefeuille, juste pour rendre la démo plus facile à lire :

src = web3.eth.accounts[0];
dst = web3.eth.accounts[1];

Notre première transaction ne contient qu’une valeur (paiement) et aucune charge de données :

web3.eth.sendTransaction({from: src, to: dst, \
  value: web3.utils.toWei(0.01, "ether"), data: ""});

Notre portefeuille affiche un écran de confirmation indiquant la valeur à envoyer, comme indiqué dans Portefeuille de Parity montrant une transaction avec une valeur, mais pas de données.

Portefeuille de Parity montrant une transaction avec valeur, mais pas de données
Figure 31. Portefeuille de Parity montrant une transaction avec une valeur, mais pas de données

L’exemple suivant spécifie à la fois une valeur et une charge utile de données :

web3.eth.sendTransaction({from: src, to: dst, \
  value: web3.utils.toWei(0.01, "ether"), data: "0x1234"});

Notre portefeuille affiche un écran de confirmation indiquant la valeur à envoyer ainsi que la charge utile des données, comme indiqué dans Portefeuille de Parity montrant une transaction avec valeur et données.

Portefeuille de Parity montrant une transaction avec valeur et données
Figure 32. Portefeuille de Parity montrant une transaction avec valeur et données

La transaction suivante inclut une charge utile de données mais spécifie une valeur de zéro :

web3.eth.sendTransaction({from: src, to: dst, value: 0, data: "0x1234"});

Notre portefeuille affiche un écran de confirmation indiquant la valeur zéro et la charge utile des données, comme indiqué dans Portefeuille de Parity montrant une transaction sans valeur, uniquement des données.

Portefeuille de Parity montrant une transaction sans valeur, uniquement des données
Figure 33. Portefeuille de Parity montrant une transaction sans valeur, uniquement des données

Enfin, la dernière transaction n’inclut ni valeur à envoyer ni données utiles :

web3.eth.sendTransaction({from: src, to: dst, value: 0, data: ""}));

Notre portefeuille affiche un écran de confirmation indiquant la valeur zéro, comme indiqué dans Portefeuille de Parity montrant une transaction sans valeur et sans données.

Portefeuille de Parity montrant une transaction sans valeur et sans données
Figure 34. Portefeuille de Parity montrant une transaction sans valeur et sans données

Transmettre de la valeur aux EOA et aux contrats

Lorsque vous construisez une transaction Ethereum qui contient une valeur, c’est l’équivalent d’un paiement. Ces transactions se comportent différemment selon que l’adresse de destination est un contrat ou non.

Pour les adresses EOA, ou plutôt pour toute adresse qui n’est pas signalée comme un contrat sur la chaîne de blocs, Ethereum enregistrera un changement d’état, ajoutant la valeur que vous avez envoyée au solde de l’adresse. Si l’adresse n’a pas été vue auparavant, elle sera ajoutée à la représentation interne de l’état du client et son solde initialisé à la valeur de votre paiement.

Si l’adresse de destination (to)est un contrat, alors l’EVM exécutera le contrat et tentera d’appeler la fonction nommée dans la charge utile de données de votre transaction. S’il n’y a pas de données dans votre transaction, l’EVM appellera une fonction fallback (ou fonction de repli ou par défaut) et, si cette fonction est payante, l’exécutera pour déterminer ce qu’il faut faire ensuite. S’il n’y a pas de code dans la fonction de secours, l’effet de la transaction sera d’augmenter le solde du contrat, exactement comme un paiement sur un portefeuille. S’il n’y a pas de fonction de secours ou de fonction de secours non payante, la transaction sera annulée.

Un contrat peut rejeter les paiements entrants en levant une exception immédiatement lorsqu’une fonction est appelée, ou tel que déterminé par des conditions codées dans une fonction. Si la fonction se termine avec succès (sans exception), l’état du contrat est mis à jour pour refléter une augmentation du solde d’ether du contrat.

Transmission d’une charge utile de données à un EOA ou à un contrat

Lorsque votre transaction contient des données, elle est très probablement adressée à une adresse de contrat. Cela ne signifie pas que vous ne pouvez pas envoyer une charge utile de données à un EOA, ce qui est tout à fait valide dans le protocole Ethereum. Cependant, dans ce cas, l’interprétation des données dépend du portefeuille que vous utilisez pour accéder à l’EOA. Il est ignoré par le protocole Ethereum. La plupart des portefeuilles ignorent également toutes les données reçues lors d’une transaction vers un EOA qu’ils contrôlent. À l’avenir, il est possible que des normes émergent qui permettent aux portefeuilles d’interpréter les données comme le font les contrats, permettant ainsi aux transactions d’invoquer des fonctions exécutées à l’intérieur des portefeuilles des utilisateurs. La différence essentielle est que toute interprétation de la charge utile de données par un EOA n’est pas soumise aux règles de consensus d’Ethereum, contrairement à un contrat exécution.

Pour l’instant, supposons que votre transaction envoie des données à une adresse contractuelle. Dans ce cas, les données seront interprétées par l’EVM comme une invocation de contrat. La plupart des contrats utilisent ces données plus spécifiquement comme invocation de fonction, appelant la fonction nommée et transmettant tous les arguments codés à la fonction.

La charge utile de données envoyée à un contrat compatible ABI (dont vous pouvez supposer que tous les contrats le sont) est un codage hexadécimal de :

Un sélecteur de fonction

Les 4 premiers octets du hachage Keccak-256 du prototype de la fonction. Cela permet au contrat d’identifier sans ambiguïté la fonction que vous souhaitez invoquer.

Les arguments de la fonction

Les arguments de la fonction, encodés selon les règles des différents types élémentaires définies dans la spécification ABI.

Dans Faucet.sol: Un contrat Solidity mettant en place un robinet, nous avons défini une fonction pour les retraits :

function withdraw(uint withdraw_amount) public {

Le prototype d’une fonction est défini comme la chaîne contenant le nom de la fonction, suivi des types de données de chacun de ses arguments, entre parenthèses et séparés par des virgules. Le nom de la fonction ici est withdraw et il prend un seul argument qui est un uint (qui est un alias pour uint256), donc le prototype de withdraw serait :

withdraw(uint256)

Calculons le hachage Keccak-256 de cette chaîne :

> web3.utils.sha3("withdraw(uint256)");
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'

Les 4 premiers octets du hachage sont 0x2e1a7d4d. C’est notre valeur "sélecteur de fonction", qui indiquera au contrat quelle fonction nous voulons appeler.

Ensuite, calculons une valeur à passer comme argument withdraw_amount. Nous voulons retirer 0,01 ether. Encodons cela en un entier 256 bits non signé gros-boutiste hexa-sérialisé, libellé en wei :

> withdraw_amount = web3.utils.toWei(0.01, "ether");
'10000000000000000'
> withdraw_amount_hex = web3.utils.toHex(withdraw_amount);
'0x2386f26fc10000'

Maintenant, nous ajoutons le sélecteur de fonction au montant (rembourré à 32 octets) :

2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000

C’est la charge utile de données pour notre transaction, appelant la fonction withdraw et demandant 0,01 éther comme withdraw_amount.

Transaction spéciale : création de contrat

Un cas particulier que nous devrions mentionner est une transaction qui crée un nouveau contrat sur la chaîne de blocs, le déployant pour une utilisation future. Les transactions de création de contrat sont envoyées à une adresse de destination spéciale appelée adresse zéro ; le champ to dans une transaction d’enregistrement de contrat contient l’adresse 0x0. Cette adresse ne représente ni un EOA (il n’y a pas de paire de clés privée-publique correspondante) ni un contrat. Il ne peut jamais dépenser d’ether ou initier une transaction. Il n’est utilisé que comme destination, avec la signification spéciale "créer ce contrat".

Alors que l’adresse zéro est destinée uniquement à la création de contrats, elle reçoit parfois des paiements de différentes adresses. Il y a deux explications à cela : soit c’est par accident, entraînant la perte d’ether, soit c’est une brûlure d’ether intentionnelle (détruisant délibérément l’ether en l’envoyant à une adresse d’où il ne peut jamais être dépensé). Cependant, si vous souhaitez effectuer une brûlure intentionnelle d’ether, vous devez indiquer clairement votre intention au réseau et utiliser à la place l’adresse de brpulure spécialement désignée :

0x000000000000000000000000000000000000dEaD
Warning

Tout ether envoyé à l’adresse de brûlure désignée deviendra inutilisable et sera perdu à jamais.

Une transaction de création de contrat ne doit contenir qu’une charge utile de données contenant le code intermédiaire compilé qui créera le contrat. Le seul effet de cette transaction est de créer le contrat. Vous pouvez inclure un montant en ether dans le champ value si vous souhaitez configurer le nouveau contrat avec un solde de départ, mais cela est entièrement facultatif. Si vous envoyez une valeur (ether) à l’adresse de création de contrat sans charge utile de données (pas de contrat), l’effet est le même que l’envoi à une adresse de brûlure : il n’y a pas de contrat à créditer, donc l’ether est perdu.

Par exemple, nous pouvons créer le contrat Faucet.sol utilisé dans Les bases d’Ethereum en créant manuellement une transaction à l’adresse zéro avec le contrat dans la charge utile des données. Le contrat doit être compilé dans une représentation en code intermédiaire. Cela peut être fait avec le compilateur Solidity :

$ solc --bin Faucet.sol

Binary:
6060604052341561000f57600080fd5b60e58061001d6000396000f30060606040526004361060...

Les mêmes informations peuvent également être obtenues à partir du compilateur en ligne Remix.

Nous pouvons maintenant créer la transaction :

> src = web3.eth.accounts[0];
> faucet_code = \
  "0x6060604052341561000f57600080fd5b60e58061001d6000396000f300606...f0029";
> web3.eth.sendTransaction({from: src, to: 0, data: faucet_code, \
  gas: 113558, gasPrice: 200000000000});

"0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b"

Il est recommandé de toujours spécifier un paramètre to, même dans le cas d’une création de contrat sans adresse, car le coût d’envoyer accidentellement votre ether à 0x0 et de le perdre pour toujours est trop élevé. Vous devez également spécifier un gasPrice et un gasLimit.

Une fois le contrat extrait, nous pouvons le voir sur l’explorateur de blocs Etherscan, comme indiqué dans Etherscan montrant le contrat miné avec succès.

Etherscan montrant le contrat extrait avec succès
Figure 35. Etherscan montrant le contrat miné avec succès

Nous pouvons regarder le reçu de la transaction pour obtenir des informations sur le contrat :

> web3.eth.getTransactionReceipt( \
  "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b");

{
  blockHash: "0x6fa7d8bf982490de6246875deb2c21e5f3665b4422089c060138fc3907a95bb2",
  blockNumber: 3105256,
  contractAddress: "0xb226270965b43373e98ffc6e2c7693c17e2cf40b",
  cumulativeGasUsed: 113558,
  from: "0x2a966a87db5913c1b22a59b0d8a11cc51c167a89",
  gasUsed: 113558,
  logs: [],
  logsBloom: \
    "0x00000000000000000000000000000000000000000000000000...00000",
  status: "0x1",
  to: null,
  transactionHash: \
    "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b",
  transactionIndex: 0
}

Cela inclut l’adresse du contrat, que nous pouvons utiliser pour envoyer et recevoir des fonds du contrat, comme indiqué dans la section précédente :

> contract_address = "0xb226270965b43373e98ffc6e2c7693c17e2cf40b"
> web3.eth.sendTransaction({from: src, to: contract_address, \
  value: web3.utils.toWei(0.1, "ether"), data: ""});

"0x6ebf2e1fe95cc9c1fe2e1a0dc45678ccd127d374fdf145c5c8e6cd4ea2e6ca9f"

> web3.eth.sendTransaction({from: src, to: contract_address, value: 0, data: \
  "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000"});

"0x59836029e7ce43e92daf84313816ca31420a76a9a571b69e31ec4bf4b37cd16e"

Après un certain temps, les deux transactions sont visibles sur Etherscan, comme indiqué dans Etherscan montrant les transactions d’envoi et de réception de fonds.

Etherscan affichant les transactions d’envoi et de réception de fonds
Figure 36. Etherscan montrant les transactions d’envoi et de réception de fonds

Signatures numériques

Jusqu’à présent, nous n’avons pas approfondi les détails sur les signatures numériques. Dans cette section, nous examinons le fonctionnement des signatures numériques et comment elles peuvent être utilisées pour présenter une preuve de propriété d’une clé privée sans révéler cette clé privée.

L’algorithme de signature numérique à courbe elliptique

L’algorithme de signature numérique utilisé dans Ethereum est l'Elliptic Curve Digital Signature Algorithm ( ECDSA). Il est basé sur des paires de clés privées-publiques à courbe elliptique, comme décrit dans La cryptographie à courbe elliptique expliquée.

Une signature numérique sert trois objectifs dans Ethereum (voir l’encadré suivant). Premièrement, la signature prouve que le propriétaire de la clé privée, qui est implicitement le propriétaire d’un compte Ethereum, a autorisé la dépense d’ether ou l’exécution d’un contrat. Deuxièmement, il garantit la non-répudiation : la preuve de l’autorisation est indéniable. Troisièmement, la signature prouve que les données de transaction n’ont pas été et ne peuvent pas être modifiées par quiconque après la signature de la transaction.

Définition de Wikipedia d’une signature numérique

Une signature numérique est un schéma mathématique permettant de présenter l’authenticité de messages ou de documents numériques. Une signature numérique valide donne au destinataire des raisons de croire que le message a été créé par un expéditeur connu (authentification), que l’expéditeur ne peut pas nier avoir envoyé le message (non-répudiation) et que le message n’a pas été altéré en transit (intégrité) .

Comment fonctionnent les signatures numériques

Une signature numérique est un schéma mathématique composé de deux parties. La première partie est un algorithme de création d’une signature, à l’aide d’une clé privée (la clé de signature), à partir d’un message (qui dans notre cas est la transaction). La deuxième partie est un algorithme qui permet à quiconque de vérifier la signature en utilisant uniquement le message et une clé publique.

Création d’une signature numérique

Dans l’implémentation ECDSA d’Ethereum, le "message" signé est la transaction , ou plus précisément, le hachage Keccak-256 des données encodées RLP de la transaction. La clé de signature est la clé privée de l’EOA. Le résultat est la signature :

S i g = F sig ( F keccak256 ( m ) , k )

où :

  • k est la clé privée de signature.

  • m est la transaction codée RLP.

  • Fkeccak256 est la fonction de hachage Keccak-256.

  • Fsig est l’algorithme de signature.

  • Sig est la signature résultante.

La fonction Fsig produit une signature Sig composée de deux valeurs, communément appelées r et s :

S i g = ( r , s )

Vérification de la signature

Pour vérifier la signature, il faut avoir la signature (r et s), la transaction sérialisée et la clé publique qui correspond à la clé privée utilisée pour créer la signature. Essentiellement, la vérification d’une signature signifie que "seul le propriétaire de la clé privée qui a généré cette clé publique pourrait avoir produit cette signature sur cette transaction".

L’algorithme de vérification de signature prend le message (c’est-à-dire, un hachage de la transaction pour notre usage), la clé publique du signataire et la signature (valeurs r et s), et renvoie true si la signature est valide pour ce message et la clé publique .

Mathématiques ECDSA

Comme mentionné précédemment, les signatures sont créées par une fonction mathématique Fsig qui produit une signature composée de deux valeurs, r et s. Dans cette section, nous examinons la fonction Fsig plus en détail.

L’algorithme de signature génère d’abord une clé privée éphémère (temporaire) de manière cryptographiquement sécurisée. Cette clé temporaire est utilisée dans le calcul des valeurs r et s pour s’assurer que la clé privée réelle de l’expéditeur ne peut pas être calculée par des attaquants qui surveillent les transactions signées sur le réseau Ethereum.

Comme nous le savons depuis Clés publiques, la clé privée éphémère est utilisée pour dériver la clé publique (éphémère) correspondante, nous avons donc :

  • Un nombre aléatoire cryptographiquement sécurisé q, qui est utilisé comme clé privée éphémère

  • La clé publique éphémère correspondante Q, générée à partir de q et du point générateur de courbe elliptique G

La valeur r de la signature numérique est alors la coordonnée x de la clé publique éphémère Q.

A partir de là, l’algorithme calcule la valeur s de la signature, telle que :

  • sq-1 (Keccak256(m) + r * k)     (mod p)

où :

  • q est la clé privée éphémère.

  • r est la coordonnée x de la clé publique éphémère.

  • k est la clé privée de signature (propriétaire EOA).

  • m est les données de transaction.

  • p est l’ordre premier de la courbe elliptique.

La vérification est l’inverse de la fonction de génération de signature, utilisant les valeurs r et s et la clé publique de l’expéditeur pour calculer une valeur Q, qui est un point sur la courbe elliptique (la clé publique éphémère utilisée dans la création de la signature). Les étapes sont les suivantes:

  1. Vérifiez que toutes les entrées sont correctement formées

  2. Calculez w = s-1 mod p

  3. Calculez u1 = Keccak256(m) * w mod p

  4. Calculez u2 = r * w mod p

  5. Enfin, calculez le point sur la courbe elliptique Qu1 * G + u2 * K     (mod p)

où :

  • r et s sont les valeurs de signature.

  • K est la clé publique du signataire (propriétaire de l’EOA).

  • m est les données de transaction qui ont été signées.

  • G est le point générateur de la courbe elliptique.

  • p est l’ordre premier de la courbe elliptique.

Si la coordonnée x du point calculé Q est égale à r, alors le vérificateur peut conclure que la signature est valide.

Notez qu’en vérifiant la signature, la clé privée n’est ni connue ni révélée.

Tip

ECDSA est nécessairement un calcul assez compliqué ; une explication complète est au-delà de la portée de ce livre. Un certain nombre d’excellents guides en ligne vous guident étape par étape : recherchez "ECDSA expliqué" ou essayez celui-ci : http://bit.ly/2r0HhGB.

Signature de transaction en pratique

Pour produire une transaction valide, l’expéditeur doit signer numériquement le message, en utilisant l’algorithme de signature numérique à courbe elliptique. Lorsque nous disons "signer la transaction", nous entendons en fait "signer le hachage Keccak-256 des données de transaction sérialisées RLP". La signature est appliquée au hachage des données de transaction, pas à la transaction elle-même.

Pour signer une transaction dans Ethereum, l’initiateur doit :

  1. Créez une structure de données de transaction contenant neuf champs : nonce, gasPrice, gasLimit, to, value, data, chainID, 0, 0.

  2. Produire un message sérialisé codé RLP de la structure de données de transaction.

  3. Calculez le hachage Keccak-256 de ce message sérialisé.

  4. Calculez la signature ECDSA, en signant le hachage avec la clé privée de l’EOA d’origine.

  5. Ajoutez les valeurs v, r et s calculées de la signature ECDSA à la transaction.

La variable de signature spéciale v indique deux choses : l’ID (identifiant) de chaîne et l’identifiant de récupération pour aider la fonction ECDSArecover à vérifier la signature. Il est calculé comme l’un des 27 ou 28, ou comme l’ID de chaîne doublé plus 35 ou 36. Pour plus d’informations sur l’ID de chaîne, voir Création de transactions brutes avec EIP-155. L’identifiant de récupération (27 ou 28 dans les signatures "à l’ancienne", ou 35 ou 36 dans les transactions complètes de type Spurious Dragon) est utilisé pour indiquer la parité du composant y de la clé publique (voir La valeur du préfixe de signature (v) et la récupération de la clé publique pour plus de détails).

Note

Au bloc #2 675 000, Ethereum a implémenté l’embranchement divergent "Spurious Dragon", qui, entre autres changements, a introduit un nouveau schéma de signature qui inclut la protection contre la relecture des transactions (empêchant les transactions destinées à un réseau d’être rejouées sur autres). Ce nouveau schéma de signature est spécifié dans EIP-155. Ce changement affecte la forme de la transaction et sa signature, il faut donc prêter attention à la première des trois variables de signature (c’est-à-dire v), qui prend l’une des deux formes et indique les champs de données inclus dans le message de transaction haché .

Création et signature de transactions brutes

Dans cette section, nous allons créer une transaction brute et la signer, en utilisant la bibliothèque ethereumjs-tx, qui peut être installé avec npm. Cela démontre les fonctions qui seraient normalement utilisées dans un portefeuille ou une application qui signe des transactions au nom d’un utilisateur. Le code source de cet exemple se trouve dans le fichier raw_tx_demo.js dans le référentiel GitHub du livre :

// Load requirements first:
//
// npm init
// npm install ethereumjs-tx
//
// Run with: $ node raw_tx_demo.js
const ethTx = require('ethereumjs-tx').Transaction;

const txData = {
  nonce: '0x0',
  gasPrice: '0x09184e72a000',
  gasLimit: '0x30000',
  to: '0xb0920c523d582040f2bcb1bd7fb1c7c1ecebdb34',
  value: '0x00',
  data: '',
  v: "0x1c", // Ethereum mainnet chainID
  r: 0,
  s: 0 
};

tx = new ethTx(txData);
console.log('RLP-Encoded Tx: 0x' + tx.serialize().toString('hex'))

txHash = tx.hash(); // Cette étape encode en RLP et calcule le hachage
console.log('Tx Hash: 0x' + txHash.toString('hex'))

// Signer la transaction
const privKey = Buffer.from(
    '91c8360c4cb4b5fac45513a7213f31d4e4a7bfcb4630e9fbf074f42a203ac0b9', 'hex');
tx.sign(privKey);

serializedTx = tx.serialize();
rawTx = 'Signed Raw Transaction: 0x' + serializedTx.toString('hex');
console.log(rawTx)

L’exécution de l’exemple de code produit les résultats suivants :

$ node raw_tx_demo.js
RLP-Encoded Tx: 0xe6808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1...
Tx Hash: 0xaa7f03f9f4e52fcf69f836a6d2bbc7706580adce0a068ff6525ba337218e6992
Signed Raw Transaction: 0xf866808609184e72a0008303000094b0920c523d582040f2bcb1...

Création de transactions brutes avec EIP-155

La norme EIP-155 "Simple Replay Attack Protection" spécifie un codage de transaction protégé contre les attaques de relecture, qui inclut un identifiant de chaîne dans les données de transaction, avant la signature. Cela garantit que les transactions créées pour une chaîne de blocs (par exemple, le réseau principal Ethereum) sont invalides sur une autre chaîne de blocs (par exemple, Ethereum Classic ou le réseau de test Ropsten). Par conséquent, les transactions diffusées sur un réseau ne peuvent pas être rejouées sur un autre, d’où le nom de la norme.

EIP-155 ajoute trois champs aux six principaux champs de la structure de données de transaction, à savoir l’identifiant de chaîne, 0 et 0. Ces trois champs sont ajoutés aux données de transaction avant qu’elles ne soient encodées et hachées. Ils modifient donc le hash de la transaction, auquel la signature est ensuite appliquée. En incluant l’identifiant de la chaîne dans les données à signer, la signature de la transaction empêche tout changement, car la signature est invalidée si l’identifiant de la chaîne est modifié. Par conséquent, EIP-155 rend impossible la relecture d’une transaction sur une autre chaîne, car la validité de la signature dépend de l’identifiant de la chaîne.

Le champ d’identifiant de chaîne prend une valeur en fonction du réseau auquel la transaction est destinée, comme indiqué dans Identifiants de chaîne.

Table 7. Identifiants de chaîne
Chaîne ID de chaîne

Mainnet Ethereum

1

Morden (obsolète), Étendue

2

Ropsten

3

Rinkeby

4

Mainnet Rootstock

30

Testnet Rootstock

31

Kovan

42

Mainnet Ethereum Classic

61

Testnet Ethereum Classic

62

Testets privés Geth

1337

La structure de transaction résultante est codée RLP, hachée et signée. L’algorithme de signature est légèrement modifié pour encoder également l’identifiant de la chaîne dans le préfixe v.

Pour plus de détails, voir la spécification EIP-155.

La valeur du préfixe de signature (v) et la récupération de la clé publique

Comme mentionné dans La structure d’une transaction, le message de transaction n’inclut pas de champ "from". En effet, la clé publique de l’expéditeur peut être calculée directement à partir de la signature ECDSA. Une fois que vous avez la clé publique, vous pouvez facilement calculer l’adresse. Le processus de récupération de la clé publique du signataire est appelé récupération de la clé publique.

Étant donné les valeurs r et s qui ont été calculées dans Mathématiques ECDSA, nous pouvons calculer deux clés publiques possibles.

Tout d’abord, nous calculons deux points de courbe elliptique, R et R', à partir de la coordonnée x r valeur qui se trouve dans la signature. Il y a deux points parce que la courbe elliptique est symétrique sur l’axe des x, de sorte que pour toute valeur x il y a deux valeurs possibles qui correspondent à la courbe, une de chaque côté de l’axe des x.

À partir de r, nous calculons également r-1, qui est l’inverse multiplicatif de r.

Enfin, nous calculons z, qui correspond aux n bits les plus bas du hachage du message, où n est l’ordre de la courbe elliptique.

Les deux clés publiques possibles sont alors :

  • K1 = r–1 (sRzG)

et:

  • K2 = r–1 (sR'zG)

où :

  • K1 et K2 sont les deux possibilités pour la clé publique du signataire.

  • r-1 est l’inverse multiplicatif de la valeur r de la signature.

  • s est la valeur s de la signature.

  • R et R' sont les deux possibilités pour la clé publique éphémère Q.

  • z correspond aux n bits les plus bas du hachage du message.

  • G est le point générateur de la courbe elliptique.

Pour rendre les choses plus efficaces, la signature de la transaction inclut une valeur de préfixe v, qui nous indique laquelle des deux valeurs R possibles est la clé publique éphémère. Si v est pair, alors R est la valeur correcte. Si v est impair, alors c’est R'. De cette façon, nous devons calculer une seule valeur pour R et une seule valeur pour K.

Séparer la signature et la transmission (signature hors ligne)

Une fois qu’une transaction est signée, elle est prête à être transmise au réseau Ethereum . Les trois étapes de création, de signature et de diffusion d’une transaction se déroulent normalement en une seule opération, par exemple en utilisant web3.eth.sendTransaction. Cependant, comme vous l’avez vu dans Création et signature de transactions brutes, vous pouvez créer et signer la transaction en deux étapes distinctes. Une fois que vous avez une transaction signée, vous pouvez ensuite la transmettre à l’aide de web3.eth.sendSignedTransaction, qui prend une transaction codée en hexadécimal et signée et la transmet sur le réseau Ethereum.

Pourquoi voudriez-vous séparer la signature et la transmission des transactions ? La raison la plus courante est la sécurité. L’ordinateur qui signe une transaction doit avoir des clés privées déverrouillées chargées en mémoire. L’ordinateur qui effectue la transmission doit être connecté à Internet (et exécuter un client Ethereum). Si ces deux fonctions sont sur un seul ordinateur, alors vous avez des clés privées sur un système en ligne, ce qui est assez dangereux. Séparer les fonctions de signature et de transmission et les exécuter sur différentes machines (sur un appareil hors ligne et en ligne, respectivement) est appelé signature hors ligne et est une pratique de sécurité courante.

  1. Créez une transaction non signée sur l’ordinateur en ligne où l’état actuel du compte, notamment le nonce actuel et les fonds disponibles, peut être récupéré.

  2. Transférez la transaction non signée vers un appareil hors ligne « isolé » pour la signature de la transaction, par exemple via un code QR ou une clé USB.

  3. Transmettez (retour) la transaction signée à un appareil en ligne pour diffusion sur la chaîne de blocs Ethereum, par exemple via un code QR ou une clé USB.

Signature hors ligne des transactions Ethereum
Figure 37. Signature hors ligne des transactions Ethereum

Selon le niveau de sécurité dont vous avez besoin, votre ordinateur de "signature hors ligne" peut avoir différents degrés de séparation de l’ordinateur en ligne, allant d’un sous-réseau isolé et protégé par un pare-feu (en ligne mais séparé) à un système complètement hors ligne connu sous le nom de système air-gapped ou à espacement d’air . Dans un système à espacement d’air, il n’y a aucune connectivité réseau - l’ordinateur est séparé de l’environnement en ligne par un espace "d’air". Pour signer des transactions, vous les transférez vers et depuis l’ordinateur isolé à l’aide d’un support de stockage de données ou (mieux) d’une webcam et d’un code QR. Bien sûr, cela signifie que vous devez transférer manuellement chaque transaction que vous souhaitez signer, et cela n’évolue pas.

Bien que peu d’environnements puissent utiliser un système entièrement isolé, même un faible degré d’isolement présente des avantages significatifs en matière de sécurité. Par exemple, un sous-réseau isolé avec un pare-feu qui n’autorise qu’un protocole de file d’attente de messages peut offrir une surface d’attaque très réduite et une sécurité bien plus élevée que la signature sur le système en ligne. De nombreuses entreprises utilisent un protocole tel que ZeroMQ (0MQ) à cette fin. Avec une telle configuration, les transactions sont sérialisées et mises en file d’attente pour signature. Le protocole de mise en file d’attente transmet le message sérialisé, d’une manière similaire à un socket TCP, à l’ordinateur signataire. L’ordinateur signataire lit les transactions sérialisées à partir de la file d’attente (avec précaution), applique une signature avec la clé appropriée et les place dans une file d’attente sortante. La file d’attente sortante transmet les transactions signées à un ordinateur avec un client Ethereum qui les retire de la file d’attente et les transmet.

Propagation des transactions

Le réseau Ethereum utilise un protocole de "routage par inondation". Chaque client Ethereum agit comme un nœud dans un réseau pair à pair (P2P), qui (idéalement) forme un réseau de toile. Aucun nœud de réseau n’est spécial : ils agissent tous comme des pairs égaux. Nous utiliserons le terme "nœud" pour désigner un client Ethereum qui est connecté et participe au réseau P2P.

La propagation de la transaction commence par la création (ou la réception hors ligne) par le nœud Ethereum d’origine d’une transaction signée. La transaction est validée puis transmise à tous les autres nœuds Ethereum qui sont directement connectés au nœud d’origine. En moyenne, chaque nœud Ethereum maintient des connexions avec au moins 13 autres nœuds, appelés ses voisins. Chaque nœud voisin valide la transaction dès qu’il la reçoit. S’ils conviennent qu’il est valide, ils en stockent une copie et le propagent à tous leurs voisins (sauf celui dont il provient). En conséquence, la transaction se répercute vers l’extérieur à partir du nœud d’origine, inondant à travers le réseau, jusqu’à ce que tous les nœuds du réseau aient une copie de la transaction. Les nœuds peuvent filtrer les messages qu’ils propagent, mais la valeur par défaut consiste à propager tous les messages de transaction valides qu’ils reçoivent.

En quelques secondes seulement, une transaction Ethereum se propage à tous les nœuds Ethereum du monde entier. Du point de vue de chaque nœud, il n’est pas possible de discerner l’origine de la transaction. Le voisin qui l’a envoyé au nœud peut être l’initiateur de la transaction ou peut l’avoir reçue de l’un de ses voisins. Pour pouvoir suivre l’origine des transactions ou interférer avec la propagation, un attaquant devrait contrôler un pourcentage significatif de tous les nœuds. Cela fait partie de la conception de la sécurité et de la confidentialité des réseaux P2P, en particulier lorsqu’elle est appliquée aux réseaux chaîne de blocs.

Enregistrement sur la chaîne de blocs

Bien que tous les nœuds d’Ethereum soient des pairs égaux, certains d’entre eux sont exploités par des mineurs et alimentent des transactions et des blocs vers des fermes minières, qui sont des ordinateurs dotés d’unités de traitement graphique (GPU) hautes performances. Les ordinateurs de minage ajoutent des transactions à un bloc candidat et tentent de trouver une preuve de travail qui rend le bloc candidat valide. Nous en discuterons plus en détail dans Consensus.

Sans entrer dans trop de détails, les transactions valides seront éventuellement incluses dans un bloc de transactions et, ainsi, enregistrées dans la chaîne de blocs Ethereum. Une fois minées dans un bloc, les transactions modifient également l’état du singleton Ethereum, soit en modifiant le solde d’un compte (dans le cas d’un simple paiement) soit en invoquant des contrats qui changent leur état interne. Ces modifications sont enregistrées parallèlement à la transaction, sous la forme d’un reçu de transaction, qui peut également inclure des événements. Nous examinerons tout cela beaucoup plus en détail dans La machine virtuelle Ethereum.

Une transaction qui a terminé son parcours depuis la création jusqu’à la signature par un EOA, la propagation et enfin l’exploitation minière a changé l’état du singleton et laissé une marque indélébile sur la chaîne de blocs.

Transactions à signatures multiples (Multisig)

Si vous connaissez les capacités de script de Bitcoin, vous savez qu’il est possible de créer un compte multisig Bitcoin qui ne peut dépenser des fonds que lorsque plusieurs parties signent la transaction (par exemple, 2 sur 2 ou 3 sur 4 signatures). Les transactions de valeur EOA de base d’Ethereum ne comportent aucune disposition pour les signatures multiples ; cependant, des restrictions de signature arbitraires peuvent être appliquées par des contrats intelligents avec toutes les conditions auxquelles vous pouvez penser, pour gérer le transfert d’ether et de jetons.

Pour tirer parti de cette capacité, l’ether doit être transféré dans un "contrat de portefeuille" programmé avec les règles de dépenses souhaitées, telles que les exigences de multisignature ou les limites de dépenses (ou des combinaisons des deux). Le contrat de portefeuille envoie ensuite les fonds lorsqu’il y est invité par un EOA autorisé une fois que les conditions de dépenses ont été remplies. Par exemple, pour protéger votre ether sous une condition multisig, transférez l’ether vers un contrat multisig. Chaque fois que vous souhaitez envoyer des fonds vers un autre compte, tous les utilisateurs requis devront envoyer des transactions au contrat à l’aide d’une application de portefeuille standard, autorisant effectivement le contrat à effectuer en finale la transaction.

Ces contrats peuvent également être conçus pour exiger plusieurs signatures avant d’exécuter du code local ou pour déclencher d’autres contrats. La sécurité du système est finalement déterminée par le code de contrat multisig.

La possibilité de mettre en œuvre des transactions multisignatures en tant que contrat intelligent démontre la flexibilité d’Ethereum. Cependant, c’est une épée à double tranchant, car la flexibilité supplémentaire peut entraîner des bogues qui compromettent la sécurité des schémas multisignatures. Il existe, en fait, un certain nombre de propositions pour créer une commande multisignature dans l’EVM qui supprime le besoin de contrats intelligents, du moins pour les simples schémas multisignatures M-de-N. Cela équivaudrait au système multisignature de Bitcoin, qui fait partie des règles de consensus de base et s’est avéré robuste et sécurisé.

Conclusion

Les transactions sont le point de départ de chaque activité dans le système Ethereum. Les transactions sont les "entrées" qui amènent la machine virtuelle Ethereum à évaluer les contrats, à mettre à jour les soldes et, plus généralement, à modifier l’état de la chaîne de blocs Ethereum. Ensuite, nous travaillerons avec des contrats intelligents de manière beaucoup plus détaillée et apprendrons à programmer dans le langage orienté contrat de Solidity.

Contrats intelligents et Solidity

Bannière Amazon du livre Maîtriser Ethereum

Comme nous en avons discuté dans Les bases d’Ethereum, il existe deux types de comptes différents dans Ethereum : les comptes externes (EOA) et comptes contractuels. Les EOA sont contrôlés par les utilisateurs, souvent via un logiciel tel qu’une application de portefeuille externe à la plate-forme Ethereum. contrats ») qui est exécuté par la machine virtuelle Ethereum. En bref, les EOA sont de simples comptes sans code ni stockage de données associés, tandis que les comptes contractuels ont à la fois un code et un stockage de données associés. Les EOA sont contrôlés par des transactions créées et signées cryptographiquement avec une clé privée dans le "monde réel" externe et indépendant du protocole, alors que les comptes contractuels n’ont pas de clés privées et "se contrôlent" donc de la manière prédéterminée prescrite par leur code de contrat intelligent. Les deux types de comptes sont identifiés par une adresse Ethereum. Dans ce chapitre, nous discuterons des comptes contractuels et du code de programme qui les contrôle.

Qu’est-ce qu’un contrat intelligent ?

Le terme contrat intelligent a été utilisé au fil des ans pour décrire une grande variété de choses différentes. Dans les années 1990, le cryptographe Nick Szabo a inventé le terme et l’a défini comme "un ensemble de promesses, spécifiées sous forme numérique, y compris des protocoles au sein que les parties exécutent sur les autres promesses. Depuis lors, le concept de contrats intelligents a évolué, notamment après l’introduction de plateformes de chaîne de blocs décentralisées avec l’invention de Bitcoin en 2009. Dans le contexte d’Ethereum, le terme est en fait un peu impropre, étant donné que les contrats intelligents Ethereum ne sont ni des contrats intelligents ni légaux, mais le terme est resté. Dans ce livre, nous utilisons le terme «contrats intelligents» pour désigner des programmes informatiques immuables qui s’exécutent de manière déterministe dans le contexte d’une machine virtuelle Ethereum dans le cadre du protocole réseau Ethereum, c’est-à-dire sur l’ordinateur mondial Ethereum décentralisé.

Déballons cette définition :

Logiciels d’ordinateur

Les contrats intelligents sont simplement des programmes informatiques. Le mot « contrat » n’a aucune signification juridique dans ce contexte.

Immuable

Une fois déployé, le code d’un contrat intelligent ne peut pas changer. Contrairement aux logiciels traditionnels, la seule façon de modifier un contrat intelligent est de déployer une nouvelle instance.

Déterministe

Le résultat de l’exécution d’un contrat intelligent est le même pour tous ceux qui l’exécutent, compte tenu du contexte de la transaction qui a initié son exécution et de l’état de la chaîne de blocs Ethereum au moment de l’exécution.

Contexte EVM

Les contrats intelligents fonctionnent avec un contexte d’exécution très limité. Ils peuvent accéder à leur propre état, au contexte de la transaction qui les a appelés et à certaines informations sur les blocs les plus récents.

Ordinateur mondial décentralisé

L’EVM fonctionne comme une instance locale sur chaque nœud Ethereum, mais comme toutes les instances de l’EVM fonctionnent sur le même état initial et produisent le même état final, le système dans son ensemble fonctionne comme un seul "ordinateur mondial".

Cycle de vie d’un contrat intelligent

Les contrats intelligents sont généralement écrits dans un langage de haut niveau, tel que Solidity. Mais pour fonctionner, ils doivent être compilés dans le code intermédiaire (bytecode) de bas niveau qui s’exécute dans l’EVM. Une fois compilés, ils sont déployés sur la plateforme Ethereum à l’aide d’une transaction spéciale de création de contrat, qui est identifiée comme telle en étant envoyée à l’adresse spéciale de création de contrat, à savoir 0x0 (voir Transaction spéciale : création de contrat). Chaque contrat est identifié par une adresse Ethereum, qui est dérivée de la transaction de création de contrat en fonction du compte d’origine et du nonce. L’adresse Ethereum d’un contrat peut être utilisée dans une transaction en tant que destinataire, envoyer des fonds au contrat ou appeler l’une des fonctions du contrat. Notez que, contrairement aux EOA, il n’y a pas de clés associées à un compte créé pour un nouveau contrat intelligent. En tant que créateur du contrat, vous n’obtenez aucun privilège spécial au niveau du protocole (bien que vous puissiez les coder explicitement dans le contrat intelligent). Vous ne recevez certainement pas la clé privée du compte de contrat, qui en fait n’existe pas - nous pouvons dire que les comptes de contrat intelligents se possèdent eux-mêmes.

Il est important de noter que les contrats ne s’exécutent que s’ils sont appelés par une transaction. Tous les contrats intelligents dans Ethereum sont exécutés, en fin de compte, en raison d’une transaction initiée à partir d’un EOA. Un contrat peut appeler un autre contrat qui peut appeler un autre contrat, et ainsi de suite, mais le premier contrat d’une telle chaîne d’exécution aura toujours été appelé par une transaction d’un EOA. Les contrats ne fonctionnent jamais "seuls" ou "en arrière-plan". Les contrats restent effectivement inactifs jusqu’à ce qu’une transaction déclenche l’exécution, soit directement, soit indirectement dans le cadre d’une chaîne d’appels de contrats. Il convient également de noter que les contrats intelligents ne sont en aucun cas exécutés "en parallèle" - l’ordinateur mondial Ethereum peut être considéré comme une machine à un seul fil d’exécution (thread).

Les transactions sont atomique, quel que soit le nombre de contrats qu’elles appellent ou ce que ces contrats font lorsqu’ils sont appelés. Les transactions s’exécutent dans leur intégralité, avec tout changement dans l’état global (contrats, comptes, etc.) enregistré uniquement si toutes les exécutions se terminent avec succès. Une fin réussie signifie que le programme s’est exécuté sans erreur et a atteint la fin de l’exécution. Si l’exécution échoue en raison d’une erreur, tous ses effets (changements d’état) sont "annulés" comme si la transaction n’avait jamais été exécutée. Une transaction échouée est toujours enregistrée comme ayant été tentée, et l’ether dépensé en gaz pour l’exécution est déduit du compte d’origine, mais cela n’a par ailleurs aucun autre effet sur le contrat ou l’état du compte.

Comme mentionné précédemment, il est important de se rappeler que le code d’un contrat ne peut pas être modifié. Cependant, un contrat peut être "supprimé", supprimant le code et son état interne (stockage) de son adresse, laissant un compte vide. Toutes les transactions envoyées à cette adresse de compte après la suppression du contrat n’entraînent aucune exécution de code, car il n’y a plus de code à exécuter. Pour supprimer un contrat, vous exécutez un opcode EVM appelé SELFDESTRUCT (précédemment appelé SUICIDE). Cette opération coûte du "gaz négatif", un remboursement de gaz, incitant ainsi à libérer les ressources du client du réseau à partir de la suppression de l’état stocké. La suppression d’un contrat de cette manière ne supprime pas l’historique des transactions (passé) du contrat, car la chaîne de blocs elle-même est immuable. Il est également important de noter que la capacité SELFDESTRUCT ne sera disponible que si l’auteur du contrat a programmé le contrat intelligent pour avoir cette fonctionnalité. Si le code du contrat n’a pas d’opcode SELFDESTRUCT, ou s’il est inaccessible, le contrat intelligent ne peut pas être supprimé.

Introduction aux langages de haut niveau Ethereum

L’EVM est une machine virtuelle qui exécute une forme spéciale de code appelé code intermédiaire EVM ou EVM bytecode, analogue au processeur de votre ordinateur, qui exécute un code machine tel que x86_64. Nous examinerons le fonctionnement et le langage de l’EVM de manière beaucoup plus détaillée dans La machine virtuelle Ethereum. Dans cette section, nous verrons comment les contrats intelligents sont écrits pour s’exécuter sur l’EVM.

Bien qu’il soit possible de programmer des contrats intelligents directement en code intermédiaire, le code intermédiaire EVM est plutôt lourd et très difficile à lire et à comprendre pour les programmeurs. Au lieu de cela, la plupart des développeurs Ethereum utilisent un langage de haut niveau pour écrire des programmes et un compilateur pour les convertir en code intermédiaire.

Bien que n’importe quel langage de haut niveau puisse être adapté pour écrire des contrats intelligents, adapter un langage arbitraire pour qu’il soit compilable au code intermédiaire EVM est un exercice assez fastidieux et conduirait en général à une certaine confusion. Les contrats intelligents fonctionnent dans un environnement d’exécution très contraint et minimaliste (l’EVM). De plus, un ensemble spécial de variables système et de fonctions spécifiques à EVM doit être disponible. En tant que tel, il est plus facile de créer un langage de contrat intelligent à partir de zéro que de créer un langage à usage général adapté à l’écriture de contrats intelligents. En conséquence, un certain nombre de langages spéciaux ont émergé pour programmer des contrats intelligents. Ethereum possède plusieurs langages de ce type, ainsi que les compilateurs nécessaires pour produire un code intermédiaire exécutable EVM.

En général, les langages de programmation peuvent être classés en deux grands paradigmes de programmation : déclaratif et impératif, également appelés respectivement fonctionnel et procédural. En programmation déclarative, nous écrivons des fonctions qui expriment la logique d’un programme, mais pas son flux. La programmation déclarative est utilisée pour créer des programmes sans effets secondaires, ce qui signifie qu’il n’y a pas de changement d’état en dehors d’une fonction. Les langages de programmation déclaratifs incluent Haskell et SQL. La programmation impérative, en revanche, est l’endroit où un programmeur écrit un ensemble de procédures qui combinent la logique et le déroulement d’un programme. Les langages de programmation impératifs incluent C++ et Java. Certains langages sont « hybrides », ce qui signifie qu’ils encouragent la programmation déclarative mais peuvent également être utilisés pour exprimer un paradigme de programmation impérative. Ces hybrides incluent Lisp, JavaScript et Python. En général, n’importe quel langage impératif peut être utilisé pour écrire dans un paradigme déclaratif, mais il en résulte souvent un code inélégant. Par comparaison, les langages déclaratifs purs ne peuvent pas être utilisés pour écrire dans un paradigme impératif. Dans les langages purement déclaratifs, il n’y a pas de "variables".

Bien que la programmation impérative soit plus couramment utilisée par les programmeurs, il peut être très difficile d’écrire des programmes qui s’exécutent exactement comme prévu. La capacité de n’importe quelle partie du programme à modifier l’état de n’importe quelle autre rend difficile le raisonnement sur l’exécution d’un programme et introduit de nombreuses opportunités de bogues. La programmation déclarative, par comparaison, permet de comprendre plus facilement le comportement d’un programme : puisqu’elle n’a pas d’effets secondaires, n’importe quelle partie d’un programme peut être comprise isolément.

Dans les contrats intelligents, les bogues coûtent littéralement de l’argent. Par conséquent, il est extrêmement important de rédiger des contrats intelligents sans effets indésirables. Pour ce faire, vous devez être capable de raisonner clairement sur le comportement attendu du programme. Ainsi, les langages déclaratifs jouent un rôle beaucoup plus important dans les contrats intelligents que dans les logiciels à usage général. Néanmoins, comme vous le verrez, le langage le plus utilisé pour les contrats intelligents (Solidity) est impératif. Les programmeurs, comme la plupart des humains, résistent au changement !

Les langages de programmation de haut niveau actuellement pris en charge pour les contrats intelligents incluent (classés par âge approximatif) :

LLL

Un langage de programmation fonctionnel (déclaratif), avec une syntaxe de type Lisp. C’était le premier langage de haut niveau pour les contrats intelligents Ethereum, mais il est rarement utilisé aujourd’hui.

Serpent

Un langage de programmation procédural (impératif) avec une syntaxe similaire à Python. Peut également être utilisé pour écrire du code fonctionnel (déclaratif), bien qu’il ne soit pas entièrement exempt d’effets secondaires.

Solidity

Un langage de programmation procédural (impératif) avec une syntaxe similaire à JavaScript, C++, ou Java. Le langage le plus populaire et le plus fréquemment utilisé pour les contrats intelligents Ethereum.

Vyper

Un langage développé plus récemment, similaire à Serpent et encore une fois avec une syntaxe de type Python. Destiné à se rapprocher d’un langage de type Python purement fonctionnel que Serpent, mais pas à remplacer Serpent.

Bamboo

Un langage nouvellement développé, influencé par Erlang, avec des transitions d’état explicites et sans flux itératifs (boucles). Destiné à réduire les effets secondaires et à augmenter l’auditabilité. Très nouveau et en attente d’être largement adopté.

Comme vous pouvez le voir, il existe de nombreux languages parmi lesquelles choisir. Cependant, de tous, Solidity est de loin le plus populaire, au point d’être le langage de haut niveau de facto d’Ethereum et même d’autres chaînes de blocs de type EVM. Nous passerons la plupart de notre temps à utiliser Solidity, mais nous explorerons également certains des exemples dans d’autres langages de haut niveau pour comprendre leurs différentes philosophies.

Construire un contrat intelligent avec Solidity

Solidity a été créé par Dr. Gavin Wood (co-auteur de ce livre) en tant que langage explicite pour écrire des contrats intelligents avec des fonctionnalités permettant de prendre directement en charge l’exécution dans l’environnement décentralisé de l’ordinateur mondial Ethereum. Les attributs résultants sont assez généraux et ont donc fini par être utilisés pour coder des contrats intelligents sur plusieurs autres plateformes de chaîne de blocs. Il a été développé par Christian Reitiwessner puis également par Alex Beregszaszi, Liana Husikyan, Yoichi Hirai et plusieurs anciens contributeurs principaux d’Ethereum. Solidity est maintenant développé et maintenu en tant que projet indépendant sur GitHub.

Le "produit" principal du projet Solidity est le compilateur Solidity, solc, qui convertit les programmes écrits dans le langage Solidity en code intermédiaire EVM. Le projet gère également l’importante norme d’interface binaire d’application (ABI) pour les contrats intelligents Ethereum, que nous explorerons en détail dans ce chapitre. Chaque version du compilateur Solidity correspond à et compile une version spécifique de language de Solidity.

Pour commencer, nous allons télécharger un exécutable binaire du compilateur Solidity. Ensuite, nous développerons et compilerons un contrat simple, à la suite de l’exemple avec lequel nous avons commencé dans Les bases d’Ethereum.

Sélection d’une version de Solidity

Solidity suit un modèle de versionnage appelé versionnage de sémantique (semantic versioning), qui spécifie les numéros de version structurés comme trois nombres séparés par des points : MAJEUR.MINEUR.CORRECTIF. Le numéro "majeur" est incrémenté pour les modifications majeures et rétro-incompatibles, le numéro "mineur" est incrémenté lorsque des fonctionnalités rétrocompatibles sont ajoutées entre les versions majeures, et le numéro "correctif" est incrémenté pour les corrections de bogues rétrocompatibles.

Au moment de la rédaction, Solidity est à la version 0.4.24. Les règles de la version majeure 0, qui concerne le développement initial d’un projet, sont différentes : tout peut changer à tout moment. En pratique, Solidity traite le numéro "mineur" comme s’il s’agissait de la version majeure et le numéro de "correctif" comme s’il s’agissait de la version mineure. Par conséquent, dans la version 0.4.24, 4 est considérée comme la version majeure et 24 comme la version mineure.

La sortie de la version majeure 0.5 de Solidity est attendue de manière imminente.

Comme vous l’avez vu dans Les bases d’Ethereum, vos programmes Solidity peuvent contenir une directive pragma qui spécifie les versions minimale et maximale de Solidity avec lesquelles il est compatible, et peut être utilisée pour compiler votre contrat.

Étant donné que Solidity évolue rapidement, il est souvent préférable d’installer la dernière version.

Télécharger et installer

Il existe un certain nombre de méthodes que vous pouvez utiliser pour télécharger et installer Solidity, soit sous forme de version binaire, soit en compilant à partir du code source. Vous pouvez trouver des instructions détaillées dans http://bit.ly/2RrZmup [la documentation de Solidity].

Voici comment installer la dernière version binaire de Solidity sur un système d’exploitation Ubuntu/Debian, en utilisant le gestionnaire de paquets apt :

$ sudo add-apt-repository ppa:ethereum/ethereum
$ sudo apt update
$ sudo apt install solc

Une fois que vous avez installé solc, vérifiez la version en exécutant :

$ solc --version
solc, the solidity compiler commandline interface
Version: 0.4.24+commit.e67f0147.Linux.g++

Il existe plusieurs autres façons d’installer Solidity, en fonction de votre système d’exploitation et de vos exigences, y compris la compilation directe à partir du code source. Pour plus d’informations, consultez https://github.com/ethereum/solidity.

Environnement de développement

Pour développer dans Solidity, vous pouvez utiliser n’importe quel éditeur de texte et solc sur la ligne de commande. Cependant, vous constaterez peut-être que certains éditeurs de texte conçus pour le développement, tels que Emacs, Vim et Atom, offrent des fonctionnalités supplémentaires telles que la coloration syntaxique et les macros qui facilitent le développement de Solidity.

Il existe également des environnements de développement basés sur le Web, tels que Remix IDE et EthFiddle.

Utilisez les outils qui vous rendent productif. En fin de compte, les programmes Solidity ne sont que des fichiers texte. Bien que des éditeurs et des environnements de développement sophistiqués puissent faciliter les choses, vous n’avez besoin de rien de plus qu’un simple éditeur de texte, tel que nano (Linux/Unix), TextEdit (macOS) ou même NotePad (Windows). Enregistrez simplement le code source de votre programme avec une extension .sol et il sera reconnu par le compilateur Solidity comme un programme Solidity.

Écrire un programme Solidity simple

Dans Les bases d’Ethereum, nous avons écrit notre premier programme Solidity. Lorsque nous avons créé le contrat Faucet pour la première fois, nous avons utilisé l’IDE Remix pour compiler et déployer le contrat. Dans cette section, nous allons revisiter, améliorer et embellir Faucet.

Notre première tentative ressemblait à Faucet.sol : Un contrat Solidity mettant en place un robinet.

Example 2. Faucet.sol : Un contrat Solidity mettant en place un robinet
// Identifiant de licence SPDX : CC-BY-SA-4.0

// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity 0.6.4;

// Notre premier contrat est un faucet !
contract Faucet {
    // Accepte tout montant entrant
    receive() external payable {}

    // Donnez de l'éther à quiconque demande
    function withdraw(uint withdraw_amount) public {
        // Limiter le montant du retrait
        require(withdraw_amount <= 100000000000000000);

        // Envoie le montant à l'adresse qui l'a demandé
        msg.sender.transfer(withdraw_amount);
    }
}

Compiler avec le compilateur Solidity (solc)

Maintenant, nous allons utilisez le compilateur Solidity en ligne de commande pour compiler directement notre contrat. Le compilateur Solidity solc offre une variété d’options, que vous pouvez voir en passant l’argument --help.

Nous utilisons les arguments --bin et --optimize de solc pour produire un binaire optimisé de notre exemple de contrat :

$ solc --optimize --bin Faucet.sol
======= Faucet.sol:Faucet =======
Binary:
6060604052341561000f57600080fd5b60cf8061001d6000396000f300606060405260043610603e5
763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416
632e1a7d4d81146040575b005b3415604a57600080fd5b603e60043567016345785d8a00008111156
06357600080fd5b73ffffffffffffffffffffffffffffffffffffffff331681156108fc0282604051
600060405180830381858888f19350505050151560a057600080fd5b505600a165627a7a723058203
556d79355f2da19e773a9551e95f1ca7457f2b5fbbf4eacf7748ab59d2532130029

Le résultat que solc produit est un binaire sérialisé hexadécimal qui peut être soumis à la chaîne de blocs Ethereum.

L’ABI du contrat Ethereum

Dans les logiciels informatiques, une interface binaire d’application est une interface entre deux modules de programme ; souvent, entre le système d’exploitation et les programmes utilisateur. Un ABI définit la façon dont les structures de données et les fonctions sont accessibles en code machine ; cela ne doit pas être confondu avec une API, qui définit cet accès dans des formats de haut niveau, souvent lisibles par l’homme, en tant que code source. L’ABI est donc le principal moyen d’encoder et de décoder des données dans et hors du code machine.

Dans Ethereum, l’ABI est utilisé pour coder les appels de contrat pour l’EVM et pour lire les données des transactions. Le but d’un ABI est de définir les fonctions du contrat qui peuvent être invoquées et de décrire comment chaque fonction acceptera les arguments et renverra son résultat.

L’ABI d’un contrat est spécifié sous la forme d’un tableau JSON de descriptions de fonctions (voir Fonctions) et événements (voir Événements). Une description de fonction est un objet JSON avec les champs type, name, inputs, outputs, constant et payable. Un objet de description d’événement a des champs type, name, inputs et anonymous.

Nous utilisons le compilateur Solidity en ligne de commande solc pour produire l’ABI pour notre exemple de contrat Faucet.sol :

$ solc --abi Faucet.sol
======= Faucet.sol:Faucet =======
Contract JSON ABI
[{"constant":false,"inputs":[{"name":"withdraw_amount","type":"uint256"}], \
"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable", \
"type":"function"},{"payable":true,"stateMutability":"payable", \
"type":"fallback"}]

Comme vous pouvez le voir, le compilateur produit un tableau JSON décrivant les deux fonctions définies par Faucet.sol. Ce JSON peut être utilisé par toute application qui souhaite accéder au contrat Faucet une fois qu’il est déployé. À l’aide de l’ABI, une application telle qu’un portefeuille ou un navigateur DApp peut construire des transactions qui appellent les fonctions dans + Faucet + avec les arguments et types d’arguments corrects. Par exemple, un portefeuille saura que pour appeler la fonction withdraw, il devra fournir un argument uint256 nommé withdraw_amount. Le portefeuille pourrait inviter l’utilisateur à fournir cette valeur, puis à créer une transaction qui l’encode et exécute la fonction retirer.

Tout ce qui est nécessaire pour qu’une application interagisse avec un contrat est un ABI et l’adresse où le contrat a été déployé.

Sélection d’un compilateur Solidity et d’une version linguistique

Comme nous l’avons vu dans le code précédent, notre contrat Faucet se compile avec succès avec Solidity version 0.4.21. Et si nous avions utilisé une version différente du compilateur Solidity ? Le langage est toujours en constante évolution et les choses peuvent changer de manière inattendue. Notre contrat est assez simple, mais que se passerait-il si notre programme utilisait une fonctionnalité qui n’a été ajoutée que dans la version 0.4.19 de Solidity et que nous essayions de la compiler avec la version 0.4.18 ?

Pour résoudre ces problèmes, Solidity propose une directive du compilateur connue sous le nom de version pragma qui indique au compilateur que le programme attend un compilateur (et un langage) spécifique à une version. Regardons un exemple :

pragma solidity ^0.4.19;

Le compilateur Solidity lit le pragma de version et génère une erreur si la version du compilateur est incompatible avec le pragma de version. Dans ce cas, notre pragma de version indique que ce programme peut être compilé par un compilateur Solidity avec une version minimale de 0.4.19. Le symbole ^ indique, cependant, que nous autorisons la compilation avec toute révision mineure au-dessus de 0.4.19 ; par exemple, 0.4.20, mais pas 0.5.0 (qui est une révision majeure, pas une révision mineure). Les directives Pragma ne sont pas compilées dans le code intermédiaire EVM. Ils ne sont utilisés par le compilateur que pour vérifier la compatibilité.

Ajoutons une directive pragma à notre contrat Faucet. Nous nommerons le nouveau fichier Faucet2.sol, pour garder une trace de nos modifications au fur et à mesure que nous avancerons dans ces exemples en commençant par Faucet2.sol : Ajout du pragma de version à Faucet.

Example 3. Faucet2.sol : Ajout du pragma de version à Faucet
// Identifiant de licence SPDX : CC-BY-SA-4.0

// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.6.4;

// Notre premier contrat est un faucet !
contract Faucet {
    // Accepte tout montant entrant
    receive() external payable {}

    // Donnez de l'éther à quiconque demande
    function withdraw(uint withdraw_amount) public {
        // Limiter le montant du retrait
        require(withdraw_amount <= 100000000000000000);

        // Envoie le montant à l'adresse qui l'a demandé
        msg.sender.transfer(withdraw_amount);
    }
}

L’ajout d’un pragma de version est une bonne pratique, car il évite les problèmes liés aux versions incompatibles du compilateur et du langage. Nous explorerons d’autres bonnes pratiques et continuerons d’améliorer le contrat Faucet tout au long de ce chapitre.

Programmation avec Solidity

Dans cette section, nous examinerons certaines des fonctionnalités du langage Solidity. Comme nous l’avons mentionné dans Les bases d’Ethereum, notre premier exemple de contrat était très simple et également défectueux à plusieurs égards. Nous allons l’améliorer progressivement ici, tout en explorant comment utiliser Solidity. Cependant, ce ne sera pas un didacticiel complet sur Solidity, car Solidity est assez complexe et évolue rapidement. Nous couvrirons les bases et vous donnerons suffisamment de bases pour pouvoir explorer le reste par vous-même. La documentation de Solidity se trouve sur le site du projet.

Types de données

D’abord, examinons certains des types de données de base proposés dans Solidity :

Boolean (bool)

valeur booléenne, true ou false, avec les opérateurs logiques ! (not), &amp;&amp; (and), || (or), == (equal) et != (non égal).

Integer (int, uint)

Entiers signés (int)et non signés (uint), déclarés par incréments de 8 bits de int8 à uint256. Sans suffixe de taille, des quantités de 256 bits sont utilisées pour correspondre à la taille de mot de l’EVM.

Fixed point (fixed,ufixed)

Nombres à virgule fixe, déclarés avec ( u) fixed M x NM est la taille en bits (incréments de 8 jusqu’à 256) et N est le nombre de décimales après le point (jusqu’à 18) ; par exemple, ufixed32x2.

Address

Une adresse Ethereum de 20 octets. L’objet address a de nombreuses fonctions membres utiles, les principales étant balance (renvoie le solde du compte) et transfer (transfère l’ether au compte).

Byte array (fixe)

Tableaux d’octets de taille fixe, déclarés avec bytes1 jusqu’à bytes32.

Byte array (dynamique):: tableaux d’octets de taille variable, déclarés avec bytes ou string.

Enum

Type défini par l’utilisateur pour énumérer des valeurs discrètes : enum NAME {LABEL1, LABEL 2, ...}.

Arrays

Un tableau de n’importe quel type, fixe ou dynamique : uint32[][5] est un tableau de taille fixe de cinq tableaux dynamiques d’entiers non signés.

Struct

Conteneurs de données définis par l’utilisateur pour regrouper les variables : struct NAME {TYPE1 VARIABLE1; TYPE2 VARIABLE2; ...}.

Mapping

Tables de recherche de hachage pour les paires key =&gt; value : mapping(KEY_TYPE =&gt; VALUE_TYPE) NAME.

En plus de ces types de données, Solidity propose également une variété de valeurs littérales qui peuvent être utilisées pour calculer différentes unités :

Unités de temps

Les unités seconds, minutes, hours et days peuvent être utilisées comme suffixes, convertis en multiples de l’unité de base seconds.

Unités d’ether

Les unités wei, finney, szabo et ether peuvent être utilisées comme suffixes, convertis en multiples de l’unité de base wei.

Dans notre exemple de contrat Faucet, nous avons utilisé un uint (qui est un alias pour uint256)pour la variable withdraw_amount. Nous avons également utilisé indirectement une variable address, que nous avons définie avec msg.sender. Nous utiliserons plus de ces types de données dans nos exemples dans le reste de ce chapitre.

Utilisons l’un des multiplicateurs d’unités pour améliorer la lisibilité de notre exemple de contrat. Dans la fonction withdraw nous limitons le retrait maximum, en exprimant la limite en wei, l’unité de base de l’ether :

require(withdraw_amount <= 100000000000000000);

Ce n’est pas très facile à lire. Nous pouvons améliorer notre code en utilisant le multiplicateur d’unité ether, pour exprimer la valeur en ether au lieu de wei :

require(withdraw_amount <= 0.1 ether);

Variables et fonctions globales prédéfinies

Lorsqu’un contrat est exécuté dans l’EVM, il a accès à un petit ensemble d’objets globaux. Ceux-ci incluent les objets block, msg et tx. De plus, Solidity expose un certain nombre d’opcodes EVM en tant que fonctions prédéfinies. Dans cette section, nous examinerons les variables et les fonctions auxquelles vous pouvez accéder à partir d’un contrat intelligent dans Solidity.

Contexte d’appel de transaction/message

L’objet msg est l’appel de transaction (origine EOA) ou l’appel de message (origine contrat) qui lancé l’exécution de ce contrat. Il contient un certain nombre d’attributs utiles :

msg.sender

Nous avons déjà utilisé celui-ci. Il représente l’adresse qui a lancé cet appel de contrat, pas nécessairement l’EOA d’origine qui a envoyé la transaction. Si notre contrat a été appelé directement par une transaction EOA, alors c’est l’adresse qui a signé la transaction, mais sinon ce sera une adresse de contrat.

msg.value

La valeur d’ether envoyée avec cet appel (en wei).

msg.gas

La quantité de gaz restant dans l’approvisionnement en gaz de cet environnement d’exécution. Cela a été déprécié dans Solidity v0.4.21 et remplacé par la fonction gasleft.

msg.data

La charge utile de données de cet appel dans notre contrat.

msg.sig

Les quatre premiers octets de la charge utile de données, qui est le sélecteur de fonction.

Note

Chaque fois qu’un contrat appelle un autre contrat, les valeurs de tous les attributs de msg changent pour refléter les informations du nouvel appelant. La seule exception à cela est la fonction delegatecall, qui exécute le code d’un autre contrat/bibliothèque dans le contexte de msg.

Contexte transactionnel

L’objet tx fournit un moyen d’accéder aux informations relatives à la transaction :

tx.gasprice

Le prix du gaz dans la transaction d’appel.

tx.origin

L’adresse de l’EOA d’origine pour cette transaction. ATTENTION : dangereux !

Contexte de bloc

L’objet block contient des informations sur le bloc courant :

block.blockhash(__blockNumber__)

Le hachage de bloc du numéro de bloc spécifié, jusqu’à 256 blocs dans le passé. Obsolète et remplacé par la fonction blockhash dans Solidity v0.4.22.

block.coinbase

L’adresse du destinataire des frais du bloc actuel et de la récompense du bloc.

block.difficulty

La difficulté (preuve de travail) du bloc actuel.

block.gaslimit

La quantité maximale de gaz pouvant être dépensée pour toutes les transactions incluses dans le bloc actuel.

block.number

Le numéro de bloc actuel (hauteur de la chaîne de blocs).

block.timestamp

L’horodatage placé dans le bloc actuel par le mineur (nombre de secondes depuis l’époque Unix).

objet d’adresse

Toute adresse, transmise en tant qu’entrée ou extraite d’un objet de contrat, possède un certain nombre d’attributs et de méthodes :

address.balance

Le solde de l’adresse, en wei. Par exemple, le solde actuel du contrat est address(this).balance.

address.transfer(__amount__)

Transfère le montant (en wei) à cette adresse, en levant une exception en cas d’erreur. Nous avons utilisé cette fonction dans notre exemple Faucet comme méthode sur l’adresse msg.sender, comme msg.sender.transfer.

address.send(__amount__)

similaire à transfer, mais au lieu de lancer une exception, il renvoie false en cas d’erreur. ATTENTION : vérifiez toujours la valeur de retour de send.

address.call(__payload__)

fonction CALL de bas niveau — peut construire un appel de message arbitraire avec une charge utile de données. Renvoie faux en cas d’erreur. AVERTISSEMENT : dangereux : le destinataire peut (accidentellement ou par malveillance) utiliser tout votre gaz, provoquant l’arrêt de votre contrat avec une exception OOG ; vérifiez toujours la valeur de retour de call.

address.callcode(__payload__)

fonction CALLCODE de bas niveau, comme address(this).call(...) mais avec le code de ce contrat remplacé par celui de address. Renvoie faux en cas d’erreur. AVERTISSEMENT : utilisation avancée uniquement !

address.delegatecall()

Fonction DELEGATECALL de bas niveau, comme callcode(...) mais avec le contexte msg complet vu par le contrat actuel. Renvoie faux en cas d’erreur. AVERTISSEMENT : utilisation avancée uniquement !

Fonctions intégrées

Les autres fonctions à noter sont :

addmod, mulmod

Pour l’addition et la multiplication modulo. Par exemple, addmod(x,y,k) calcule (x + y) % k.

keccak256, sha256, sha3, ripemd160

Fonctions pour calculer les hachages avec divers algorithmes de hachage standard.

ecrecover

Récupère l’adresse utilisée pour signer un message à partir de la signature.

selfdestruct(__recipient_address__)

Supprime le contrat actuel, en envoyant tout ether restant dans le compte à l’adresse du destinataire.

this

L’adresse du compte de contrat en cours d’exécution.

Définition du contrat

Le type de données principal de Solidity est contract ; notre exemple Faucet définit simplement un objet contract. Semblable à tout objet dans un langage orienté objet, le contrat est un conteneur qui inclut des données et des méthodes.

Solidity propose deux autres types d’objets qui s’apparentent à un contrat :

interface

Une définition d’interface est structurée exactement comme un contrat, sauf qu’aucune des fonctions n’est définie, elles sont seulement déclarées. Ce type de déclaration est souvent appelé stub ou souche ; il vous indique les arguments des fonctions et les types de retour sans aucune implémentation. Une interface précise la « forme » d’un contrat ; lorsqu’elle est héritée, chacune des fonctions déclarées par l’interface doit être définie par l’enfant.

library

Un contrat de bibliothèque est un contrat destiné à être déployé une seule fois et utilisé par d’autres contrats, en utilisant la méthode delegatecall (voir objet d’adresse).

Fonctions

Au sein d’un contrat, on définit des fonctions qui peuvent être appelées par une transaction EOA ou un autre contrat. Dans notre exemple Faucet, nous avons deux fonctions : withdraw et la fonction (sans nom) fallback.

La syntaxe que nous utilisons pour déclarer une fonction dans Solidity est la suivante :

function FunctionName([parameters]) {public|private|internal|external}
[pure|constant|view|payable] [modifiers] [returns (return types)]

Examinons chacun de ces composants :

FunctionName

Le nom de la fonction, qui est utilisé pour appeler la fonction dans une transaction (à partir d’un EOA), à partir d’un autre contrat, ou même à partir du même contrat. Une fonction dans chaque contrat peut être définie sans nom, auquel cas c’est la fonction fallback, qui est appelée lorsqu’aucune autre fonction n’est nommée. La fonction de secours ne peut pas avoir d’arguments ni rien renvoyer.

parameters

Après le nom, nous spécifions les arguments qui doivent être passés à la fonction, avec leurs noms et leurs types. Dans notre exemple Faucet, nous avons défini uint remove_amount comme le seul argument de la fonction withdraw.

Le prochain ensemble de mots clés (public, private, internal, external) spécifie la visibilité de la fonction :

public

Public est la valeur par défaut ; ces fonctions peuvent être appelées par d’autres contrats ou transactions EOA, ou depuis le contrat. Dans notre exemple Faucet, les deux fonctions sont définies comme publiques.

external

Les fonctions externes sont comme des fonctions publiques, sauf qu’elles ne peuvent pas être appelées depuis le contrat à moins d’être explicitement préfixées avec le mot-clé this.

interne

Les fonctions internes ne sont accessibles qu’à partir du contrat - elles ne peuvent pas être appelées par un autre contrat ou une transaction EOA. Ils peuvent être appelés par des contrats dérivés (ceux qui héritent de celui-ci).

private

Les fonctions privées sont comme des fonctions internes mais ne peuvent pas être appelées par les contrats dérivés.

Gardez à l’esprit que les termes internal et private sont quelque peu trompeurs. Toute fonction ou donnée à l’intérieur d’un contrat est toujours visible sur la chaîne de blocs publique, ce qui signifie que n’importe qui peut voir le code ou les données. Les mots clés décrits ici n’affectent que comment et quand une fonction peut être appelée.

Le deuxième ensemble de mots clés (pure, constant, view, payable) affecte le comportement de la fonction :

constant ou view

Une fonction marquée comme view promet de ne modifier aucun état. Le terme constant est un alias pour la vue qui sera obsolète dans une future version. Pour le moment, le compilateur n’applique pas le modificateur view, ne produisant qu’un avertissement, mais cela devrait devenir un mot-clé appliqué dans la v0.5 de Solidity.

pure

Une fonction pure est une fonction qui ne lit ni écrit aucune variable dans le stockage. Il ne peut fonctionner que sur des arguments et renvoyer des données, sans référence à aucune donnée stockée. Les fonctions pures sont destinées à encourager la programmation de style déclaratif sans effets secondaires ni état.

payable

Une fonction payable est une fonction qui peut accepter des paiements entrants. Les fonctions non déclarées comme payable rejetteront les paiements entrants. Il existe deux exceptions, dues à des décisions de conception dans l’EVM : les paiements coinbase et l’héritage SELFDESTRUCT seront payés même si la fonction de secours n’est pas déclarée comme payable, mais cela a du sens car l’exécution de code ne fait pas partie de ces paiements de toute façon.

Comme vous pouvez le voir dans notre exemple Faucet, nous avons une fonction payante (la fonction de secours), qui est la seule fonction qui peut recevoir des paiements entrants.

Constructeur de contrat et autodestruction

Il existe une fonction spéciale qui n’est utilisée qu’une seule fois . Lorsqu’un contrat est créé, il exécute également la fonction constructor s’il en existe une, pour initialiser l’état du contrat. Le constructeur est exécuté dans la même transaction que la création du contrat. La fonction constructeur est facultative ; vous remarquerez que notre exemple Faucet n’en a pas.

Les constructeurs peuvent être spécifiés de deux manières. Jusqu’à et y compris dans Solidity v0.4.21, le constructeur est une fonction dont le nom correspond au nom du contrat, comme vous pouvez le voir ici :

contract MEContract {
	function MEContract() {
	// C'est le constructeur
	}
}

La difficulté avec ce format est que si le nom du contrat est modifié et que le nom de la fonction constructeur n’est pas modifié, ce n’est plus un constructeur. De même, s’il y a une faute de frappe accidentelle dans la dénomination du contrat et/ou du constructeur, la fonction n’est plus un constructeur. Cela peut provoquer des bogues assez désagréables, inattendus et difficiles à trouver. Imaginez par exemple si le constructeur met le propriétaire du contrat à des fins de contrôle. Si la fonction n’est pas réellement le constructeur en raison d’une erreur de nommage, non seulement le propriétaire ne sera pas défini au moment de la création du contrat, mais la fonction peut également être déployée en tant que partie permanente et "appelable" du contrat, comme un fonction normale, permettant à tout tiers de détourner le contrat et d’en devenir le "propriétaire" après la création du contrat.

Pour résoudre les problèmes potentiels avec les fonctions constructeur basées sur le fait d’avoir un nom identique à celui du contrat, Solidity v0.4.22 introduit un mot-clé constructor qui fonctionne comme une fonction constructeur mais n’a pas de nom. Renommer le contrat n’affecte en rien le constructeur. De plus, il est plus facile d’identifier quelle fonction est le constructeur. Il ressemble à ceci :

pragma ^0.4.22
contract MEContract {
	constructor () {
	// C'est le constructeur
	}
}

Pour résumer, le cycle de vie d’un contrat commence par une transaction de création à partir d’un compte EOA ou un contrat. S’il existe un constructeur, il est exécuté dans le cadre de la création du contrat, pour initialiser l’état du contrat lors de sa création, puis est supprimé.

L’autre la fin du cycle de vie du contrat est la destruction du contrat. Les contrats sont détruits par un opcode EVM spécial appelé SELFDESTRUCT. Il s’appelait autrefois SUICIDE, mais ce nom a été déprécié en raison des associations négatives du mot. Dans Solidity, cet opcode est exposé sous la forme d’une fonction intégrée de haut niveau appelée selfdestruct, qui prend un argument : l’adresse pour recevoir tout solde d’ether restant dans le compte du contrat. Il ressemble à ceci :

selfdestruct(address recipient);

Notez que vous devez explicitement ajouter cette commande à votre contrat si vous voulez qu’il soit supprimable - c’est la seule façon de supprimer un contrat, et il n’est pas présent par défaut. De cette manière, les utilisateurs d’un contrat qui pourraient s’attendre à ce qu’un contrat soit là pour toujours peuvent être certains qu’un contrat ne peut pas être supprimé s’il ne contient pas d’opcode SELFDESTRUCT.

Ajout d’un constructeur et autodestruction à notre exemple de robinet

L’exemple de contrat Faucet que nous avons introduit dans Les bases d’Ethereum n’a pas de constructeur ou de fonctions selfdestruct. C’est un contrat éternel qui ne peut pas être supprimé. Changeons cela en ajoutant un constructeur et une fonction selfdestruct. Nous voulons probablement que selfdestruct soit appelable uniquement par l’EOA qui a initialement créé le contrat. Par convention, ceci est généralement stocké dans une variable d’adresse appelée owner. Notre constructeur définit la variable owner, et la fonction selfdestruct vérifiera d’abord que le propriétaire l’a appelée directement.

Tout d’abord, notre constructeur :

// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.4.22;

// Notre premier contrat est un robinet !
contract Faucet {

	address owner;

	// Initialiser le contrat Faucet : définir le propriétaire
	constructor() {
		owner = msg.sender;
	}

[...]

Nous avons modifié la directive pragma pour spécifier la v0.4.22 comme version minimale pour cet exemple, car nous utilisons le nouveau mot-clé constructor introduit dans la v0.4.22 de Solidity. Notre contrat a maintenant une variable de type address nommée owner. Le nom "owner" n’est en aucun cas spécial. Nous pourrions appeler cette variable d’adresse "potato" et l’utiliser toujours de la même manière. Le nom owner rend simplement son objectif clair.

Ensuite, notre constructeur, qui s’exécute dans le cadre de la transaction de création de contrat, attribue l’adresse de msg.sender à la variable owner. Nous avons utilisé msg.sender dans la fonction remove pour identifier l’initiateur de la demande de withdraw. Dans le constructeur, cependant, le msg.sender est l’EOA ou l’adresse du contrat qui a initié la création du contrat. Nous savons que c’est le cas car il s’agit d’une fonction constructeur : elle ne s’exécute qu’une seule fois, lors de la création du contrat.

Nous pouvons maintenant ajouter une fonction pour détruire le contrat. Nous devons nous assurer que seul le propriétaire peut exécuter cette fonction, nous utiliserons donc une instruction require pour contrôler l’accès. Voici à quoi cela ressemblera :

// Destructeur de contrat
function destroy() public {
	require(msg.sender == owner);
	selfdestruct(owner);
}

Si quelqu’un appelle cette fonction destroy depuis une adresse autre que owner, elle échouera. Mais si la même adresse stockée dans owner par le constructeur l’appelle, le contrat s’autodétruira et enverra tout solde restant à l’adresse owner. Notez que nous n’avons pas utilisé le non sécurisé tx.origin pour déterminer si le propriétaire souhaitait détruire le contrat. L’utilisation de tx.origin permettrait à des contrats malveillants de détruire votre contrat sans votre permission.

Modificateurs de fonction

Solidity offre un type spécial de fonction appelé modificateur de fonction. Vous appliquez des modificateurs aux fonctions en ajoutant le nom du modificateur dans la déclaration de la fonction. Les modificateurs sont le plus souvent utilisés pour créer des conditions qui s’appliquent à de nombreuses fonctions au sein d’un contrat. Nous avons déjà une instruction de contrôle d’accès dans notre fonction destroy. Créons un modificateur de fonction qui exprime cette condition :

modifier onlyOwner {
    require(msg.sender == owner);
    _;
}

Ce modificateur de fonction, nommé onlyOwner, définit une condition sur toute fonction qu’il modifie, exigeant que l’adresse stockée en tant que owner du contrat soit la même que l’adresse du msg.sender de la transaction. Il s’agit du modèle de conception de base pour le contrôle d’accès, permettant uniquement au propriétaire d’un contrat d’exécuter toute fonction qui a le modificateur onlyOwner.

Vous avez peut-être remarqué que notre modificateur de fonction contient un "espace réservé" syntaxique particulier, un trait de soulignement suivi d’un point-virgule (_;). Cet espace réservé est remplacé par le code de la fonction en cours de modification. Essentiellement, le modificateur est "enroulé autour" de la fonction modifiée, plaçant son code à l’emplacement identifié par le caractère de soulignement.

Pour appliquer un modificateur, vous ajoutez son nom à la déclaration de la fonction. Plusieurs modificateurs peuvent être appliqués à une fonction ; ils sont appliqués dans l’ordre dans lequel ils sont déclarés, sous forme de liste séparée par des virgules.

Réécrivons notre fonction destroy pour utiliser le modificateur onlyOwner :

function destroy() public onlyOwner {
    selfdestruct(owner);
}

Le nom du modificateur de fonction (onlyOwner)se trouve après le mot-clé public et nous indique que la fonction destroy est modifiée par le modificateur onlyOwner. Essentiellement, vous pouvez lire cela comme "Seul le propriétaire peut détruire ce contrat". En pratique, le code résultant équivaut à "envelopper" le code de onlyOwner autour de destroy.

Les modificateurs de fonction sont un outil extrêmement utile car ils nous permettent d’écrire des conditions préalables pour les fonctions et de les appliquer de manière cohérente, ce qui rend le code plus facile à lire et, par conséquent, plus facile à auditer pour la sécurité. Ils sont le plus souvent utilisés pour le contrôle d’accès, mais ils sont assez polyvalents et peuvent être utilisés à diverses autres fins.

A l’intérieur d’un modificateur, vous pouvez accéder à toutes les valeurs (variables et arguments) visibles par la fonction modifiée. Dans ce cas, nous pouvons accéder à la variable owner, qui est déclarée dans le contrat. Cependant, l’inverse n’est pas vrai : vous ne pouvez accéder à aucune des variables du modificateur à l’intérieur de la fonction modifiée.

Héritage du contrat

L’objet contract de Solidity prend en charge l'héritage, qui est un mécanisme permettant d’étendre un contrat de base avec des fonctionnalités supplémentaires. Pour utiliser l’héritage, spécifiez un contrat parent avec le mot-clé is :

contract Child is Parent {
  ...
}

Avec cette construction, le contrat Child hérite de toutes les méthodes, fonctionnalités et variables de Parent. Solidity prend également en charge l’héritage multiple, qui peut être spécifié par des noms de contrat séparés par des virgules après le mot-clé is :

contract Child is Parent1, Parent2 {
  ...
}

L’héritage de contrat nous permet d’écrire nos contrats de manière à atteindre la modularité, l’extensibilité et la réutilisation. Nous commençons par des contrats simples et implémentons les capacités les plus génériques, puis nous les étendons en héritant de ces capacités dans des contrats plus spécialisés.

Dans notre contrat Faucet, nous avons introduit le constructeur et le destructeur, ainsi qu’un contrôle d’accès pour un propriétaire, assigné à la construction. Ces capacités sont assez génériques : de nombreux contrats en auront. Nous pouvons les définir comme des contrats génériques, puis utiliser l’héritage pour les étendre au contrat Faucet.

Nous commençons par définir un contrat de base owned, qui a une variable owner, en la définissant dans le constructeur du contrat :

contract owned {
	address owner;

	// Constructeur de contrat : définit le propriétaire
	constructor() {
		owner = msg.sender;
	}

	// Modificateur de contrôle d'accès
	modifier onlyOwner {
	    require(msg.sender == owner);
	    _;
	}
}

Ensuite, nous définissons un contrat de base mortal, qui hérite de owned :

contract mortal is owned {
	// Contract destructor
	function destroy() public onlyOwner {
		selfdestruct(owner);
	}
}

Comme vous pouvez le voir, le contrat mortal peut utiliser le modificateur de fonction onlyOwner, défini dans owned. Il utilise aussi indirectement la variable d’adresse owner et le constructeur défini dans owned. L’héritage rend chaque contrat plus simple et axé sur sa fonctionnalité spécifique, nous permettant de gérer les détails de manière modulaire.

Nous pouvons maintenant étendre davantage le contrat owned, en héritant de ses capacités dans Faucet :

contract Faucet is mortal {
	// Donnez de l'ether à quiconque demande
    function withdraw(uint withdraw_amount) public {
	// Limiter le montant du retrait
        require(withdraw_amount <= 0.1 ether);
	// Envoie le montant à l'adresse qui l'a demandé
        msg.sender.transfer(withdraw_amount);
    }
	// Accepte tout montant entrant
    function () external payable {}
}

En héritant de mortal, qui à son tour hérite de owned, le contrat Faucet a maintenant les fonctions constructeur et destroy, et un propriétaire défini. La fonctionnalité est la même que lorsque ces fonctions étaient dans Faucet, mais maintenant nous pouvons réutiliser ces fonctions dans d’autres contrats sans les réécrire. La réutilisation et la modularité du code rendent notre code plus propre, plus facile à lire et plus facile à auditer.

Gestion des erreurs (assert, require, revert)

Un appel de contrat peut se terminer et renvoyer une erreur. La gestion des erreurs dans Solidity est gérée par quatre fonctions : assert, require, revert et throw (désormais obsolète).

Lorsqu’un contrat se termine avec une erreur, tous les changements d’état (changements de variables, de soldes, etc.) sont annulés, tout au long de la chaîne d’appels de contrat si plusieurs contrats ont été appelés. Cela garantit que les transactions sont atomiques, ce qui signifie qu’elles se terminent avec succès ou n’ont aucun effet sur l’état et sont entièrement annulées.

Les fonctions assert et require fonctionnent de la même manière, évaluant une condition et arrêtant l’exécution avec une erreur si la condition c’est fausse. Par convention, assert est utilisé lorsque le résultat est censé être vrai, ce qui signifie que nous utilisons assert pour tester les conditions internes. Par comparaison, require est utilisé lors du test d’entrées (telles que des arguments de fonction ou des champs de transaction), définissant nos attentes pour ces conditions.

Nous avons utilisé require dans notre modificateur de fonction onlyOwner, pour tester que l’expéditeur du message est le propriétaire du contrat :

require(msg.sender == owner);

La fonction require agit comme une condition d’entrée, empêchant l’exécution du reste de la fonction et produisant une erreur si elle n’est pas satisfaite.

Depuis Solidity v0.4.22, require peut également inclure un message texte utile qui peut être utilisé pour indiquer la raison de l’erreur. Le message d’erreur est enregistré dans le journal des transactions. Ainsi, nous pouvons améliorer notre code en ajoutant un message d’erreur dans notre fonction require :

require(msg.sender == owner, "Seul le propriétaire du contrat peut appeler cette fonction");

Les fonctions revert et throw interrompent l’exécution du contrat et annulent tout changement d’état. La fonction throw est obsolète et sera supprimée dans les futures versions de Solidity ; vous devriez utiliser revert à la place. La fonction revert peut également prendre un message d’erreur comme seul argument, qui est enregistré dans le journal des transactions.

Certaines conditions d’un contrat généreront des erreurs, que nous les vérifiions explicitement ou non. Par exemple, dans notre contrat Faucet, nous ne vérifions pas s’il y a suffisamment d’ether pour satisfaire une demande de retrait. En effet, la fonction transfer échouera avec une erreur et annulera la transaction si le solde est insuffisant pour effectuer le transfert :

msg.sender.transfer(withdraw_amount);

Cependant, il peut être préférable de vérifier explicitement et de fournir un message d’erreur clair en cas d’échec. Nous pouvons le faire en ajoutant une instruction require avant le transfert :

require(this.balance >= withdraw_amount,
"Solde insuffisant dans le robinet pour la demande de retrait" );
msg.sender.transfer(withdraw_amount);

Un code de vérification d’erreur supplémentaire comme celui-ci augmentera légèrement la consommation de gaz, mais il offre un meilleur rapport d’erreur que s’il est omis. Vous devrez trouver le bon équilibre entre la consommation de gaz et la vérification détaillée des erreurs en fonction de l’utilisation prévue de votre contrat. Dans le cas d’un contrat Faucet destiné à un testnet, nous pouvons probablement abuser du côté des rapports supplémentaires même si cela coûte plus cher en gaz. Peut-être que pour un contrat de réseau principal, nous choisirons plutôt d’être économes avec notre consommation de gaz.

Événements

Lorsque une transaction se termine (avec succès ou non), elle produit une réception de transaction, comme nous le verrons dans La machine virtuelle Ethereum. Le reçu de transaction contient des entrées log qui fournissent des informations sur les actions qui se sont produites lors de l’exécution de la transaction. Les Événements ou Events sont les objets de haut niveau de Solidity utilisés pour construire ces journaux.

Les événements sont particulièrement utiles pour les clients légers et les services DApp, qui peuvent "surveiller" des événements spécifiques et les signaler à l’interface utilisateur, ou modifier l’état de l’application pour refléter un événement dans un contrat sous-jacent.

Les objets d’événement prennent des arguments qui sont sérialisés et enregistrés dans les journaux de transactions, dans la chaîne de blocs. Vous pouvez fournir le mot-clé indexé avant un argument, pour que la valeur fasse partie d’une table indexée (table de hachage) qui peut être recherchée ou filtrée par une application.

Nous n’avons ajouté aucun événement dans notre exemple Faucet jusqu’à présent, alors faisons cela. Nous ajouterons deux événements, un pour enregistrer les retraits et un pour enregistrer les dépôts. Nous appellerons ces événements Withdrawal et Deposit, respectivement. Tout d’abord, nous définissons les événements dans le contrat Faucet :

contract Faucet is mortal {
	event Withdrawal(address indexed to, uint amount);
	event Deposit(address indexed from, uint amount);

	[...]
}

Nous avons choisi de rendre les adresses indexed, pour permettre la recherche et le filtrage dans n’importe quelle interface utilisateur conçue pour accéder à notre Faucet.

Ensuite, nous utilisons le mot-clé emit pour incorporer les données d’événement dans les journaux de transactions :

// Donnez de l'ether à quiconque demande
function withdraw(uint withdraw_amount) public {
    [...]
    msg.sender.transfer(withdraw_amount);
    emit Withdrawal(msg.sender, withdraw_amount);
}
// Accepte tout montant entrant
function () external payable {
    emit Deposit(msg.sender, msg.value);
}

Le contrat Faucet.sol résultant ressemble à Faucet8.sol : contrat de robinet révisé, avec événements.

Example 4. Faucet8.sol : contrat de robinet révisé, avec événements
// Identifiant de licence SPDX : CC-BY-SA-4.0

// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.6.4;

contract Owned {
    address payable owner;

    // Constructeur de contrat : définit le propriétaire
    constructor() public {
        owner = msg.sender;
    }

    // Modificateur de contrôle d'accès
    modifier onlyOwner {
        require(msg.sender == owner, "Only the contract owner can call this function");
        _;
    }
}

contract Mortal is Owned {
    // Destructeur de contrat
    function destroy() public onlyOwner {
        selfdestruct(owner);
    }
}

contract Faucet is Mortal {
    event Withdrawal(address indexed to, uint amount);
    event Deposit(address indexed from, uint amount);

    // Accepte tout montant entrant
    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    // Donnez de l'éther à quiconque demande
    function withdraw(uint withdraw_amount) public {
        // Limiter le montant du retrait
        require(withdraw_amount <= 0.1 ether);

        require(
            address(this).balance >= withdraw_amount,
            "Insufficient balance in faucet for withdrawal request"
        );

        // Envoie le montant à l'adresse qui l'a demandé
        msg.sender.transfer(withdraw_amount);

        emit Withdrawal(msg.sender, withdraw_amount);
    }
}
Attraper des événements

OK, nous avons donc configuré notre contrat pour émettre des événements. Comment voir les résultats d’une transaction et "attraper" les événements ? La bibliothèque web3.js fournit une structure de données qui contient les journaux d’une transaction. Dans ceux-ci, nous pouvons voir les événements générés par la transaction.

Utilisons truffle pour exécuter une transaction de test sur le contrat Faucet révisé. Suivez les instructions dans Truffle pour configurer un répertoire de projet et compiler le code Faucet. Le code source peut être trouvé dans le référentiel GitHub du livre sous code/truffle/FaucetEvents.

$ truffle develop
truffle(develop)> compile
truffle(develop)> migrate
Using network 'develop'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xb77ceae7c3f5afb7fbe3a6c5974d352aa844f53f955ee7d707ef6f3f8e6b4e61
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying Faucet...
  ... 0xfa850d754314c3fb83f43ca1fa6ee20bc9652d891c00a2f63fd43ab5bfb0d781
  Faucet: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

truffle(develop)> Faucet.deployed().then(i => {FaucetDeployed = i})
truffle(develop)> FaucetDeployed.send(web3.utils.toWei(1, "ether")).then(res => \
                  { console.log(res.logs[0].event, res.logs[0].args) })
Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }
truffle(develop)> FaucetDeployed.withdraw(web3.utils.toWei(0.1, "ether")).then(res => \
                  { console.log(res.logs[0].event, res.logs[0].args) })
Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }

Après avoir déployé le contrat à l’aide de la fonction deployed, nous exécutons deux transactions. La première transaction est un dépôt (en utilisant send), qui émet un événement Deposit dans les journaux de transactions :

Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }

Ensuite, nous utilisons la fonction withdraw pour effectuer un retrait. Cela émet un événement Withdrawal :

Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }

Pour obtenir ces événements, nous avons examiné le tableau logs renvoyé comme résultat (res)des transactions. La première entrée de journal (logs[0])contient un nom d’événement dans logs[0].event et les arguments d’événement dans logs[0].args. En les affichant sur la console, nous pouvons voir le nom de l’événement émis et les arguments de l’événement.

Les événements sont un mécanisme très utile, non seulement pour la communication intra-contrat, mais aussi pour le débogage pendant le développement.

Appel d’autres contrats (send, call, callcode, delegatecall)

Appeler d’autres contrats depuis votre contrat est une opération très utile mais potentiellement dangereuse. Nous examinerons les différentes façons d’y parvenir et évaluerons les risques de chaque méthode. En bref, les risques découlent du fait que vous ne savez peut-être pas grand-chose sur un contrat auquel vous faites appel ou qui fait appel à votre contrat. Lors de la rédaction de contrats intelligents, vous devez garder à l’esprit que même si vous vous attendez principalement à traiter avec des EOA, rien n’empêche des contrats arbitrairement complexes et peut-être malveillants d’appeler et d’être appelés par votre code.

Création d’une nouvelle instance

Le moyen le plus sûr d’appeler un autre contrat est de créer vous-même cet autre contrat. De cette façon, vous êtes certain de ses interfaces et de son comportement. Pour ce faire, vous pouvez simplement l’instancier, en utilisant le mot-clé new, comme dans d’autres langages orientés objet. Dans Solidity, le mot-clé new créera le contrat sur la chaîne de blocs et renverra un objet que vous pourrez utiliser pour le référencer. Supposons que vous souhaitiez créer et appeler un contrat Faucet à partir d’un autre contrat appelé Token :

contract Token is mortal {
	Faucet _faucet;

	constructor() {
		_faucet = new Faucet();
	}
}

Ce mécanisme de construction de contrat garantit que vous connaissez le type exact du contrat et son interface. Le contrat Faucet doit être défini dans la portée de Token, ce que vous pouvez faire avec une instruction import si la définition se trouve dans un autre fichier :

import "Faucet.sol";

contract Token is mortal {
	Faucet _faucet;

	constructor() {
		_faucet = new Faucet();
	}
}

Vous pouvez éventuellement spécifier la value du transfert d’ether lors de la création et passer des arguments au constructeur du nouveau contrat :

import "Faucet.sol";

contract Token is mortal {
	Faucet _faucet;

	constructor() {
		_faucet = (new Faucet).value(0.5 ether)();
	}
}

Vous pouvez également appeler ensuite les fonctions Faucet. Dans cet exemple, nous appelons la fonction destroy de Faucet depuis la fonction destroy de Token :

import "Faucet.sol";

contract Token is mortal {
	Faucet _faucet;

	constructor() {
		_faucet = (new Faucet).value(0.5 ether)();
	}

	function destroy() ownerOnly {
		_faucet.destroy();
	}
}

Notez que tant que vous êtes le propriétaire du contrat Token, le contrat Token lui-même possède le nouveau contrat Faucet, donc seul le contrat Token peut le détruire.

Adressage d’une instance existante

Une autre façon d’appeler un contrat consiste à convertir l’adresse d’une instance existante du contrat. Avec cette méthode, vous appliquez une interface connue à une instance existante. Il est donc extrêmement important que vous sachiez avec certitude que l’instance à laquelle vous vous adressez est en fait du type que vous supposez. Regardons un exemple :

import "Faucet.sol";

contract Token is mortal {

	Faucet _faucet;

	constructor(address _f) {
		_faucet = Faucet(_f);
		_faucet.withdraw(0.1 ether)
	}
}

Ici, nous prenons une adresse fournie comme argument au constructeur, _f, et nous la transformons en un objet Faucet. C’est beaucoup plus risqué que le mécanisme précédent, car nous ne savons pas avec certitude si cette adresse est réellement un objet Faucet. Lorsque nous appelons withdraw, nous supposons qu’il accepte les mêmes arguments et exécute le même code que notre déclaration Faucet, mais nous ne pouvons pas en être sûrs. Pour autant que nous sachions, la fonction withdraw à cette adresse pourrait exécuter quelque chose de complètement différent de ce que nous attendons, même si elle porte le même nom. Utiliser des adresses passées en entrée et les intégrer dans des objets spécifiques est donc beaucoup plus dangereux que de créer soi-même le contrat.

Appel brut call et delegatecall

Solidity propose des fonctions encore plus "de bas niveau" pour appeler d’autres contrats. Ceux-ci correspondent directement aux opcodes EVM du même nom et nous permettent de construire manuellement un appel de contrat à contrat. En tant que tels, ils représentent les mécanismes les plus flexibles et les plus dangereux pour appeler d’autres contrats.

Voici le même exemple, en utilisant une méthode call :

contract Token is mortal {
	constructor(address _faucet) {
		_faucet.call("withdraw", 0.1 ether);
	}
}

Comme vous pouvez le voir, ce type de call est un appel aveugle dans une fonction, un peu comme la construction d’une transaction brute, uniquement à partir du contexte d’un contrat. Cela peut exposer votre contrat à un certain nombre de risques de sécurité, le plus important étant la réentrance, dont nous discuterons plus en détail dans Réentrance. La fonction call renverra false s’il y a un problème, vous pouvez donc évaluer la valeur de retour pour la gestion des erreurs :

contract Token is mortal {
	constructor(address _faucet) {
		if !(_faucet.call("withdraw", 0.1 ether)) {
			revert("Withdrawal from faucet failed");
		}
	}
}

Une autre variante de call est delegatecall, qui a remplacé le plus dangereux callcode. La méthode callcode sera bientôt obsolète, elle ne doit donc pas être utilisée.

Comme mentionné dans objet d’adresse, un delegatecall est différent d’un call en ce que le contexte msg ne change pas. Par exemple, alors qu’un call change la valeur de msg.sender pour être le contrat d’appel, un delegatecall conserve le même msg.sender que dans le contrat d’appel. Essentiellement, delegatecall exécute le code d’un autre contrat dans le contexte de l’exécution du contrat en cours. Il est le plus souvent utilisé pour invoquer du code à partir d’une bibliothèque. Cela vous permet également de dessiner sur le modèle d’utilisation des fonctions de bibliothèque stockées ailleurs, mais de faire fonctionner ce code avec les données de stockage de votre contrat.

L’appel delegatecall doit être utilisé avec beaucoup de prudence. Cela peut avoir des effets inattendus, surtout si le contrat que vous appelez n’a pas été conçu comme une bibliothèque.

Utilisons un exemple de contrat pour démontrer les différentes sémantiques d’appel utilisées par call et delegatecall pour appeler des bibliothèques et des contrats. Dans CallExamples.sol : un exemple de différentes sémantiques d’appel, nous utilisons un événement pour enregistrer les détails de chaque appel et voir comment le contexte d’appel change en fonction du type d’appel.

Example 5. CallExamples.sol : un exemple de différentes sémantiques d’appel
pragma solidity ^0.4.22;

contract calledContract {
    event callEvent(address sender, address origin, address from);
    function calledFunction() public {
        emit callEvent(msg.sender, tx.origin, this);
    }
}

library calledLibrary {
    event callEvent(address sender, address origin,  address from);
    function calledFunction() public {
        emit callEvent(msg.sender, tx.origin, this);
    }
}

contract caller {

    function make_calls(calledContract _calledContract) public {

        // Appel calledContract et callLibrary directement
        _calledContract.calledFunction();
        calledLibrary.calledFunction();

        // Appels de bas niveau utilisant l'objet d'adresse pour le contrat appelé
        require(address(_calledContract).
        call(bytes4(keccak256("calledFunction()"))));
        require(address(_calledContract).
        delegatecall(bytes4(keccak256("calledFunction()"))));
	}
}

Comme vous pouvez le voir dans cet exemple, notre contrat principal est caller, qui appelle une bibliothèque calledLibrary et un contrat calledContract. La bibliothèque appelée et le contrat ont des fonctions calledFunction identiques, qui émettent un événement calledEvent. L’événement calledEvent enregistre trois éléments de données : msg.sender, tx.origin et this. Chaque fois que calledFunction est appelé, il peut avoir un contexte d’exécution différent (avec des valeurs différentes pour potentiellement toutes les variables de contexte), selon qu’il est appelé directement ou via delegatecall.

Dans caller, nous appelons d’abord le contrat et la bibliothèque directement, en invoquant calledFunction dans chacun. Ensuite, nous utilisons explicitement les fonctions de bas niveau call et delegatecall pour appeler calledContract.calledFunction. De cette façon, nous pouvons voir comment les différents mécanismes d’appel se comportent.

Exécutons ceci dans un environnement de développement Truffle et capturons les événements, pour voir à quoi cela ressemble :

truffle(develop)> migrate
Using network 'develop'.
[...]
Saving artifacts...
truffle(develop)> web3.eth.accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'
truffle(develop)> caller.address
'0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'
truffle(develop)> calledContract.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'
truffle(develop)> calledLibrary.address
'0xf25186b5081ff5ce73482ad761db0eb0d25abfbf'
truffle(develop)> caller.deployed().then( i => { callerDeployed = i })

truffle(develop)> callerDeployed.make_calls(calledContract.address).then(res => \
                  { res.logs.forEach( log => { console.log(log.args) })})
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }

Voyons ce qui s’est passé ici. Nous avons appelé la fonction make_calls et passé l’adresse de calledContract, puis avons intercepté les quatre événements émis par chacun des différents appels. Examinons la fonction make_calls et passons en revue chaque étape.

Le premier appel est :

_calledContract.calledFunction();

Ici, nous appelons calledContract.calledFunction directement, en utilisant l’ABI de haut niveau pour calledFunction. L’événement émis est :

sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10'

Comme vous pouvez le voir, msg.sender est l’adresse du contrat caller. Le tx.origin est l’adresse de notre compte, web3.eth.accounts[0], qui a envoyé la transaction à caller. L’événement a été émis par calledContract, comme nous pouvons le voir sur le dernier argument de l’événement.

Le prochain appel dans make_calls est vers la bibliothèque :

calledLibrary.calledFunction();

Il semble identique à la façon dont nous avons appelé le contrat, mais se comporte très différemment. Regardons le second événement émis :

sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'

Cette fois, le msg.sender n’est pas l’adresse de caller. Au lieu de cela, c’est l’adresse de notre compte, et c’est la même que l’origine de la transaction. En effet, lorsque vous appelez une bibliothèque, l’appel est toujours delegatecall et s’exécute dans le contexte de l’appelant. Ainsi, lorsque le code calledLibrary s’exécutait, il héritait du contexte d’exécution de caller, comme si son code s’exécutait à l’intérieur de caller. La variable this (affichée comme from dans l’événement émis) est l’adresse de caller, même si elle est accessible depuis calledLibrary.

Les deux appels suivants, utilisant les appels call et delegatecall de bas niveau, vérifient nos attentes, émettant des événements qui reflètent ce que nous venons de faire

Considérations sur le gaz

Le gaz, décrit dans plus de détails dans Gaz, est une considération extrêmement importante dans la programmation de contrats intelligents. Le gaz est une ressource limitant la quantité maximale de calculs qu’Ethereum permettra à une transaction de consommer. Si la limite de gaz est dépassée pendant le calcul, la série d’événements suivante se produit :

  • Une exception "out of gas" est levée.

  • L’état du contrat avant l’exécution est restauré (inversé).

  • Tout l’ether utilisé pour payer le gaz est considéré comme des frais de transaction ; il n’est pas remboursé.

Étant donné que le gaz est payé par l’utilisateur qui initie la transaction, les utilisateurs sont découragés d’appeler des fonctions qui ont un coût de gaz élevé. Il est donc dans l’intérêt du programmeur de minimiser le coût en gaz des fonctions d’un contrat. À cette fin, certaines pratiques sont recommandées lors de la construction de contrats intelligents, afin de minimiser le coût en gaz d’un appel de fonction.

Évitez les tableaux dimensionnés dynamiquement

Toute boucle à travers un tableau de taille dynamique où une fonction effectue des opérations sur chaque élément ou recherche un élément particulier introduit le risque d’utiliser trop de gaz. En effet, le contrat peut s’essouffler avant d’avoir trouvé le résultat souhaité, ou avant d’agir sur chaque élément, perdant ainsi du temps et de l’ether sans donner le moindre résultat.

Évitez les appels vers d’autres contrats

Appeler d’autres contrats, surtout lorsque le coût en gaz de leurs fonctions n’est pas connu, introduit le risque de manquer de gaz. Évitez d’utiliser des bibliothèques qui ne sont pas bien testées et largement utilisées. Moins une bibliothèque a été examinée par d’autres programmeurs, plus le risque de l’utiliser est grand.

Estimation du coût du gaz

Si vous avez besoin d’estimer le gaz nécessaire pour exécuter une certaine méthode d’un contrat en tenant compte ses arguments, vous pouvez utiliser la procédure suivante :

var contract = web3.eth.contract(abi).at(address);
var gasEstimate = contract.myAweSomeMethod.estimateGas(arg1, arg2,
    {from: account});

gasEstimate vous indiquera le nombre d’unités de gaz nécessaires à son exécution. Il s’agit d’une estimation en raison de l’exhaustivité de Turing de l’EVM - il est relativement trivial de créer une fonction qui prendra des quantités de gaz très différentes pour exécuter différents appels. Même le code de production peut modifier les chemins d’exécution de manière subtile, ce qui entraîne des coûts de gaz extrêmement différents d’un appel à l’autre. Cependant, la plupart des fonctions sont sensibles et estimateGas donnera une bonne estimation la plupart du temps.

Pour obtenir le prix du gaz du réseau, vous pouvez utiliser :

var gasPrice = web3.eth.getGasPrice();

Et à partir de là, vous pouvez estimer le coût du gaz :

var gasCostInEther = web3.utils.fromWei((gasEstimate * gasPrice), 'ether');

Appliquons nos fonctions d’estimation de gaz pour estimer le coût du gaz de notre exemple Faucet, en utilisant le code du référentiel du livre.

Démarrez Truffle en mode développement et exécutez le fichier JavaScript dans gas_estimates.js : Utilisation de la fonction estimateGas, gas_estimates.js.

Example 6. gas_estimates.js : Utilisation de la fonction estimateGas
var FaucetContract = artifacts.require("./Faucet.sol");

FaucetContract.web3.eth.getGasPrice(function(error, result) {
    var gasPrice = Number(result);
    console.log("Gas Price is " + gasPrice + " wei"); // "10000000000000"

	// Récupère l'instance de contrat
    FaucetContract.deployed().then(function(FaucetContractInstance) {

	// Utilisez le mot clé 'estimateGas' après le nom de la fonction pour obtenir le gaz
	// estimation pour cette fonction particulière (aprove)
		FaucetContractInstance.send(web3.utils.toWei(1, "ether"));
        return FaucetContractInstance.withdraw.estimateGas(web3.utils.toWei(0.1, "ether"));

    }).then(function(result) {
        var gas = Number(result);

        console.log("gas estimation = " + gas + " units");
        console.log("gas cost estimation = " + (gas * gasPrice) + " wei");
        console.log("gas cost estimation = " +
                FaucetContract.web3.utils.fromWei((gas * gasPrice), 'ether') + " ether");
    });
});

Voici à quoi cela ressemble dans la console de développement Truffle :

$ truffle develop

truffle(develop)> exec gas_estimates.js
Using network 'develop'.

Gas Price is 20000000000 wei
gas estimation = 31397 units
gas cost estimation = 627940000000000 wei
gas cost estimation = 0.00062794 ether

Il est recommandé d’évaluer le coût en gaz des fonctions dans le cadre de votre workflow de développement, afin d’éviter toute surprise lors du déploiement de contrats sur le réseau principal.

Conclusion

Dans ce chapitre, nous avons commencé à travailler en détail avec les contrats intelligents et exploré le langage de programmation de contrats Solidity. Nous avons pris un exemple de contrat simple, Faucet.sol, et l’avons progressivement amélioré et rendu plus complexe, en l’utilisant pour explorer divers aspects du langage Solidity. Dans Contrats intelligents et Vyper nous travaillerons avec Vyper, un autre langage de programmation orienté contrat. Nous comparerons Vyper à Solidity, montrant certaines des différences dans la conception de ces deux langages et approfondirons notre compréhension de la programmation de contrats intelligents.

Contrats intelligents et Vyper

Bannière Amazon du livre Maîtriser Ethereum

Vyper est un langage de programmation expérimental orienté contrat pour la machine virtuelle Ethereum qui s’efforce de fournir une auditabilité supérieure, en permettant aux développeurs de produire plus facilement du code intelligible. En fait, l’un des principes de Vyper est de rendre pratiquement impossible pour les développeurs d’écrire du code trompeur.

Dans ce chapitre, nous examinerons les problèmes courants liés aux contrats intelligents, présenterons le langage de programmation de contrats Vyper et le comparerons à Solidity, en démontrant les différences.

Vulnérabilités et Vyper

Une étude récente a analysé près d’un million de contrats intelligents Ethereum déployés et a constaté que nombre de ces contrats contenaient de graves vulnérabilités. Au cours de leur analyse, les chercheurs ont défini trois catégories de base de vulnérabilités en trace:

Contrats suicidaires

Contrats intelligents qui peuvent être tués par des adresses arbitraires

Contrats cupides

Contrats intelligents pouvant atteindre un état dans lequel ils ne peuvent pas libérer d’ether

Contrats prodigues

Contrats intelligents pouvant être conclus pour libérer de l’ether à des adresses arbitraires

Les vulnérabilités sont introduites dans les contrats intelligents via le code. Il peut être fortement soutenu que ces vulnérabilités et d’autres ne sont pas introduites intentionnellement, mais quoi qu’il en soit, un code de contrat intelligent indésirable entraîne évidemment une perte de fonds inattendue pour les utilisateurs d’Ethereum, et ce n’est pas idéal. Vyper est conçu pour faciliter l’écriture de code sécurisé, ou également pour rendre plus difficile l’écriture accidentelle de code trompeur ou vulnérable.

Comparaison avec Solidity

L’une des façons dont Vyper essaie de rendre le code dangereux plus difficile à écrire est en omettant délibérément certaines fonctionnalités de Solidity. Il est important pour ceux qui envisagent de développer des contrats intelligents dans Vyper de comprendre quelles fonctionnalités Vyper n’a pas, et pourquoi. Par conséquent, dans cette section, nous allons explorer ces fonctionnalités et expliquer pourquoi elles ont été omises.

Modificateurs

Comme nous l’avons vu dans le chapitre précédent, dans Solidity vous pouvez écrire une fonction à l’aide de modificateurs (modifiers). Par exemple, la fonction suivante, changeOwner, exécutera le code dans un modificateur appelé onlyBy dans le cadre de son exécution :

function changeOwner(address _newOwner)
    public
    onlyBy(owner)
{
    owner = _newOwner;
}

Ce modificateur applique une règle relative à la propriété. Comme vous pouvez le voir, ce modificateur particulier agit comme un mécanisme pour effectuer une pré-vérification au nom de la fonction changeOwner :

modifier onlyBy(address _account)
{
    require(msg.sender == _account);
    _;
}

Mais les modificateurs ne sont pas là uniquement pour effectuer des vérifications, comme illustré ici. En fait, en tant que modificateurs, ils peuvent modifier considérablement l’environnement d’un contrat intelligent, dans le contexte de la fonction appelante. En termes simples, les modificateurs sont omniprésents.

Regardons un autre exemple de style Solidity :

enum Stages {
    SafeStage,
    DangerStage,
    FinalStage
}

uint public creationTime = now;
Stages public stage = Stages.SafeStage;

function nextStage() internal {
    stage = Stages(uint(stage) + 1);
}

modifier stageTimeConfirmation() {
    if (stage == Stages.SafeStage &&
                now >= creationTime + 10 days)
        nextStage();
    _;
}

function a()
    public
    stageTimeConfirmation
    // Plus de code va ici
{
}

D’une part, les développeurs doivent toujours vérifier tout autre code que leur propre code appelle. Cependant, il est possible que dans certaines situations (comme lorsque les contraintes de temps ou l’épuisement entraînent un manque de concentration), un développeur oublie une seule ligne de code. Cela est encore plus probable si le développeur doit se déplacer dans un fichier volumineux tout en gardant mentalement une trace de la hiérarchie des appels de fonction et en mémorisant l’état des variables de contrat intelligent.

Regardons l’exemple précédent un peu plus en profondeur. Imaginez qu’un développeur écrive une fonction publique appelée "a". Le développeur est nouveau dans ce contrat et utilise un modificateur écrit par quelqu’un d’autre. En un coup d’œil, il semble que le modificateur stageTimeConfirmation effectue simplement quelques vérifications concernant l’âge du contrat par rapport à la fonction appelante. Ce que le développeur peut ne pas réaliser, c’est que le modificateur appelle également une autre fonction, nextStage. Dans ce scénario de démonstration simpliste, le simple fait d’appeler la fonction publique "a" entraîne le déplacement de la variable "stage" du contrat intelligent de "SafeStage" à "DangerStage".

Vyper a complètement supprimé les modificateurs. Les recommandations de Vyper sont les suivantes : si vous n’exécutez que des assertions avec des modificateurs, utilisez simplement des vérifications et des assertions en ligne dans le cadre de la fonction ; si vous modifiez l’état du contrat intelligent, etc., intégrez à nouveau ces modifications explicitement à la fonction. Cela améliore l’auditabilité et la lisibilité, car le lecteur n’a pas à "envelopper" mentalement (ou manuellement) le code du modificateur autour de la fonction pour voir ce qu’elle fait.

Héritage de classe

L’héritage permet aux programmeurs pour exploiter la puissance du code pré-écrit en acquérant des fonctionnalités, des propriétés et des comportements préexistants à partir de bibliothèques de logiciels existantes. L’héritage est puissant et favorise la réutilisation du code. Solidity prend en charge l’héritage multiple ainsi que le polymorphisme, mais bien que ce soient des fonctionnalités clés de la programmation orientée objet, Vyper ne les prend pas en charge. Vyper soutient que la mise en œuvre de l’héritage nécessite que les codeurs et les auditeurs sautent entre plusieurs fichiers afin de comprendre ce que fait le programme. Vyper considère également que l’héritage multiple peut rendre le code trop compliqué à comprendre - un point de vue tacitement admis par la documentation Solidity, qui donne un exemple de la façon dont l’héritage multiple peut être problématique.

Assemblage en ligne

L’assemblage en ligne donne aux développeurs un accès de bas niveau à Ethereum Virtual Machine, permettant aux programmes Solidity d’effectuer des opérations en accédant directement aux instructions EVM. Par exemple, le code assembleur en ligne suivant ajoute 3 à l’emplacement mémoire 0x80 :

3 0x80 mload add 0x80 mstore

Vyper considère que la perte de lisibilité est un prix trop élevé à payer pour la puissance supplémentaire et ne prend donc pas en charge l’assemblage en ligne.

Surcharge de fonction

La surcharge de fonction permet aux développeurs d’écrire plusieurs fonctions du même nom. La fonction utilisée à une occasion donnée dépend des types d’arguments fournis. Prenons par exemple les deux fonctions suivantes :

function f(uint _in) public pure returns (uint out) {
    out = 1;
}

function f(uint _in, bytes32 _key) public pure returns (uint out) {
    out = 2;
}

La première fonction (nommée f)accepte un argument d’entrée de type uint ; la deuxième fonction (également nommée f)accepte deux arguments, un de type uint et un de type bytes32. Avoir plusieurs définitions de fonction avec le même nom prenant des arguments différents peut être déroutant, donc Vyper ne prend pas en charge la surcharge de fonction.

Conversion de type variable

Il existe deux types de typage : implicite et explicite

Le transtypage implicite est souvent effectué au moment de la compilation. Par exemple, si une conversion de type est sémantiquement correcte et qu’aucune information n’est susceptible d’être perdue, le compilateur peut effectuer une conversion implicite, telle que la conversion d’une variable de type uint8 en uint16. Les premières versions de Vyper autorisaient le transtypage implicite des variables, mais pas les versions récentes.

Les transtypages explicites peuvent être insérés dans Solidity. Malheureusement, ils peuvent entraîner des comportements inattendus. Par exemple, convertir un uint32 en un type plus petit uint16 supprime simplement les bits d’ordre supérieur, comme illustré ici :

uint32 a = 0x12345678;
uint16 b = uint16(a);
// La variable b est 0x5678 maintenant

Vyper a à la place une fonction convert pour effectuer des transtypages explicites. La fonction convert (trouvée à la ligne 82 de convert.py) :

def convert(expr, context):
    output_type = expr.args[1].s
    if output_type in conversion_table:
        return conversion_table[output_type](expr, context)
    else:
        raise Exception("Conversion to {} is invalid.".format(output_type))

Notez l’utilisation de conversion_table (trouvé à la ligne 90 du même fichier), qui ressemble à ceci :

conversion_table = {
    'int128': to_int128,
    'uint256': to_unint256,
    'decimal': to_decimal,
    'bytes32': to_bytes32,
}

Lorsqu’un développeur appelle convert, il fait référence à conversion_table, ce qui garantit que la conversion appropriée est effectuée. Par exemple, si un développeur passe un int128 à la fonction convert, la fonction to_int128 à la ligne 26 du même fichier (convert.py) sera exécutée. La fonction to_int128 est la suivante :

@signature(('int128', 'uint256', 'bytes32', 'bytes'), 'str_literal')
def to_int128(expr, args, kwargs, context):
    in_node = args[0]
    typ, len = get_type(in_node)
    if typ in ('int128', 'uint256', 'bytes32'):
        if in_node.typ.is_literal
            and not SizeLimits.MINNUM <= in_node.value <= SizeLimits.MAXNUM:
            raise InvalidLiteralException(
                "Number out of range: {}".format(in_node.value), expr
            )
        return LLLnode.from_list(
            ['clamp', ['mload', MemoryPositions.MINNUM], in_node,
            ['mload', MemoryPositions.MAXNUM]], typ=BaseType('int128'),
            pos=getpos(expr)
        )
    else:
        return byte_array_to_num(in_node, expr, 'int128')

Comme vous pouvez le constater, le processus de conversion garantit qu’aucune information ne peut être perdue. si c’est possible, une exception est levée. Le code de conversion empêche la troncation ainsi que d’autres anomalies qui seraient normalement autorisées par un transtypage implicite.

Choisir un transtypage explicite plutôt qu’implicite signifie que le développeur est responsable de l’exécution de tous les transtypages. Bien que cette approche produise un code plus détaillé, elle améliore également la sécurité et la vérifiabilité des contrats intelligents.

Préconditions et Postconditions

Vyper gère explicitement les préconditions, les postconditions et les changements d’état. Bien que cela produise un code redondant, cela permet également une lisibilité et une sécurité maximales. Lors de la rédaction d’un contrat intelligent dans Vyper, un développeur doit observer les trois points suivants :

Condition

Quel est l’état/condition actuel des variables d’état Ethereum ?

Effets

Quels effets ce code de contrat intelligent aura-t-il sur la condition des variables d’état lors de l’exécution ? Autrement dit, qu’est-ce qui sera affecté et qu’est-ce qui ne sera pas affecté ? Ces effets sont-ils conformes aux intentions du contrat intelligent ?

Interaction

Une fois que les deux premières considérations ont été traitées de manière exhaustive, il est temps d’exécuter le code. Avant le déploiement, parcourez logiquement le code et examinez tous les résultats permanents possibles, les conséquences et les scénarios d’exécution du code, y compris les interactions avec d’autres contrats.

Idéalement, chacun de ces points devrait être soigneusement examiné puis documenté de manière approfondie dans le code. Cela améliorera la conception du code, le rendant finalement plus lisible et auditable.

Décorateurs

Les décorateurs suivants peuvent être utilisés au début de chaque fonction :

@private

Le décorateur @private rend la fonction inaccessible depuis l’extérieur du contrat.

@public

Le décorateur @public rend la fonction à la fois visible et exécutable publiquement. Par exemple, même le portefeuille Ethereum affichera de telles fonctions lors de la visualisation du contrat.

@constant

Les fonctions avec le décorateur @constant ne sont pas autorisées à modifier les variables d’état. En fait, le compilateur rejettera le programme entier (avec une erreur appropriée) si la fonction essaie de changer une variable d’état.

@payable

Seules les fonctions avec le décorateur @payable sont autorisées à transférer de la valeur.

Vyper implémente explicitement la logique des décorateurs. Par exemple, le processus de compilation de Vyper échouera si une fonction a à la fois un décorateur @payable et un décorateur @constant. Cela a du sens car une fonction qui transfère une valeur a par définition mis à jour l’état, elle ne peut donc pas être @constant. Chaque fonction Vyper doit être décorée avec @public ou @private (mais pas les deux !).

Ordre des fonctions et des variables

Chaque contrat intelligent Vyper individuel se compose d’un seul fichier Vyper uniquement. En d’autres termes, tout le code d’un contrat intelligent Vyper donné, y compris toutes les fonctions, variables, etc., existe au même endroit. Vyper exige que la fonction et les déclarations de variables de chaque contrat intelligent soient écrites physiquement dans un ordre particulier. Solidity n’a pas du tout cette exigence. Jetons un coup d’œil à un exemple Solidity :

pragma solidity ^0.4.0;

contract ordering {

    function topFunction()
    external
    returns (bool) {
        initiatizedBelowTopFunction = this.lowerFunction();
        return initiatizedBelowTopFunction;
    }

    bool initiatizedBelowTopFunction;
    bool lowerFunctionVar;

    function lowerFunction()
    external
    returns (bool) {
        lowerFunctionVar = true;
        return lowerFunctionVar;
    }

}

Dans cet exemple, la fonction appelée topFunction appelle une autre fonction, lowerFunction. topFunction attribue également une valeur à une variable appelée initiatizedBelowTopFunction. Comme vous pouvez le voir, Solidity n’exige pas que ces fonctions et variables soient physiquement déclarées avant d’être appelées par le code d’exécution. Il s’agit d’un code Solidity valide qui se compilera avec succès.

Les exigences de commande de Vyper ne sont pas une nouveauté ; en fait, ces exigences de commande ont toujours été présentes dans la programmation Python. La commande requise par Vyper est simple et logique, comme illustré dans cet exemple suivant :

# Déclarez une variable appelée theBool
theBool: public(bool)

# Déclarez une fonction appelée topFunction
@public
def topFunction() -> bool:
# Attribuez une valeur à la fonction déjà déclarée appelée theBool
    self.theBool = True
    return self.theBool

# Déclarez une fonction appelée lowerFunction
@public
def lowerFunction():
# Appelez la fonction déjà déclarée appelée topFunction
    assert self.topFunction()

Cela montre le bon ordre des fonctions et des variables dans un contrat intelligent Vyper. Notez comment la variable theBool et la fonction topFunction sont déclarées avant qu’elles ne reçoivent une valeur et ne soient appelées, respectivement. Si theBool était déclaré sous topFunction ou si topFunction était déclaré sous lowerFunction, ce contrat ne serait pas compilé.

Compilation

Vyper a son propre éditeur de code et compilateur en ligne, qui vous permet d’écrire puis compilez vos contrats intelligents en code intermédiaire, ABI et LLL en utilisant uniquement votre navigateur Web. Le compilateur en ligne Vyper propose une variété de contrats intelligents pré-écrits pour votre commodité, y compris des contrats pour une simple enchère ouverte, des achats à distance sécurisés, des jetons ERC20, etc. Cet outil, propose une seule version du logiciel de compilation. Il est mis à jour régulièrement mais ne garantit pas toujours la dernière version. Etherscan a un compilateur Vyper en ligne qui vous permet de sélectionner la version du compilateur. De plus, Remix, conçu à l’origine pour les contrats intelligents Solidity, dispose désormais d’un plug-in Vyper disponible dans l’onglet Paramètres.

Note

Vyper implémente ERC20 en tant que contrat précompilé, ce qui permet d’utiliser facilement ces contrats intelligents prêts à l’emploi. Les contrats dans Vyper doivent être déclarés en tant que variables globales. Un exemple de déclaration de la variable ERC20 est le suivant :

token: address(ERC20)

Vous pouvez également compiler un contrat à l’aide de la ligne de commande. Chaque contrat Vyper est enregistré dans un seul fichier avec l’extension .vy. Une fois installé, vous pouvez compiler un contrat avec Vyper en exécutant la commande suivante :

vyper ~/hello_world.vy

La description ABI lisible par l’homme (au format JSON) peut ensuite être obtenue en exécutant la commande suivante :

vyper -f json ~/hello_world.v.py

Protection contre les erreurs de dépassement au niveau du compilateur

Les erreurs de dépassement dans le logiciel peut être catastrophique lorsqu’il s’agit de valeur réelle. Par exemple, une transaction de la mi-avril 2018 montre le transfert malveillant de plus de 57 896 044 618 658 100 000 000 000 000 000 000 000 000,000 000 000 000 000 000 tokens ou jetons BEC. Cette transaction était le résultat d’un problème de dépassement d’entier dans le contrat de jeton ERC20 de BeautyChain (BecToken.sol). Les développeurs de Solidity ont accès à des bibliothèques comme SafeMathainsi qu’à des outils d’analyse de la sécurité des contrats intelligents Ethereum comme Mythril OSS. Cependant, les développeurs ne sont pas obligés d’utiliser les outils de sécurité. En termes simples, si la sécurité n’est pas appliquée par le langage, les développeurs peuvent écrire du code non sécurisé qui se compilera avec succès et s’exécutera plus tard "avec succès".

Vyper dispose d’une protection intégrée contre les débordements, mise en œuvre selon une approche à deux volets. Tout d’abord, Vyper fournit un SafeMath équivalent qui inclut les cas d’exception nécessaires pour l’arithmétique entière. Deuxièmement, Vyper utilise des pinces chaque fois qu’une constante littérale est chargée, qu’une valeur est transmise à une fonction ou qu’une variable est affectée. Les pinces sont implémentées via des fonctions personnalisées dans le compilateur LLL (Low-level Lisp-like Language) et ne peuvent pas être désactivées. (Le compilateur Vyper génère LLL plutôt que le code intermédiaire EVM ; cela simplifie le développement de Vyper lui-même.)

Lecture et écriture de données

Bien qu’il soit coûteux de stocker, lire et modifier des données, ces opérations de stockage sont une composante nécessaire de la plupart des contrats intelligents. Les contrats intelligents peuvent écrire des données à deux endroits :

État global

Les variables d’état d’un contrat intelligent donné sont stockées dans le trie d’état global d’Ethereum ; un contrat intelligent ne peut stocker, lire et modifier que des données relatives à l’adresse de ce contrat particulier (c’est-à-dire que les contrats intelligents ne peuvent pas lire ou écrire dans d’autres contrats intelligents).

Journaux

Un contrat intelligent peut également écrire dans les données de la chaîne d’Ethereum via des événements de journal. Alors que Vyper utilisait initialement la syntaxe __log__ pour déclarer ces événements, une mise à jour a été effectuée pour aligner davantage sa déclaration d’événement sur la syntaxe d’origine de Solidity. Par exemple, la déclaration par Vyper d’un événement appelé MyLog était à l’origine MyLog: __log__({arg1: indexed(bytes[3])}). La syntaxe est maintenant devenue MyLog: event({arg1: indexed(bytes[3])}). Il est important de noter que l’exécution de l’événement de journalisation dans Vyper était, et est toujours, comme suit : log.MyLog("123").

Bien que les contrats intelligents puissent écrire dans les données de la chaîne d’Ethereum (via des événements de journal), ils ne peuvent pas lire les événements de journal en chaîne qu’ils ont créés. Néanmoins, l’un des avantages de l’écriture dans les données de la chaîne d’Ethereum via des événements de journal est que les journaux peuvent être découverts et lus, sur la chaîne publique, par des clients légers. Par exemple, la valeur logsBloom dans un bloc extrait peut indiquer si un événement de journal est présent ou non. Une fois que l’existence d’événements de journal a été établie, les données de journal peuvent être obtenues à partir d’un reçu de transaction donné.

Conclusion

Vyper est un nouveau langage de programmation orienté contrat puissant et intéressant. Sa conception est biaisée vers "l’exactitude", au détriment d’une certaine flexibilité. Cela peut permettre aux programmeurs de rédiger de meilleurs contrats intelligents et d’éviter certains pièges qui provoquent l’apparition de graves vulnérabilités. Ensuite, nous examinerons plus en détail la sécurité des contrats intelligents. Certaines des nuances de la conception de Vyper peuvent devenir plus apparentes une fois que vous avez lu tous les problèmes de sécurité possibles qui peuvent survenir dans les contrats intelligents.

Sécurité des contrats intelligents

Bannière Amazon du livre Maîtriser Ethereum

La sécurité est l’une des considérations les plus importantes lors de la rédaction de contrats intelligents. Dans le domaine de la programmation de contrats intelligents, les erreurs sont coûteuses et facilement exploitables. Dans ce chapitre, nous examinerons les meilleures pratiques de sécurité et les modèles de conception, ainsi que les «anti-modèles de sécurité», qui sont des pratiques et des modèles qui peuvent introduire des vulnérabilités dans nos contrats intelligents.

Comme avec d’autres programmes, un contrat intelligent exécutera exactement ce qui est écrit, ce qui n’est pas toujours ce que le programmeur avait prévu. De plus, tous les contrats intelligents sont publics et tout utilisateur peut interagir avec eux simplement en créant une transaction. Toute vulnérabilité peut être exploitée et les pertes sont presque toujours impossibles à récupérer. Il est donc essentiel de suivre les meilleures pratiques et d’utiliser des modèles de conception éprouvés.

Meilleures pratiques de sécurité

La programmation défensive est un style de programmation particulièrement bien adapté aux contrats intelligents. Il met l’accent sur les éléments suivants, qui sont tous des pratiques exemplaires :

Minimalisme/simplicité

La complexité est l’ennemie de la sécurité. Plus le code est simple et moins il en fait, moins il y a de chances qu’un bogue ou un effet imprévu se produise. Lorsqu’ils s’engagent pour la première fois dans la programmation de contrats intelligents, les développeurs sont souvent tentés d’essayer d’écrire beaucoup de code. Au lieu de cela, vous devriez parcourir votre code de contrat intelligent et essayer de trouver des moyens d’en faire moins, avec moins de lignes de code, moins de complexité et moins de "fonctionnalités". Si quelqu’un vous dit que son projet a produit "des milliers de lignes de code" pour ses contrats intelligents, vous devez vous interroger sur la sécurité de ce projet. Plus simple est plus sûr.

Réutilisation du code

Essayez de ne pas réinventer la roue. S’il existe déjà une bibliothèque ou un contrat qui fait la plupart de ce dont vous avez besoin, réutilisez-le. Dans votre propre code, suivez le principe DRY (Don’t Repeat Yourself) : ne vous répétez pas. Si vous voyez un extrait de code répété plus d’une fois, demandez-vous s’il pourrait être écrit en tant que fonction ou bibliothèque et réutilisé. Le code qui a été largement utilisé et testé est probablement plus sécurisé que tout nouveau code que vous écrivez. Méfiez-vous du syndrome "Pas inventé ici", où vous êtes tenté d'"améliorer" une fonctionnalité ou un composant en le construisant à partir de zéro. Le risque de sécurité est souvent supérieur à la valeur d’amélioration.

Qualité du code

Le code de contrat intelligent est impitoyable. Chaque bogue peut entraîner une perte monétaire. Vous ne devez pas traiter la programmation de contrats intelligents de la même manière que la programmation à usage général. Écrire des DApps dans Solidity n’est pas comme créer un widget Web en JavaScript. Vous devez plutôt appliquer des méthodologies d’ingénierie et de développement de logiciels rigoureuses, comme vous le feriez dans l’ingénierie aérospatiale ou dans toute discipline similairement impitoyable. Une fois que vous avez "lancé" votre code, vous ne pouvez pas faire grand-chose pour résoudre les problèmes.

Lisibilité/auditabilité

Votre code doit être clair et facile à comprendre. Plus c’est facile à lire, plus c’est facile à auditer. Les contrats intelligents sont publics, car tout le monde peut lire le code intermédiaire et tout le monde peut le désosser. Par conséquent, il est avantageux de développer votre travail en public, en utilisant des méthodologies collaboratives et à source libre, pour tirer parti de la sagesse collective de la communauté des développeurs et bénéficier du plus grand dénominateur commun du développement à source libre. Vous devez écrire un code bien documenté et facile à lire, en suivant les conventions de style et de nommage qui font partie de la communauté Ethereum.

Couverture de test

Testez tout ce que vous pouvez. Les contrats intelligents s’exécutent dans un environnement d’exécution public, où n’importe qui peut les exécuter avec la contribution de son choix. Vous ne devez jamais supposer que l’entrée, telle que les arguments de fonction, est bien formée, correctement délimitée ou a un objectif bénin. Testez tous les arguments pour vous assurer qu’ils se situent dans les plages attendues et qu’ils sont correctement formatés avant d’autoriser la poursuite de l’exécution de votre code.

Risques de sécurité et anti-modèles

En tant que programmeur de contrats intelligents, vous devez être familiarisé avec les risques de sécurité les plus courants, afin de pouvoir détecter et éviter les modèles de programmation qui exposent vos contrats à ces risques. Dans les sections suivantes, nous examinerons différents risques de sécurité, des exemples de la façon dont les vulnérabilités peuvent survenir et des contre-mesures ou des solutions préventives qui peuvent être utilisées pour y faire face.

Réentrance

L’une des caractéristiques des contrats intelligents Ethereum est leur capacité à appeler et à utiliser le code d’autres contrats externes. Les contrats gèrent également généralement l’ether et, en tant que tels, envoient souvent de l’ether à diverses adresses d’utilisateurs externes. Ces opérations nécessitent que les contrats soumettent des appels externes. Ces appels externes peuvent être détournés par des attaquants, qui peuvent forcer les contrats à exécuter du code supplémentaire (via une fonction de secours ), y compris des rappels vers eux-mêmes. Des attaques de ce type ont été utilisées dans le tristement célèbre piratage DAO .

Pour en savoir plus sur les attaques par réentrance , voir l’article de blog de Gus Guimareas sur le sujet et les meilleures pratiques pour les contrats intelligents Ethereum.

La vulnérabilité

Ce type d’attaque peut se produire lorsqu’un contrat envoie de l’ether à une adresse inconnue. Un attaquant peut soigneusement construire un contrat à une adresse externe qui contient du code malveillant dans la fonction de secours. Ainsi, lorsqu’un contrat envoie de l’ether à cette adresse, il invoquera le code malveillant. Généralement , le code malveillant exécute une fonction sur le contrat vulnérable, effectuant des opérations non prévues par le développeur. Le terme « réentrance » vient du fait que le contrat externe malveillant appelle une fonction sur le contrat vulnérable et que le chemin d’exécution du code le « réintègre ». Pour clarifier cela, considérons le simple contrat vulnérable dans EtherStore.sol , qui agit comme un coffre-fort Ethereum qui permet aux déposants de retirer seulement 1 ether par semaine.

Example 7. EtherStore.sol
contract EtherStore {

    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

    function depositFunds() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw);
        // limite le retrait
        require(_weiToWithdraw <= withdrawalLimit);
        // limite le temps accordé pour se rétracter
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
 }

Ce contrat a deux fonctions publiques, depositFunds et removeFunds. La fonction de dépôt de fonds incrémente simplement le solde de l’expéditeur. La fonction de retrait de fonds permet à l’expéditeur de spécifier le montant de wei à retirer. Cette fonction est destinée à réussir uniquement si le montant demandé à retirer est inférieur à 1 ether et qu’aucun retrait n’a eu lieu la semaine dernière.

La vulnérabilité se trouve à la ligne 17, où le contrat envoie à l’utilisateur la quantité d’ether demandée. Considérez un attaquant qui a créé le contrat dans Attack.sol .

Example 8. Attack.sol
import "EtherStore.sol";

contract Attack {
  EtherStore public etherStore;

  // initialise la variable etherStore avec l'adresse du contrat
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }

  function attackEtherStore() external payable {
      // attaque à l'ether le plus proche
      require(msg.value >= 1 ether);
      // envoie eth à la fonction depositFunds()
      etherStore.depositFunds.value(1 ether)();
      // début de magie
      etherStore.withdrawFunds(1 ether);
  }

  function collectEther() public {
      msg.sender.transfer(this.balance);
  }

  // fonction de secours - où la magie opère
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

Comment l’exploit peut-il se produire ? Tout d’abord, l’attaquant créerait le contrat malveillant (disons à l’adresse 0x0…​123) avec l' adresse de contrat de l' EtherStore comme seul paramètre constructeur. Cela initialiserait et pointerait la variable publique etherStore vers le contrat à attaquer.

L’attaquant appellerait alors la fonction attackEtherStore , avec une certaine quantité d’ether supérieure ou égale à 1 - supposons 1 ether pour le moment. Dans cet exemple, nous supposerons également qu’un certain nombre d’autres utilisateurs ont déposé de l’ether dans ce contrat, de sorte que son solde actuel est de 10 ether. La suite sera alors ainsi :

  1. Attack.sol , ligne 15 : La fonction DepositFunds du contrat EtherStore sera appelée avec un msg.value de 1 ether (et beaucoup de gaz). L’expéditeur (msg.sender) sera le contrat malveillant ( 0x0…​​123 ). Ainsi , balances[0x0…​123] = 1 ether.

  2. Attack.sol , ligne 17 : Le contrat malveillant va alors appeler la fonction withdrawFunds du contrat EtherStore avec un paramètre de 1 ether . Cela satisfera à toutes les exigences (lignes 12 à 16 du contrat EtherStore ) car aucun retrait précédent n’a été effectué.

  3. EtherStore.sol , ligne 17 : Le contrat renverra 1 ether au contrat malveillant.

  4. Attack.sol , ligne 25 : Le paiement au contrat malveillant exécutera alors la fonction de secours.

  5. Attack.sol , ligne 26 : Le solde total du contrat EtherStore était de 10 ether et est maintenant de 9 ether , donc cette instruction if passe.

  6. Attack.sol , ligne 27 : La fonction de secours appelle de l' EtherStore la fonction withdrawFunds à nouveau et « réintègre » le contrat EtherStore .

  7. EtherStore.sol , ligne 11 : Dans ce deuxième appel à withdrawFunds , le solde du contrat attaquant est toujours de 1 ether car la ligne 18 n’a pas encore été exécutée. Ainsi, nous avons toujours balances[0x0…​123] = 1 ether . C’est également le cas pour la variable lastWithdrawTime . Encore une fois , nous passons toutes les exigences .

  8. EtherStore.sol , ligne 17 : Le contrat attaquant retire un autre 1 ether .

  9. Répétez les étapes 4 à 8 jusqu’à ce qu’il ne soit plus le cas que EtherStore.balance > 1 , comme dicté par la ligne 26 dans Attack.sol .

  10. Attack.sol , ligne 26 : Une fois qu’il reste 1 (ou moins) d’ether dans le contrat EtherStore , cette instruction if échouera. Cela permettra alors d’exécuter les lignes 18 et 19 du contrat EtherStore (pour chaque appel à la fonction withdrawFunds).

  11. EtherStore.sol , lignes 18 et 19 : Les mappages balances et lastWithdrawTime seront définis et l’exécution se terminera.

Le résultat final est que l’attaquant a retiré tous les ethers sauf 1 du contrat EtherStore en une seule transaction.

Techniques préventives

Il existe un certain nombre de techniques courantes qui permettent d’éviter les vulnérabilités potentielles de réentrance dans les contrats intelligents. La première consiste à (dans la mesure du possible) utiliser la fonction de transfert intégrée lors de l’envoi d’ether à des contrats externes. La fonction de transfert n’envoie que 2300 gaz avec l’appel externe, ce qui n’est pas suffisant pour que l’adresse/le contrat de destination appelle un autre contrat (c’est-à-dire qu’il ressaisisse le contrat d’envoi).

La deuxième technique consiste à s’assurer que toute la logique qui modifie les variables d’état se produit avant que l’ether ne soit envoyé hors du contrat (ou de tout appel externe). Dans l' exemple EtherStore, les lignes 18 et 19 de EtherStore.sol doivent être placées avant la ligne 17. Il est recommandé que tout code effectuant des appels externes à des adresses inconnues soit la dernière opération d’une fonction localisée ou d’un morceau de code exécuté. C’est ce qu’on appelle le modèle vérifications-effets-interactions .

Une troisième technique consiste à introduire un mutex, c’est-à-dire à ajouter une variable d’état qui verrouille le contrat pendant l’exécution du code, empêchant les appels réentrants .

L’application de toutes ces techniques (l’utilisation des trois n’est pas nécessaire, mais nous le faisons à des fins de démonstration) à EtherStore.sol donne le contrat sans réentrance :

contract EtherStore {

    // initialise le mutex
    bool reEntrancyMutex = false;
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

    function depositFunds() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(!reEntrancyMutex);
        require(balances[msg.sender] >= _weiToWithdraw);
        // limite le retrait
        require(_weiToWithdraw <= withdrawalLimit);
        // limite le temps accordé pour se rétracter
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
        // définit le mutex de réentrance avant l'appel externe
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);
        // libère le mutex après l'appel externe
        reEntrancyMutex = false;
    }
 }

Exemple concret : le DAO

L’attaque DAO (Decentralized Autonomous Organization) a été l’un des principaux piratages survenus au début du développement d’Ethereum. À l’époque, le contrat détenait plus de 150 millions de dollars. La réentrance a joué un rôle majeur dans l’attaque, qui a finalement conduit à l’embranchement divergent (hard fork) qui a créé Ethereum Classic (ETC). Pour une bonne analyse de l’exploit DAO, voir http://bit.ly/2EQaLCI . Plus d’informations sur l’historique des embranchements d’Ethereum, la chronologie du piratage DAO et la naissance d’ETC dans un embranchement divergent peuvent être trouvées dans Normes Ethereum.

Dépassement et soupassement arithmétique

La machine virtuelle Ethereum spécifie des types de données de taille fixe pour les entiers. Cela signifie qu’une variable entière ne peut représenter qu’une certaine plage de nombres. Un uint8 , par exemple, ne peut stocker que des nombres dans la plage [0,255]. Essayer de stocker 256 dans un uint8 donnera 0 . Si l’on n’y prend pas garde, les variables de Solidity peuvent être exploitées si la saisie de l’utilisateur n’est pas cochée et si des calculs sont effectués qui aboutissent à des nombres qui se situent en dehors de la plage du type de données qui les stocke.

La vulnérabilité

Un dépassement/soupassement se produit lorsqu’une opération effectuée nécessite une variable de taille fixe pour stocker un nombre (ou un élément de données) qui est en dehors de la plage du type de données de la variable.

Par exemple, soustraire 1 d’une variable uint8 (entier non signé de 8 bits, c’est-à-dire non négatif) dont la valeur est 0 donnera le nombre 255 . Il s’agit d’un soupassement . Nous avons attribué un nombre en dessous de la plage de uint8 , de sorte que le résultat est une boucle et donne le plus grand nombre qu’un uint8 puisse stocker. De même, ajouter 2^8=256 à un uint8 laissera la variable inchangée, car nous avons enroulé autour de toute la longueur du uint . Deux analogies simples de ce comportement sont les odomètres dans les voitures, qui mesurent la distance parcourue (ils se réinitialisent à 000000, après que le plus grand nombre, c’est-à-dire 999999, est dépassé) et les fonctions mathématiques périodiques (l’ajout de 2 π à l’argument de sin laisse la valeur inchangée ).

L’ajout de nombres supérieurs à la plage du type de données est appelé un dépassement . Pour plus de clarté, ajouter 257 à un uint8 qui a actuellement une valeur de 0 se traduira par le nombre 1 . Il est parfois instructif de considérer les variables de taille fixe comme étant cycliques, où nous recommençons à partir de zéro si nous ajoutons des nombres au-dessus du plus grand nombre stocké possible, et commençons à compter à partir du plus grand nombre si nous soustrayons de zéro. Dans le cas des types int signés , qui peuvent représenter des nombres négatifs, nous recommençons une fois que nous atteignons la plus grande valeur négative; par exemple, si nous essayons de soustraire 1 à un int8 dont la valeur est -128 , nous obtiendrons 127 .

Ces types de pièges numériques permettent aux attaquants de mal utiliser le code et de créer des flux logiques inattendus. Par exemple, considérez le contrat TimeLock dans TimeLock.sol.

Example 9. TimeLock.sol
contract TimeLock {

    mapping(address => uint) public balances;
    mapping(address => uint) public lockTime;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
        lockTime[msg.sender] = now + 1 weeks;
    }

    function increaseLockTime(uint _secondsToIncrease) public {
        lockTime[msg.sender] += _secondsToIncrease;
    }

    function withdraw() public {
        require(balances[msg.sender] > 0);
        require(now > lockTime[msg.sender]);
        balances[msg.sender] = 0;
        msg.sender.transfer(balance);
    }
}

Ce contrat est conçu pour agir comme un coffre-fort temporel : les utilisateurs peuvent déposer de l’ether dans le contrat et il y sera verrouillé pendant au moins une semaine. L’utilisateur peut prolonger le temps d’attente à plus d’une semaine s’il le souhaite, mais une fois déposé, l’utilisateur peut être sûr que son ether est verrouillé en toute sécurité pendant au moins une semaine, du moins c’est ce que prévoit ce contrat.

Dans le cas où un utilisateur est obligé de remettre sa clé privée, un contrat comme celui-ci peut être utile pour s’assurer que son ether est introuvable pendant une courte période. Mais si un utilisateur avait verrouillé 100 ether dans ce contrat et remis ses clés à un attaquant, l’attaquant pourrait utiliser un dépassement pour recevoir l’ether, quel que soit le lockTime.

L’attaquant pourrait déterminer le lockTime actuel pour l’adresse pour laquelle il détient maintenant la clé (c’est une variable publique). Appelons le userLockTime . Ils pourraient alors appeler la fonction increaseLockTime et passer en argument le nombre 2^256 - userLockTime . Ce nombre serait ajouté à l' userLockTime actuel et provoquerait un dépassement, réinitialisant lockTime[msg.sender] à 0. L’attaquant pourrait alors simplement appeler la fonction withdraw pour obtenir sa récompense.

ALERTE DIVULGATION: Si vous n’avez pas encore fait les défis Ethernaut, cela donne une solution à l’un des niveaux .

Example 10. Underflow vulnerability example from Ethernaut challenge
pragma solidity ^0.4.18;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  function Token(uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public constant returns (uint balance) {
    return balances[_owner];
  }
}

Il s’agit d’un simple contrat de jeton qui utilise une fonction de transfer , permettant aux participants de déplacer leurs jetons. Pouvez-vous voir l’erreur dans ce contrat ?

La faille vient de la fonction transfer. L’instruction require de la ligne 13 peut être contournée à l’aide d’un soupassement. Considérez un utilisateur avec un solde nul. Il pourrait appeler la fonction transfer avec n’importe quelle _value différente de zéro et passer l’instruction require à la ligne 13. En effet, balances[msg.sender] est égal à 0 (et un uint256 ), donc la soustraction de tout montant positif (à l’exception de 2^256 ) entraînera un nombre positif, comme décrit précédemment. Ceci est également vrai pour la ligne 14, où le solde sera crédité d’un nombre positif. Ainsi, dans cet exemple, un attaquant peut obtenir des jetons gratuits en raison d’une vulnérabilité de soupassement.

Techniques préventives

La technique conventionnelle actuelle pour se prémunir contre les vulnérabilités de soupassement/dépassement consiste à utiliser ou à créer des bibliothèques mathématiques qui remplacent les opérateurs mathématiques standard d' addition, de soustraction et de multiplication (la division est exclue car elle ne provoque pas de soupassement/dépassement et l’EVM revient à la division par 0 ).

OpenZeppelin a fait un excellent travail de création et d’audit de bibliothèques sécurisées pour la communauté Ethereum. En particulier, la bibliothèque SafeMath peut être utilisée pour éviter les vulnérabilités de soupassement/dépassement.

Pour montrer comment ces bibliothèques sont utilisées dans Solidity, corrigeons le contrat TimeLock à l’aide de la bibliothèque SafeMath. La version sans dépassement du contrat est :

library SafeMath {

  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert( b > 0); // Solidity relance automatiquement lors de la division par 0
    uint256 c = a / b;
    // assert( a == b * c + a % b); // Cela vaut dans tous les cas
    return c;
  }

  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

contract TimeLock {
    using SafeMath for uint; // utiliser la bibliothèque pour le type uint
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lockTime;

    function deposit() external payable {
        balances[msg.sender] = balances[msg.sender].add(msg.value);
        lockTime[msg.sender] = now.add(1 weeks);
    }

    function increaseLockTime(uint256 _secondsToIncrease) public {
        lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
    }

    function withdraw() public {
        require(balances[msg.sender] > 0);
        require(now > lockTime[msg.sender]);
        balances[msg.sender] = 0;
        msg.sender.transfer(balance);
    }
}

Notez que toutes les opérations mathématiques standard ont été remplacées par celles définies dans la bibliothèque SafeMath. Le contrat TimeLock n’effectue plus aucune opération capable de soupassement/dépassement.

Exemples concrets : PoWHC et dépassement de transfert par lots (CVE-2018–10299)

Proof of Weak Hands Coin ( PoWHC ), conçu à l’origine comme une sorte de blague, était un stratagème de Ponzi écrit par un collectif Internet. Malheureusement , il semble que l’auteur ou les auteurs du contrat n’avaient pas vu de soupassement/dépassement auparavant, et par conséquent 866 ethers ont été libérés de son contrat. Eric Banisadr donne un bon aperçu de la façon dont le dépassement s’est produit (ce qui n’est pas trop différent du défi Ethernaut décrit précédemment) dans son article de blog sur l’événement.

Un autre exemple provient de l’implémentation d’une fonction batchTransfer() dans un groupe de contrats de jetons ERC20. L’implémentation contenait une vulnérabilité de dépassement; vous pouvez lire les détails dans le compte rendu de PeckShield.

Ether inattendu

En règle générale, lorsque de l’ether est envoyé à un contrat, il doit exécuter soit la fonction de secours, soit une autre fonction définie dans le contrat. Il y a deux exceptions à cela, où l’ether peut exister dans un contrat sans avoir exécuté de code. Les contrats qui reposent sur l’exécution de code pour tout l’ether qui leur est envoyé peuvent être vulnérables aux attaques où l’ether est envoyé de force.

La vulnérabilité

Une technique de programmation défensive courante qui est utile pour appliquer des transitions d’état correctes ou valider des opérations est la vérification invariante . Cette technique consiste à définir un ensemble d’invariants (métriques ou paramètres qui ne doivent pas changer) et à vérifier qu’ils restent inchangés après une (ou plusieurs) opération(s). C’est généralement une bonne conception, à condition que les invariants vérifiés soient en fait des invariants. Un exemple d’invariant est totalSupply d’une émission fixe de Jeton ERC20 . Comme aucune fonction ne doit modifier cet invariant, on pourrait ajouter une vérification à la fonction transfer qui s’assure que le totalSupply reste inchangé, pour garantir que la fonction fonctionne comme prévu.

En particulier, il existe un invariant apparent qu’il peut être tentant d’utiliser mais qui peut en fait être manipulé par des utilisateurs externes (quelles que soient les règles mises en place dans le contrat intelligent). Il s’agit de l’ether actuel stocké dans le contrat. Souvent, lorsque les développeurs apprennent Solidity pour la première fois , ils ont l’idée fausse qu’un contrat ne peut accepter ou obtenir de l’ether que via des fonctions payantes. Cette idée fausse peut conduire à des contrats qui contiennent de fausses hypothèses sur l’équilibre de l’ether, ce qui peut entraîner une série de vulnérabilités. La preuve irréfutable de cette vulnérabilité est l’utilisation (incorrecte) de this.balance.

Il existe deux manières d’envoyer (de force) de l’ether à un contrat sans utiliser de fonction payante ni exécuter de code sur le contrat :

Autodestruction/suicide

Tout contrat est capable d' implémenter la fonction selfdestruct , qui supprime tout le code intermédiaire de l’adresse du contrat et envoie tout l’ether qui y est stocké à l’adresse spécifiée par le paramètre. Si cette adresse spécifiée est également un contrat, aucune fonction (y compris la fonction de secours) n’est appelée. Par conséquent, la fonction selfdestruct peut être utilisée pour envoyer de force de l’ether à n’importe quel contrat, quel que soit le code pouvant exister dans le contrat, même les contrats sans fonctions payantes. Cela signifie que tout attaquant peut créer un contrat avec une fonction selfdestruct , lui envoyer de l’ether, appeler selfdestruct(target) et forcer l’ether à être envoyé à un contrat target . Martin Swende a un excellent article de blog décrivant certaines bizarreries de l’opcode d’autodestruction (Quirk # 2) ainsi qu’un compte rendu de la façon dont les nœuds clients vérifiaient des invariants incorrects, ce qui aurait pu conduire à un crash plutôt catastrophique du réseau Ethereum.

Ether pré-envoyé

Une autre façon d’intégrer de l’ether dans un contrat consiste à précharger l’adresse du contrat avec de l’ether. Les adresses de contrat sont déterministes - en fait, l’adresse est calculée à partir du hachage Keccak-256 (généralement synonyme de SHA-3) de l’adresse créant le contrat et du nonce de transaction qui crée le contrat. Plus précisément, il se présente sous la forme address = sha3(rlp.encode([ account_address,transaction_nonce ])) (voir la discussion d’Adrian Manning sur "Keyless Ether" pour quelques cas d’utilisation amusants). Cela signifie que n’importe qui peut calculer quelle sera l’adresse d’un contrat avant sa création et envoyer de l’ether à cette adresse. Lorsque le contrat est créé, il aura un solde d’ether non nul.

Explorons quelques pièges qui peuvent survenir compte tenu de ces connaissances. Considérez le contrat trop simple dans EtherGame.sol.

Example 11. EtherGame.sol
contract EtherGame {

    uint public payoutMileStone1 = 3 ether;
    uint public mileStone1Reward = 2 ether;
    uint public payoutMileStone2 = 5 ether;
    uint public mileStone2Reward = 3 ether;
    uint public finalMileStone = 10 ether;
    uint public finalReward = 5 ether;

    mapping(address => uint) redeemableEther;
    // Les utilisateurs paient 0,5 ether. À des étapes spécifiques, créditez leurs comptes.
    function play() external payable {
        require(msg.value == 0.5 ether); // chaque jeu est 0.5 ether
        uint currentBalance = this.balance + msg.value;
        // s'assure qu'il n'y a plus de joueurs après la fin du jeu
        require(currentBalance <= finalMileStone);
        // si à un jalon, créditer le compte du joueur
        if (currentBalance == payoutMileStone1) {
            redeemableEther[msg.sender] += mileStone1Reward;
        }
        else if (currentBalance == payoutMileStone2) {
            redeemableEther[msg.sender] += mileStone2Reward;
        }
        else if (currentBalance == finalMileStone ) {
            redeemableEther[msg.sender] += finalReward;
        }
        return;
    }

    function claimReward() public {
        // s'assure que le jeu est terminé
        require(this.balance == finalMileStone);
        // s'assure qu'il y a une récompense à donner
        require(redeemableEther[msg.sender] > 0);
        redeemableEther[msg.sender] = 0;
        msg.sender.transfer(transferValue);
    }
 }

Ce contrat représente un jeu simple (qui impliquerait naturellement des conditions de course) où les joueurs envoient 0,5 ether au contrat dans l’espoir d’être le joueur qui atteint l’un des trois jalons en premier. Les jalons sont libellés en ether. Le premier à atteindre le jalon peut réclamer une partie de l’ether à la fin de la partie. Le jeu se termine lorsque le dernier jalon (10 ether) est atteint; les utilisateurs peuvent ensuite réclamer leurs récompenses.

Les problèmes avec le contrat EtherGame proviennent de la mauvaise utilisation de this.balance dans les deux lignes 14 (et par association 16) et 32. Un attaquant malicieux pourrait envoyer de force une petite quantité d’ether, disons 0,1 ether, via la fonction selfdestruct ( discuté plus tôt) pour empêcher tout futur joueur d’atteindre un jalon. this.balance ne sera jamais un multiple de 0,5 ether grâce à cette contribution de 0,1 ether, car tous les joueurs légitimes ne peuvent envoyer que des incréments de 0,5 ether. Cela empêche toutes les conditions if des lignes 18, 21 et 24 d’être vraies.

Pire encore, un attaquant vengeur qui a raté un jalon pourrait envoyer de force 10 ethers (ou une quantité équivalente d’ethers qui pousse le solde du contrat au-dessus du finalMileStone ), ce qui verrouillerait toutes les récompenses dans le contrat pour toujours. En effet, la fonction claimReward sera toujours rétablie, en raison de l’exigence à la ligne 32 (c’est-à-dire parce que this.balance est supérieur à finalMileStone ).

Techniques préventives

Ce type de vulnérabilité provient généralement d’une mauvaise utilisation de this.balance. La logique contractuelle, dans la mesure du possible, doit éviter de dépendre des valeurs exactes du solde du contrat, car elle peut être artificiellement manipulée. Si vous appliquez une logique basée sur this.balance , vous devez faire face à des soldes inattendus.

Si des valeurs exactes d’ether déposé sont requises, une variable auto-définie doit être utilisée qui est incrémentée dans les fonctions payables, pour suivre en toute sécurité l’ether déposé. Cette variable ne sera pas influencée par l’ether forcé envoyé via un appel selfdestruct.

contract EtherGame {

    uint public payoutMileStone1 = 3 ether;
    uint public mileStone1Reward = 2 ether;
    uint public payoutMileStone2 = 5 ether;
    uint public mileStone2Reward = 3 ether;
    uint public finalMileStone = 10 ether;
    uint public finalReward = 5 ether;
    uint public depositedWei;

    mapping (address => uint) redeemableEther;

    function play() external payable {
        require(msg.value == 0.5 ether);
        uint currentBalance = depositedWei + msg.value;
        // s'assure qu'il n'y a plus de joueurs après la fin du jeu
        require(currentBalance <= finalMileStone);
        if (currentBalance == payoutMileStone1) {
            redeemableEther[msg.sender] += mileStone1Reward;
        }
        else if (currentBalance == payoutMileStone2) {
            redeemableEther[msg.sender] += mileStone2Reward;
        }
        else if (currentBalance == finalMileStone ) {
            redeemableEther[msg.sender] += finalReward;
        }
        depositedWei += msg.value;
        return;
    }

    function claimReward() public {
        // s'assure que le jeu est terminé
        require(depositedWei == finalMileStone);
        // s'assure qu'il y a une récompense à donner
        require(redeemableEther[msg.sender] > 0);
        redeemableEther[msg.sender] = 0;
        msg.sender.transfer(transferValue);
    }
 }

Ici, nous avons créé une nouvelle variable, depositWei , qui garde la trace de l’ether connu déposé, et c’est cette variable que nous utilisons pour nos tests. Notez que nous n’avons plus aucune référence à this.balance.

Autres exemples

Quelques exemples de contrats exploitables ont été donnés dans le Underhanded Solidity Coding Contest , qui fournit également des exemples détaillés d’un certain nombre de pièges soulevés dans cette section.

DELEGATECALL (Appel délégué)

Les opcodes CALL et DELEGATECALL sont utiles pour permettre aux développeurs Ethereum de modulariser leur code. Les appels de messages externes standard aux contrats sont gérés par l' opcode CALL, le code étant exécuté dans le contexte du contrat/de la fonction externe. L' opcode DELEGATECALL est presque identique, sauf que le code exécuté à l’adresse ciblée est exécuté dans le contexte du contrat appelant, et msg.sender et msg.value restent inchangés. Cette fonctionnalité permet la mise en œuvre de bibliothèques , permettant aux développeurs de déployer du code réutilisable une seule fois et de l’appeler à partir de futurs contrats.

Bien que les différences entre ces deux opcodes soient simples et intuitives, l’utilisation de DELEGATECALL peut conduire à l’exécution de code inattendu.

Pour plus de lecture, voir  http://bit.ly/2AAElb8[Question Ethereum Stack Exchange sur ce sujet] de Loi.Luu et la documentation Solidity .

La vulnérabilité

En raison de la nature de préservation du contexte de DELEGATECALL , la création de bibliothèques personnalisées sans vulnérabilité n’est pas aussi simple qu’on pourrait le penser. Le code des bibliothèques elles-mêmes peut être sécurisé et sans vulnérabilité ; cependant, lorsqu’il est exécuté dans le contexte d’une autre application, de nouvelles vulnérabilités peuvent survenir. Voyons un exemple assez complexe de cela, en utilisant des nombres de Fibonacci.

Considérez la bibliothèque dans FibonacciLib.sol, qui peut générer la séquence de Fibonacci et des séquences de forme similaire. (Remarque : ce code a été modifié à partir de https://bit.ly/2MReuii .)

Example 12. FibonacciLib.sol
// contrat de bibliothèque - calcule les nombres de type Fibonacci
contract FibonacciLib {
    // initialisation de la suite standard de Fibonacci
    uint public start;
    uint public calculatedFibNumber;

    // modifie le numéro zéro de la séquence
    function setStart(uint _start) public {
        start = _start;
    }

    function setFibonacci(uint n) public {
        calculatedFibNumber = fibonacci(n);
    }

    function fibonacci(uint n) internal returns (uint) {
        if (n == 0) return start;
        else if (n == 1) return start + 1;
        else return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

Cette bibliothèque fournit une fonction qui peut générer le n-ème nombre de Fibonacci dans la séquence. Il permet aux utilisateurs de changer le numéro de départ de la séquence (start) et de calculer les n-èmes nombres de type Fibonacci dans cette nouvelle séquence.

Considérons maintenant un contrat qui utilise cette bibliothèque, montré dans FibonacciBalance.sol.

Example 13. FibonacciBalance.sol
contract FibonacciBalance {

    address public fibonacciLibrary;
    // le nombre de Fibonacci actuel à retirer
    uint public calculatedFibNumber;
    // le numéro de séquence de Fibonacci de départ
    uint public start = 3;
    uint public withdrawalCounter;
    // le sélecteur de fonction de Fibonancci
    bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));

    // constructeur - charge le contrat avec ether
    constructor(address _fibonacciLibrary) external payable {
        fibonacciLibrary = _fibonacciLibrary;
    }

    function withdraw() {
        withdrawalCounter += 1;
        // calcule le nombre de Fibonacci pour l'utilisateur de retrait actuel-
        // ceci définit le nombreFibcalculé
        require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
        msg.sender.transfer(calculatedFibNumber * 1 ether);
    }

    // permet aux utilisateurs d'appeler les fonctions de la bibliothèque Fibonacci
    function() public {
        require(fibonacciLibrary.delegatecall(msg.data));
    }
}

Ce contrat permet à un participant de retirer des ethers du contrat, la quantité d’ethers étant égale au nombre de Fibonacci correspondant à l’ordre de retrait du participant ; c’est-à-dire que le premier participant obtient 1 ether, le second obtient également 1, le troisième obtient 2, le quatrième obtient 3, le cinquième 5, et ainsi de suite (jusqu’à ce que le solde du contrat soit inférieur au nombre de Fibonacci retiré).

Il y a un certain nombre d' éléments dans ce contrat qui peuvent nécessiter quelques explications. Tout d’abord, il existe une variable intéressante, fibSig. Cela contient les 4 premiers octets du hachage Keccak-256 (SHA-3) de la chaîne 'setFibonacci (uint256)'. Ceci est connu sous le nom de sélecteur de fonction et est placé dans calldata pour spécifier quelle fonction d’un contrat intelligent sera appelée. Il est utilisé dans la fonction d’appel délégué à la ligne 21 pour spécifier que nous souhaitons exécuter la fonction fibonacci(uint256). Le deuxième argument est delegatecall le paramètre que nous passons à la fonction. Deuxièmement, nous supposons que l’adresse de la bibliothèque FibonacciLib est correctement référencée dans le constructeur (Référencement des contrats externes traite de certaines vulnérabilités potentielles liées à ce type d’initialisation de référence de contrat).

Pouvez-vous repérer des erreurs dans ce contrat ? Si l’on déployait ce contrat, le remplissait d’ether et appelait withdraw, il reviendrait probablement.

Vous avez peut-être remarqué que la variable d’état start est utilisée à la fois dans la bibliothèque et dans le contrat d’appel principal. Dans le contrat de bibliothèque, start est utilisé pour spécifier le début de la séquence de Fibonacci et est défini sur 0 , alors qu’il est défini sur 3 dans le contrat appelant. Vous avez peut-être également remarqué que la fonction de secours dans le contrat FibonacciBalance permet de transmettre tous les appels au contrat de bibliothèque, ce qui permet d’appeler la fonction setStart du contrat de bibliothèque. En rappelant que nous préservons l’état du contrat, il peut sembler que cette fonction permettrait de changer l’état de la variable start dans le contrat local FibonnacciBalance. Si c’est le cas, cela permettrait de retirer plus d’ether, car le calculatedFibNumber résultant dépend de la variable start (comme indiqué dans le contrat de bibliothèque). En réalité , la fonction setStart ne modifie pas (et ne peut pas modifier) la variable start dans le contrat FibonacciBalance. La vulnérabilité sous-jacente de ce contrat est bien pire que la simple modification de la variable start.

Avant de discuter du problème réel, faisons un petit détour pour comprendre comment les variables d’état sont réellement stockées dans les contrats. Les variables d’état ou de stockage (variables qui persistent sur des transactions individuelles) sont placées dans des emplacements de manière séquentielle au fur et à mesure qu’elles sont introduites dans le contrat. (Il y a quelques complexités ici ; consultez la documentation de Solidity pour une compréhension plus approfondie.)

Prenons l’exemple du contrat de bibliothèque. Il a deux variables d’état, start et calculatedFibNumber . La première variable, start, est stockée dans le stockage du contrat à slot[0] (c’est-à-dire le premier slot (fente ou espace)). La deuxième variable, calculatedFibNumber, est placée dans le prochain emplacement de stockage disponible, slot[1]. La fonction setStart prend une entrée et définit start quelle que soit l’entrée. Cette fonction définit donc slot[0] sur l’entrée que nous fournissons dans la fonction setStart. De la même manière, la fonction setFibonacci définit la fonction calculatedFibNumber sur le résultat de fibonacci(n). Encore une fois, il s’agit simplement de définir storage slot[1] sur la valeur de fibonacci(n).

Regardons maintenant le contrat FibonacciBalance. Storage slot[0] correspond maintenant à l' adresse fibonacciLibrary , et slot[1] correspond à calculatedFibNumber . C’est dans ce mappage incorrect que la vulnérabilité se produit. delegatecall préserve le contexte du contrat . Cela signifie que le code exécuté via l’appel délégué delegatecall agira sur l’état (c’est-à-dire le stockage) du contrat appelant.

Notez maintenant que withdraw à la ligne 21, nous exécutons fibonacciLibrary.delegatecall(fibSig,withdrawalCounter). Cela appelle la fonction setFibonacci, qui, comme nous l’avons vu, modifie l’espace de stockage slot[1] , qui dans notre contexte actuel est calculatedFibNumber . C’est comme prévu (c’est-à-dire qu’après l’exécution, le calculatedFibNumber est modifié). Cependant, rappelez-vous que la variable de début dans le contrat FibonacciLib est située dans l’espace de stockage slot[0], qui est l' adresse fibonacciLibrary dans le contrat actuel. Cela signifie que la fonction fibonacci donnera un résultat inattendu. En effet, il fait référence à start ( slot[0] ), qui dans le contexte d’appel actuel est l' adresse fibonacciLibrary (qui sera souvent assez grande, lorsqu’elle est interprétée comme un uint). Ainsi , il est probable que la fonction de retrait withdraw reviendra, car elle ne contiendra pas la quantité uint(fibonacciLibrary) d’ether, ce qui est ce que calculatedFibNumber renverra.

Pire encore, le contrat FibonacciBalance permet aux utilisateurs d’appeler toutes les fonctions fibonacciLibrary via la fonction de secours à la ligne 26. Comme nous l’avons vu précédemment, cela inclut la fonction setStart. Nous avons expliqué que cette fonction permet à quiconque de modifier ou de définir l' emplacement de stockage slot[0]. Dans ce cas, emplacement de stockage slot[0] est l' adresse fibonacciLibrary. Par conséquent, un attaquant pourrait créer un contrat malveillant, convertir l’adresse en uint (cela peut être fait facilement en Python en utilisant int('<address>',16) ), puis appeler setStart ([attack_contract_address_as_uint]) . Cela changera fibonacciLibrary à l’adresse du contrat d’attaque. Ensuite, chaque fois qu’un utilisateur appelle withdraw ou la fonction de secours, le contrat malveillant s’exécutera (ce qui peut voler la totalité du solde du contrat) car nous avons modifié l’adresse réelle de fibonacciLibrary. Un exemple d’un tel contrat d’attaque serait :

contract Attack {
    uint storageSlot0; // correspond à fibonacciLibrary
    uint storageSlot1; // correspond à calculatedFibNumber

    // défaut - cela s'exécutera si une fonction spécifiée n'est pas trouvée
    function() public {
        storageSlot1 = 0; // nous définissons calculateFibNumber sur 0, donc si retirer
        // s'appelle nous n'envoyons pas d'ether
        <attacker_address>.transfer(this.balance); // on prend tout l'ether
    }
 }

Notez que ce contrat d’attaque modifie le calculatedFibNumber en changeant l’espace de stockage slot[1]. En principe, un attaquant pourrait modifier n’importe quel autre emplacement de stockage de son choix, pour effectuer toutes sortes d’attaques sur ce contrat. Nous vous encourageons à mettre ces contrats dans Remix et à expérimenter différents contrats d’attaque et changements d’état via ces fonctions d’appel délégué delegatecall.

Il est également important de noter que lorsque nous disons que l’appel delegatecall préserve l’état, nous ne parlons pas des noms de variables du contrat, mais plutôt des emplacements de stockage réels vers lesquels ces noms pointent. Comme vous pouvez le voir dans cet exemple, une simple erreur peut conduire un attaquant à détourner l’intégralité du contrat et de son ether.

Techniques préventives

Solidity fournit le mot-clé de library (bibliothèque) pour la mise en œuvre des contrats de bibliothèque (voir la documentation pour plus de détails). Cela garantit que le contrat de bibliothèque est sans état et non autodestructible. Forcer les bibliothèques à être sans état atténue les complexités du contexte de stockage démontrées dans cette section. Les bibliothèques sans état empêchent également les attaques dans lesquelles les attaquants modifient directement l’état de la bibliothèque afin d' affecter les contrats qui dépendent du code de la bibliothèque. En règle générale, lorsque vous utilisez DELEGATECALL, faites très attention au contexte d’appel possible du contrat de bibliothèque et du contrat d’appel, et chaque fois que possible, créez des bibliothèques. sans état.

Exemple concret : Parity Multisig Wallet (Second Hack)

Le deuxième hack de Parity Multisig Wallet est un exemple de la façon dont un code de bibliothèque bien écrit peut être exploité s’il est exécuté en dehors de son contexte prévu. Il existe un certain nombre de bonnes explications de ce hack, telles que "Parity Multisig Hacked. Encore une fois » et « Un examen approfondi du bogue multisig de parity ».

Pour compléter ces références, explorons les contrats qui ont été exploités. Les contrats de bibliothèque et de portefeuille peuvent être trouvés sur GitHub .

Le contrat de bibliothèque est le suivant :

contract WalletLibrary is WalletEvents {

  ...

  // lancer sauf si le contrat n'est pas encore initialisé.
  modifier only_uninitialized { if (m_numOwners > 0) throw; _; }

  // constructeur - il suffit de transmettre le tableau propriétaire à multipropriété et
  // la limite à daylimit
  function initWallet(address[] _owners, uint _required, uint _daylimit)
      only_uninitialized {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }

  // tue le contrat en envoyant tout à `_to`.
  function kill(address _to) onlymanyowners(sha3(msg.data)) external {
    suicide(_to);
  }

  ...

}

Et voici le contrat de portefeuille :

contract Wallet is WalletEvents {

  ...
  // MÉTHODES

  // est appelé lorsqu'aucune autre fonction ne correspond
  function() payable {
    // juste envoyer de l' argent ?
    if (msg.value > 0)
      Deposit(msg.sender, msg.value);
    else if (msg.data.length > 0)
      _walletLibrary.delegatecall(msg.data);
  }

  ...

  // DES CHAMPS
  address constant _walletLibrary =
    0xcafecafecafecafecafecafecafecafecafecafe;
}

Notez que le contrat Wallet transmet essentiellement tous les appels au contrat WalletLibrary via un appel délégué. L’adresse constante _walletLibrary dans cet extrait de code agit comme un espace réservé pour le déploiement réel de contrat WalletLibrary (qui était à 0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4 ).

L’opération voulue de ces contrats est d’avoir un portefeuille Wallet déployable à faible coût dont la base de code et les principales fonctionnalités se trouvaient dans le contrat WalletLibrary. Malheureusement, le contrat WalletLibrary est lui-même un contrat et conserve son propre état. Pouvez-vous voir pourquoi cela pourrait être un problème?

Il est possible d’envoyer des appels au contrat WalletLibrary lui-même. Plus précisément, le contrat WalletLibrary pourrait être initialisé et devenir propriétaire. En fait, un utilisateur a fait cela, appelant la fonction initWallet sur le contrat WalletLibrary et devenant propriétaire du contrat de bibliothèque. Le même utilisateur a ensuite appelé la fonction kill . Étant donné que l’utilisateur était propriétaire du contrat de bibliothèque, le modificateur a été adopté et le contrat de bibliothèque s’est auto-détruit. Comme tous les contrats Wallet existants se réfèrent à ce contrat de bibliothèque et ne contiennent aucune méthode pour modifier cette référence, toutes leurs fonctionnalités, y compris la possibilité de retirer de l’ether, ont été perdues avec le contrat WalletLibrary . En conséquence, tout l’ether de tous les portefeuilles multisig Parity de ce type a été instantanément perdu ou définitivement irrécupérable..

Visibilités par défaut

Les fonctions dans Solidity ont des spécificateurs de visibilité qui dictent comment elles peuvent être appelées. La visibilité détermine si une fonction peut être appelée en externe par les utilisateurs, par d’autres contrats dérivés, uniquement en interne ou uniquement en externe. Il existe quatre spécificateurs de visibilité, qui sont décrits en détail dans la documentation Solidity. Les fonctions sont par défaut à public , permettant aux utilisateurs de les appeler de l’extérieur. Nous allons maintenant voir comment une utilisation incorrecte des spécificateurs de visibilité peut entraîner des vulnérabilités dévastatrices dans les contrats intelligents.

La vulnérabilité

La visibilité par défaut des fonctions est public , donc les fonctions qui ne spécifient pas leur visibilité pourront être appelées par des utilisateurs externes. Le problème survient lorsque les développeurs omettent par erreur les spécificateurs de visibilité sur les fonctions qui devraient être privées (ou uniquement appelables dans le contrat lui-même).

Explorons rapidement un exemple trivial :

contract HashForEther {

    function withdrawWinnings() {
        // Gagnant si les 8 derniers caractères hexadécimaux de l'adresse sont 0
        require(uint32(msg.sender) == 0);
        _sendWinnings();
     }

     function _sendWinnings() {
         msg.sender.transfer(this.balance);
     }
}

Ce contrat simple est conçu pour agir comme un jeu de prime de devinette d’adresse. Pour remporter le solde du contrat, un utilisateur doit générer une adresse Ethereum dont les 8 derniers caractères hexadécimaux sont 0. Une fois atteint, il peut appeler la fonction withdrawWinnings pour obtenir sa prime.

Malheureusement, la visibilité des fonctions n’a pas été précisée. En particulier, la fonction _sendWinnings est public (par défaut), et donc n’importe quelle adresse peut appeler cette fonction pour voler la prime.

Techniques préventives

Il est de bonne pratique de toujours spécifier la visibilité de toutes les fonctions dans un contrat, même si elles sont intentionnellement public. Les versions récentes de solc affichent un avertissement pour les fonctions qui n’ont pas de visibilité explicite définie, pour encourager cette pratique.

Exemple concret : Parity Multisig Wallet (premier hack)

Dans le premier multisig hack de Parity, environ 31 millions de dollars d’ether ont été volés, principalement dans trois portefeuilles. Un bon récapitulatif de la façon exacte dont cela a été fait est donné par Haseeb Qureshi.

Essentiellement, le portefeuille multisig est construit à partir d’un contrat Wallet de base, qui appelle un contrat de bibliothèque contenant la fonctionnalité de base (comme décrit dans Exemple concret : Parity Multisig Wallet (Second Hack)). Le contrat de bibliothèque contient le code pour initialiser le portefeuille, comme le montre l’extrait suivant :

contract WalletLibrary is WalletEvents {

  ...

  // MÉTHODES

  ...

  // le constructeur reçoit le nombre de sigs requis pour faire des
  // transactions protégés "seulement pour ces propriétaires" ainsi que
  //  la sélection des adresses capable de les confirmer
  function initMultiowned(address[] _owners, uint _required) {
    m_numOwners = _owners.length + 1;
    m_owners[1] = uint(msg.sender);
    m_ownerIndex[uint(msg.sender)] = 1;
    for (uint i = 0; i < _owners.length; ++i)
    {
      m_owners[2 + i] = uint(_owners[i]);
      m_ownerIndex[uint(_owners[i])] = 2 + i;
    }
    m_required = _required;
  }

  ...

  // constructeur - il suffit de transmettre le tableau propriétaire à multipropriété et
  // la limite à daylimit
  function initWallet(address[] _owners, uint _required, uint _daylimit) {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }
}

Notez qu’aucune des fonctions ne spécifie leur visibilité, donc les deux sont par défaut public . La fonction initWallet est appelée dans le constructeur du portefeuille et définit les propriétaires du portefeuille multisig comme on peut le voir dans la fonction initMultiowned . Étant donné que ces fonctions ont été accidentellement laissées publiques , un attaquant a pu appeler ces fonctions sur des contrats déployés, réinitialisant la propriété à l’adresse de l’attaquant. En tant que propriétaire, l’attaquant a ensuite vidé les portefeuilles de tout leur ether.

Illusion d’entropie

Toutes les transactions sur la chaîne de blocs Ethereum sont des opérations de transition d’état déterministes. Cela signifie que chaque transaction modifie l’état global de l' écosystème Ethereum de manière calculable, sans incertitude. Cela a pour implication fondamentale qu’il n’y a pas de source d’entropie ou d’aléatoire dans Ethereum. Atteindre une entropie décentralisée (aléatoire) est un problème bien connu pour lequel de nombreuses solutions ont été proposées, y compris RANDAO , ou en utilisant une chaîne de hachages, comme décrit par Vitalik Buterin dans le billet de blog « Validator Ordering and Randomness in PoS ».

La vulnérabilité

Certains des premiers contrats construits sur la plate-forme Ethereum étaient basés sur le jeu. Fondamentalement, le jeu nécessite de l’incertitude (quelque chose sur lequel parier), ce qui rend la construction d’un système de jeu sur la chaîne de blocs (un système déterministe) plutôt difficile. Il est clair que l' incertitude doit provenir d’une source extérieure à la chaîne de blocs. C’est possible pour les paris entre joueurs (voir par exemple la technique commit–reveal) ; cependant, c’est beaucoup plus difficile si vous voulez mettre en place un contrat pour agir en tant que "croupier" (comme au blackjack ou à la roulette). Un écueil courant consiste à utiliser des variables de bloc futures, c’est-à-dire des variables contenant des informations sur le bloc de transaction dont les valeurs ne sont pas encore connues, telles que des hachages, des horodatages, des numéros de bloc ou des limites de gaz. Le problème avec ceux-ci est qu’ils sont contrôlés par le mineur qui exploite le bloc et, en tant que tels, ne sont pas vraiment aléatoires. Considérez, par exemple, un contrat intelligent de roulette avec une logique qui renvoie un nombre noir si le hachage du bloc suivant se termine par un nombre pair. Un mineur (ou pool de mineurs) pourrait parier 1 million de dollars sur le noir. S’ils résolvent le bloc suivant et trouvent que le hachage se termine par un nombre impair, ils pourraient heureusement ne pas publier leur bloc et en exploiter un autre, jusqu’à ce qu’ils trouvent une solution avec le hachage du bloc étant un nombre pair (en supposant que la récompense du bloc et les frais sont inférieurs à 1 M$). L’utilisation de variables passées ou présentes peut être encore plus dévastatrice, comme le démontre Martin Swende dans son excellent article de blog. De plus, utiliser uniquement des variables de bloc signifie que le nombre pseudo-aléatoire sera le même pour toutes les transactions d’un bloc, de sorte qu’un attaquant peut multiplier ses gains en effectuant de nombreuses transactions dans un bloc (si il devait y avoir une mise maximale).

Techniques préventives

La source d’entropie (aléatoire) doit être externe à la chaîne de blocs. Cela peut être fait entre pairs avec des systèmes tels que commit–reveal, ou en changeant le modèle de confiance en un groupe de participants (comme dans RandDAO). Cela peut également être fait via une entité centralisée qui agit comme un oracle aléatoire. Les variables de bloc (en général, il y a quelques exceptions) ne doivent pas être utilisées pour générer de l’entropie, car elles peuvent être manipulées par les mineurs.

Exemple concret : contrats PRNG

En février 2018 Arseny Reutov a blogué sur son analyse de 3 649 contrats intelligents en direct qui utilisaient une sorte de générateur de nombres pseudo-aléatoires (PRNG) ; il a trouvé 43 contrats qui pourraient être exploités.

Référencement des contrats externes

L’un des avantages de "l’ordinateur mondial" Ethereum est la possibilité de réutiliser du code et d’interagir avec des contrats déjà déployés sur le réseau. En conséquence, un grand nombre de contrats font référence à des contrats externes, généralement via des appels de messages externes. Ces appels de messages externes peuvent masquer les intentions des acteurs malveillants de certaines manières non évidentes, que nous allons maintenant examiner.

La vulnérabilité

Dans Solidity, n’importe quelle adresse peut être convertie en contrat, que le code à l’adresse représente ou non le type de contrat en cours de conversion. Cela peut causer des problèmes, en particulier lorsque l’auteur du contrat tente de dissimuler un code malveillant. Illustrons cela par un exemple.

Considérez un morceau de code comme Rot13Encryption.sol, qui implémente rudimentairement le chiffrement ROT13 .

Example 14. Rot13Encryption.sol
// contrat de chiffrement
contract Rot13Encryption {

   event Result(string convertedString);

    // rot13-chiffre une chaîne
    function rot13Encrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            // assemblage en ligne pour modifier la chaîne
            assembly {
                // récupère le premier octet
                char := byte(0,char)
                // si le caractère est dans [ n,z ], c'est-à-dire l'enveloppant
                if and(gt(char,0x6D), lt(char,0x7B))
                // soustraire du nombre ASCII 'a',
                // la différence entre le caractère <char> et 'z'
                { char:= sub(0x60, sub(0x7A,char)) }
                if iszero(eq(char, 0x20)) // ignore espaces
                // ajoute 13 au caractère
                {mstore8(add(add(text,0x20), mul(i,1)), add(char,13))}
            }
        }
        emit Result(text);
    }

    // rot13-déchiffre une chaîne
    function rot13Decrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            assembly {
                char := byte(0,char)
                if and(gt(char,0x60), lt(char,0x6E))
                { char:= add(0x7B, sub(char,0x61)) }
                if iszero(eq(char, 0x20))
                {mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}
            }
        }
        emit Result(text);
    }
}

Ce code prend simplement une chaîne (lettres a à z, sans validation) et l'encrypte en décalant chaque caractère de 13 positions vers la droite (en s’enroulant autour de z) ; c’est-à-dire a passe à n et x passe à k. L’assemblage dans le contrat précédent n’a pas besoin d’être compris pour apprécier le problème discuté, de sorte que les lecteurs non familiarisés avec l’assemblage peuvent l’ignorer en toute sécurité.

Considérons maintenant le contrat suivant, qui utilise ce code pour son chiffrement :

import "Rot13Encryption.sol";

// crypte vos informations top-secrètes
contract EncryptionContract {
    // bibliothèque pour le chiffrement
    Rot13Encryption encryptionLibrary;

    // constructeur - initialise la bibliothèque
    constructor(Rot13Encryption _encryptionLibrary) {
        encryptionLibrary = _encryptionLibrary;
    }

    function encryptPrivateData(string privateInfo) {
        // faire potentiellement quelques opérations ici
        encryptionLibrary.rot13Encrypt(privateInfo);
     }
 }

Le problème avec ce contrat est que l’adresse de encryptionLibrary n’est pas publique ou constante. Ainsi, le déployeur du contrat pourrait donner une adresse dans le constructeur qui pointe vers ce contrat :

// contrat de chiffrement
contract Rot26Encryption {

   event Result(string convertedString);

    // rot13-encrypte une chaîne
    function rot13Encrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            // assemblage en ligne pour modifier la chaîne
            assembly {
                // récupère le premier octet
                char := byte(0,char)
                // si le caractère est dans [ n,z ], c'est-à-dire l'enveloppant
                if and(gt(char,0x6D), lt(char,0x7B))
                // soustraire du nombre ASCII 'a',
                // la différence entre le caractère <char> et 'z'
                { char:= sub(0x60, sub(0x7A,char)) }
                // ignore les espaces
                if iszero(eq(char, 0x20))
                // ajoute 26 au caractère !
                {mstore8(add(add(text,0x20), mul(i,1)), add(char,26))}
            }
        }
        emit Result(text);
    }

    // rot13-décrypte une chaîne
    function rot13Decrypt (string text) public {
        uint256 length = bytes(text).length;
        for (var i = 0; i < length; i++) {
            byte char = bytes(text)[i];
            assembly {
                char := byte(0,char)
                if and(gt(char,0x60), lt(char,0x6E))
                { char:= add(0x7B, sub(char,0x61)) }
                if iszero(eq(char, 0x20))
                {mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}
            }
        }
        emit Result(text);
    }
}

Ce contrat implémente le chiffrement ROT26, qui décale chaque caractère de 26 places (c’est-à-dire, ne fait rien). Encore une fois, il n’est pas nécessaire de comprendre l’assemblage dans ce contrat. Plus simplement, l’attaquant aurait pu lier le contrat suivant au même effet :

contract Print{
    event Print(string text);

    function rot13Encrypt(string text) public {
        emit Print(text);
    }
 }

Si l’adresse de l’un de ces contrats était donnée dans le constructeur, la fonction encryptPrivateData produirait simplement un événement qui imprime les données privées non chiffrées.

Bien que dans cet exemple, un contrat de type bibliothèque ait été défini dans le constructeur, il arrive souvent qu’un utilisateur privilégié (tel qu’un propriétaire) puisse modifier les adresses de contrat de bibliothèque. Si un contrat lié ne contient pas la fonction appelée, la fonction de secours s’exécutera. Par exemple, avec la ligne encryptionLibrary.rot13​Encrypt(), si le contrat spécifié par encryptionLibrary était :

 contract Blank {
     event Print(string text);
     function () {
         emit Print("Here");
         // placez le code malveillant ici et il s'exécutera
     }
 }

alors un événement avec le texte Here serait émis. Ainsi, si les utilisateurs peuvent modifier les bibliothèques de contrats, ils peuvent en principe amener d’autres utilisateurs à exécuter du code arbitraire sans le savoir.

Warning

Les contrats représentés ici sont uniquement à des fins de démonstration et ne représentent pas un cryptage approprié. Ils ne devrait pas être utilisé pour le cryptage .

Techniques préventives

Comme démontré précédemment, les contrats sûrs peuvent (dans certains cas) être déployés de telle manière qu’ils se comportent de manière malveillante. Un auditeur pourrait vérifier publiquement un contrat et demander à son propriétaire de le déployer de manière malveillante, ce qui entraînerait un contrat audité publiquement présentant des vulnérabilités ou une intention malveillante.

Il existe un certain nombre de techniques qui empêchent ces scénarios.

Une technique consiste à utiliser le mot-clé new pour créer des contrats. Dans l’exemple précédent, le constructeur pourrait s’écrire :

constructor() {
    encryptionLibrary = new Rot13Encryption();
}

de cette manière, une instance du contrat référencé est créée au moment du déploiement et le déployeur ne peut pas remplacer le contrat Rot13Encryption sans le modifier.

Une autre solution consiste à coder en dur les adresses de contrat externes.

En général, le code qui appelle des contrats externes doit toujours être audité avec soin. En tant que développeur, lors de la définition de contrats externes, il peut être judicieux de rendre publiques les adresses des contrats (ce qui n’est pas le cas dans l’exemple du pot de miel de la section suivante) pour permettre aux utilisateurs d’examiner facilement le code référencé par le contrat. Inversement, si un contrat a une adresse de contrat en variable privée, cela peut être le signe d’un comportement malveillant (comme le montre l’exemple du monde réel). Si un utilisateur peut modifier une adresse de contrat utilisée pour appeler des fonctions externes, il peut être important (dans un contexte de système décentralisé) d’implémenter un mécanisme de verrouillage du temps et/ou de vote pour permettre aux utilisateurs de voir quel code est modifié, ou pour donner aux participants la possibilité de s’inscrire ou s’abstenir avec la nouvelle adresse contractuelle.

Exemple concret : pot de miel de réentrance

Un certain nombre de pots de miel récents ont été publiés sur le réseau principal. Ces contrats tentent de déjouer les pirates Ethereum qui tentent d’exploiter les contrats, mais qui finissent par perdre de l’ether au profit du contrat qu’ils s’attendent à exploiter. Un exemple utilise cette attaque en remplaçant un contrat attendu par un contrat malveillant dans le constructeur. Le code se trouve ici :

pragma solidity ^0.4.19;

contract Private_Bank
{
    mapping (address => uint) public balances;
    uint public MinDeposit = 1 ether;
    Log TransferLog;

    function Private_Bank(address _log)
    {
        TransferLog = Log(_log);
    }

    function Deposit()
    public
    payable
    {
        if(msg.value >= MinDeposit)
        {
            balances[msg.sender]+=msg.value;
            TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
        }
    }

    function CashOut(uint _am)
    {
        if(_am<=balances[msg.sender])
        {
            if(msg.sender.call.value(_am)())
            {
                balances[msg.sender]-=_am;
                TransferLog.AddMessage(msg.sender,_am,"CashOut");
            }
        }
    }

    function() external payable{}

}

contract Log
{
    struct Message
    {
        address Sender;
        string  Data;
        uint Val;
        uint  Time;
    }

    Message[] public History;
    Message LastMsg;

    function AddMessage(address _adr,uint _val,string _data)
    public
    {
        LastMsg.Sender = _adr;
        LastMsg.Time = now;
        LastMsg.Val = _val;
        LastMsg.Data = _data;
        History.push(LastMsg);
    }
}

Ce message d’un utilisateur de reddit explique comment il a perdu 1 ether à cause de ce contrat en essayant d’exploiter le bogue de réentrance qu’il s’attendait à trouver dans le contrat..

Attaque par adresse courte/paramètre

Cette attaque n’est pas effectuée sur les contrats Solidity eux-mêmes, mais sur des applications tierces susceptibles d’interagir avec eux. Cette section est ajoutée par souci d’exhaustivité et pour donner au lecteur une idée de la façon dont les paramètres peuvent être manipulés dans les contrats.

La vulnérabilité

Lors de la transmission de paramètres à un contrat intelligent, les paramètres sont codés conformément à la spécification ABI . Il est possible d’envoyer des paramètres codés plus courts que la longueur de paramètre attendue (par exemple, envoyer une adresse qui ne contient que 38 caractères hexadécimaux (19 octets) au lieu des 40 caractères hexadécimaux standard (20 octets)). Dans un tel scénario, l’EVM ajoutera des zéros à la fin des paramètres codés pour compenser la longueur attendue.

Cela devient un problème lorsque les applications tierces ne valident pas les entrées. L’exemple le plus clair est un échange qui ne vérifie pas l’adresse d’un jeton ERC20 lorsqu’un utilisateur demande un retrait. Cet exemple est traité plus en détail dans l’article de Peter Vessenes , "The ERC20 Short Address Attack Explained" .

Considérez l’interface de fonction de transfert standard ERC20 , en notant l’ordre des paramètres :

function transfer(address to, uint tokens) public returns (bool success);

Considérons maintenant un échange détenant une grande quantité d’un jeton (disons REP) et un utilisateur qui souhaite retirer sa part de 100 jetons. L’utilisateur soumettrait son adresse, 0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead , et le nombre de jetons, 100. L’échange coderait ces paramètres dans l’ordre spécifié par la fonction transfer; c’est-à-dire address puis tokens . Le résultat encodé serait :

a9059cbb000000000000000000000000deaddeaddea \
ddeaddeaddeaddeaddeaddeaddead0000000000000
000000000000000000000000000000000056bc75e2d63100000

Les 4 premiers octets ( a9059cbb ) sont la signature ou le sélecteur de fonction de transfer, les 32 octets suivants sont l’adresse et les 32 derniers octets représentent le nombre uint256 de jetons. Notez que l’hex 56bc75e2d63100000 à la fin correspond à 100 jetons (avec 18 décimales, comme spécifié par le contrat de jeton REP ).

Voyons maintenant ce qui se passerait si l’on envoyait une adresse à laquelle il manquait 1 octet (2 chiffres hexadécimaux). Plus précisément, disons qu’un attaquant envoie 0xdeaddeaddeaddeaddeaddeaddeaddeadde comme adresse (il manque les deux derniers chiffres) et les mêmes 100 jetons à retirer. Si l’échange ne valide pas cette entrée, elle sera encodée comme :

a9059cbb000000000000000000000000deaddeaddea \
ddeaddeaddeaddeaddeaddeadde00000000000000
00000000000000000000000000000000056bc75e2d6310000000

La différence est subtile. Notez que 00 a été ajouté à la fin de l’encodage, pour compenser l’adresse courte qui a été envoyée. Lorsque cela est envoyé au contrat intelligent, les paramètres de address seront lus comme 0xdeaddeaddeaddeaddeaddeaddeaddeadde00 et la valeur sera lue comme 56bc75e2d6310000000 (notez les deux 0 supplémentaires). Cette valeur est maintenant de 25600 jetons (la valeur a été multipliée par 256). Dans cet exemple, si l’échange contenait autant de jetons, l’utilisateur retirerait 25600 jetons (alors que l’échange pense que l’utilisateur n’en retire que 100) à l’adresse modifiée. Évidemment , l’attaquant ne possédera pas l’adresse modifiée dans cet exemple, mais si l’attaquant devait générer une adresse qui se terminait par des 0 (qui peut être facilement forcée brutalement) et utilisait cette adresse générée, il pourrait voler des jetons à l’échange sans méfiance.

Techniques préventives

Tous les paramètres d’entrée dans les applications externes doivent être validés avant de les envoyer à la chaîne de blocs. Il convient également de noter que l’ordre des paramètres joue ici un rôle important. Comme le rembourrage ne se produit qu’à la fin, un ordre minutieux des paramètres dans le contrat intelligent peut atténuer certaines formes de cette attaque.

Valeurs de retour CALL non vérifiés

Il existe plusieurs façons d’effectuer des appels externes dans Solidity. L’envoi d’ether à des comptes externes est généralement effectué via la méthode transfer . Cependant, la fonction d’envoi send peut également être utilisée, et pour des appels externes plus polyvalents, l' opcode CALL peut être directement utilisé dans Solidity. Les fonctions d’appel call et d’envoi send renvoient un booléen indiquant si l’appel a réussi ou échoué. Ainsi, ces fonctions ont une simple mise en garde, en ce sens que la transaction qui exécute ces fonctions ne reviendra pas si l’appel externe ( initialisé par call ou send ) échoue; à la place, les fonctions renverront simplement false. Une erreur courante est que le développeur s’attend à ce qu’un retour se produise si l’appel externe échoue et ne vérifie pas la valeur de retour.

La vulnérabilité

Considérez l’exemple suivant :

contract Lotto {

    bool public payedOut = false;
    address public winner;
    uint public winAmount;

    // ... fonctionnalité supplémentaire ici

    function sendToWinner() public {
        require(!payedOut);
        winner.send(winAmount);
        payedOut = true;
    }

    function withdrawLeftOver() public {
        require(payedOut);
        msg.sender.send(this.balance);
    }
}

Cela représente un contrat de type Lotto, où un gagnant winner reçoit une quantité d’ether (winAmount), ce qui laisse généralement un peu de reste à retirer.

La vulnérabilité existe sur la ligne 11, où un envoi send est utilisé sans vérifier la réponse. Dans cet exemple trivial, un gagnant winner dont la transaction échoue (soit parce qu’il est à court d’essence, soit parce qu’il s’agit d’un contrat qui lance intentionnellement la fonction de secours) permet à payedOut d’être défini sur true, que l’ether ait été envoyé ou non. Dans ce cas, n’importe qui peut retirer les gains du gagnant winner via la fonction withdrawLeftOver.

Techniques préventives

Dans la mesure du possible, utilisez la fonction transfer plutôt que send, car le transfert sera annulé si la transaction externe est annulée. Si un envoi send est requis, vérifiez toujours la valeur de retour.

Une recommandation plus robuste est d’adopter un modèle de retrait . Dans cette solution, chaque utilisateur doit appeler une fonction de retrait isolée qui gère l’envoi d’ether hors du contrat et traite les conséquences des transactions d’envoi échouées. L’idée est d’isoler logiquement la fonctionnalité d’envoi externe du reste de la base de code et de placer le fardeau d’une transaction potentiellement échouée sur l’utilisateur final appelant la fonction de retrait.

Exemple concret : Etherpot et King of the Ether

Etherpot était une loterie de contrats intelligents, pas trop différente de l’exemple de contrat mentionné précédemment. La chute de ce contrat était principalement due à une utilisation incorrecte des hachages de bloc (seuls les 256 derniers hachages de bloc sont utilisables ; voir le post d’Aakil Fernandes sur la manière dont Etherpot n’a pas réussi à en tenir compte correctement). Cependant, ce contrat a également souffert d’une valeur d’appel non contrôlée. Considérez la fonction cash dans lotto.sol: Code snippet.

Example 15. lotto.sol: Code snippet
...
  function cash(uint roundIndex, uint subpotIndex){

        var subpotsCount = getSubpotsCount(roundIndex);

        if(subpotIndex>=subpotsCount)
            return;

        var decisionBlockNumber = getDecisionBlockNumber(roundIndex,subpotIndex);

        if(decisionBlockNumber>block.number)
            return;

        if(rounds[roundIndex].isCashed[subpotIndex])
            return;
        // Les sous-cagnottes ne peuvent être encaissés qu'une seule fois. C'est pour éviter les doubles paiements
        var winner = calculateWinner(roundIndex,subpotIndex);
        var subpot = getSubpot(roundIndex);

        winner.send(subpot);

        rounds[roundIndex].isCashed[subpotIndex] = true;
        // Marquer le tour comme encaissé
}
...

Notez qu’à la ligne 21, la valeur de retour de la fonction send n’est pas vérifiée, et la ligne suivante définit alors un booléen indiquant que le gagnant a reçu ses fonds. Ce bogue peut autoriser un état où le gagnant ne reçoit pas son ether, mais l’état du contrat peut indiquer que le gagnant a déjà été payé.

Une version plus sérieuse de ce bogue s’est produite dans le King of the Ether. Un excellent post-mortem de ce contrat a été écrit qui détaille comment un envoi échoué non contrôlé pourrait être utilisé pour attaquer le contrat.

Conditions de course/devancement

La combinaison d’appels externes à d’autres contrats et la nature multi-utilisateurs de la chaîne de blocs sous-jacente donnent lieu à une variété de pièges potentiels de Solidity dans lesquels les utilisateurs font la course à l’exécution du code pour obtenir des états inattendus. La réentrance (discutée plus haut dans ce chapitre) est un exemple d’une telle condition de concurrence. Dans cette section, nous discuterons d’autres types de conditions de concurrence pouvant survenir sur la chaîne de blocs Ethereum. Il existe une variété de bons articles sur ce sujet, y compris "Race Conditions" sur le Wiki Ethereum , #7 sur le DASP Top10 de 2018 , et les meilleures pratiques pour les contrats intelligents Ethereum .

La vulnérabilité

Comme avec la plupart des chaînes de blocs, les nœuds Ethereum regroupent les transactions et les forment en blocs. Les transactions ne sont considérées comme valides qu’une fois qu’un mineur a résolu un mécanisme de consensus (actuellement Ethash PoW (preuve de travail) pour Ethereum). Le mineur qui résout le bloc choisit également les transactions du bassin qui seront incluses dans le bloc, généralement classées par le gasPrice de chaque transaction. Voici un vecteur d’attaque potentiel. Un attaquant peut surveiller le bassin de transactions à la recherche de transactions susceptibles de contenir des solutions à des problèmes, et modifier ou révoquer les autorisations du solveur ou changer l’état d’un contrat au détriment du solveur. L’attaquant peut alors obtenir les données de cette transaction et créer sa propre transaction avec un gasPrice plus élevé afin que sa transaction soit incluse dans un bloc avant l’original.

Voyons comment cela pourrait fonctionner avec un exemple simple. Considérez le contrat affiché dans FindThisHash.sol.

Example 16. FindThisHash.sol
contract FindThisHash {
    bytes32 constant public hash =
      0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;

    constructor() external payable {} // charge de l'ether

    function solve(string solution) public {
        // Si vous pouvez trouver la pré-image du hachage, recevez 1000 ether
        require(hash == sha3(solution));
        msg.sender.transfer(1000 ether);
    }
}

Supposons que ce contrat contienne 1 000 ether . L’utilisateur qui peut trouver la préimage du hachage SHA-3 suivant :

0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a

peut soumettre la solution et récupérer les 1 000 ethers . Disons qu’un utilisateur découvre que la solution est Ethereum!. Ils appellent resolve (résoudre) avec Ethereum! comme paramètre. Malheureusement, un attaquant a été assez intelligent pour surveiller le bassin de transactions pour toute personne soumettant une solution. Ils voient cette solution, vérifient sa validité, puis soumettent une transaction équivalente avec un gasPrice beaucoup plus élevé que la transaction d’origine. Le mineur qui résout le bloc donnera probablement la préférence à l’attaquant en raison du gasPrice plus élevé et exploitera sa transaction avant celle du solveur d’origine. L’attaquant prendra les 1 000 ethers et l’utilisateur qui a résolu le problème n’obtiendra rien. Gardez à l’esprit que dans ce type de vulnérabilité de "devancement", les mineurs sont particulièrement incités à exécuter les attaques eux-mêmes (ou peuvent être soudoyés pour exécuter ces attaques avec des frais extravagants). La possibilité que l’attaquant soit lui-même un mineur ne doit pas être sous-estimée.

Techniques préventives

Il existe deux classes d’acteurs qui peuvent effectuer ce type d’attaques de devancement: les utilisateurs (qui modifient la valeur du gasPrice de leurs transactions) et les mineurs eux-mêmes (qui peuvent réorganiser les transactions dans un bloc comme ils l’entendent). Un contrat vulnérable à la première classe (utilisateurs) est nettement moins bien loti qu’un contrat vulnérable à la seconde (mineurs), car les mineurs ne peuvent effectuer l’attaque que lorsqu’ils résolvent un bloc, ce qui est peu probable pour un mineur individuel ciblant un bloc spécifique. Nous énumérerons ici quelques mesures d’atténuation relatives aux deux classes d’attaquants.

Une méthode consiste à placer une limite supérieure sur le gasPrice. Cela empêche les utilisateurs d’augmenter le gasPrice et d’obtenir un ordre de transaction préférentiel au-delà de la limite supérieure. Cette mesure protège uniquement contre la première classe d’attaquants (utilisateurs arbitraires). Les mineurs dans ce scénario peuvent toujours attaquer le contrat, car ils peuvent commander les transactions dans leur bloc comme ils le souhaitent, quel que soit le prix du gaz.

Une méthode plus robuste consiste à utiliser un schéma de validation-révélation. Un tel système dicte que les utilisateurs envoient des transactions avec des informations cachées (généralement un hachage). Une fois la transaction incluse dans un bloc, l’utilisateur envoie une transaction révélant les données qui ont été envoyées (la phase de révélation). Cette méthode empêche les mineurs et les utilisateurs d’exécuter des transactions en amont, car ils ne peuvent pas déterminer le contenu de la transaction. Cette méthode, cependant, ne peut pas dissimuler la valeur de la transaction (qui, dans certains cas, est l’information précieuse qui doit être cachée). Le contrat intelligent ENS permettait aux utilisateurs d’envoyer des transactions dont les données engagées comprenaient la quantité d’ether qu’ils étaient prêts à dépenser. Les utilisateurs pourraient alors envoyer des transactions de valeur arbitraire. Au cours de la phase de révélation, les utilisateurs ont été remboursés de la différence entre le montant envoyé lors de la transaction et le montant qu’ils étaient prêts à dépenser.

Une autre suggestion de Lorenz Breidenbach, Phil Daian , Ari Juels et Florian Tramèr est d’utiliser des "envois sous-marins". Une implémentation efficace de cette idée nécessite l' opcode CREATE2, qui n’a actuellement pas été adopté mais semble susceptible de l’être dans les embranchements à venir.

Exemples concrets : ERC20 et Bancor

La norme ERC20 est assez connue pour la construction de jetons sur Ethereum. Cette norme présente une vulnérabilité de devancement potentielle due à la fonction d’approbation approve. Mikhail Vladimirov et Dmitry Khovratovich ont écrit une bonne explication de cette vulnérabilité (et des moyens d’atténuer l’attaque).

La norme spécifie la fonction d' approbation approve comme suit :

function approve(address _spender, uint256 _value) returns (bool success)

Cette fonction permet à un utilisateur d’autoriser d’autres utilisateurs à transférer des jetons en son nom. La vulnérabilité de devancement se produit dans le scénario où une utilisatrice Alice autorise son ami Bob à dépenser 100 jetons. Alice décide plus tard qu’elle veut révoquer l’autorisation de Bob de dépenser, disons, 100 jetons, alors elle crée une transaction qui fixe l’allocation de Bob à 50 jetons. Bob, qui a observé attentivement la chaîne, voit cette transaction et construit sa propre transaction en dépensant les 100 jetons. Il met un gasPrice plus élevé sur sa transaction que celle d’Alice, donc sa transaction est prioritaire sur la sienne. Certaines implémentations d' approbation approve permettraient à Bob de transférer ses 100 jetons, puis, lorsque la transaction d’Alice est validée, réinitialiserait l’approbation de Bob à 50 jetons, donnant ainsi à Bob l’accès à 150 jetons.

Bancor est un autre exemple important dans le monde réel. Ivan Bogatyy et son équipe ont documenté une attaque rentable sur la mise en œuvre initiale de Bancor. Son article de blog et sa conférence DevCon3 expliquent en détail comment cela a été fait. Essentiellement, les prix des jetons sont déterminés en fonction de la valeur de la transaction ; les utilisateurs peuvent surveiller le bassin de transactions pour les transactions Bancor et les exécuter en amont pour profiter des différences de prix. Cette attaque a été traitée par l’équipe de Bancor.

Déni de service (DoS)

Cette catégorie est très large, mais se compose essentiellement d’attaques où les utilisateurs peuvent rendre un contrat inopérant pendant une période de temps, ou dans certains cas de façon permanente. Cela peut piéger l’ether dans ces contrats pour toujours, comme ce fut le cas dans Exemple concret : Parity Multisig Wallet (Second Hack).

La vulnérabilité

Un contrat peut devenir inopérant de différentes manières. Ici, nous mettons en évidence quelques modèles de codage Solidity moins évidents qui peuvent conduire à des vulnérabilités DoS :

Boucle à travers des mappages ou des tableaux manipulés en externe

Ce modèle apparaît généralement lorsqu’un propriétaire souhaite distribuer des jetons aux investisseurs avec une fonction de type distribution distribute, comme dans cet exemple de contrat :

contract DistributeTokens {
    address public owner; // est défini ailleurs
    address[] investors; // tableau d'investisseurs
    uint[] investorTokens; // quantité de jeton que reçoivent les investisseurs

    // ... fonctionnalité supplémentaire, y compris transfertoken()

    function invest() external payable {
        investors.push(msg.sender);
        investorTokens.push(msg.value * 5); // 5 fois de wei envoyé
        }

    function distribute() public {
        require(msg.sender == owner); // seulement le propriétaire
        for(uint i = 0; i < investors.length; i++) {
            // ici transferToken(to,amount) transfère le "montant" de
            // jetons à l'adresse "to"
            transferToken(investors[i],investorTokens[i]);
        }
    }
}

Notez que la boucle de ce contrat s’exécute sur un tableau qui peut être gonflé artificiellement. Un attaquant peut créer de nombreux comptes d’utilisateurs, ce qui agrandit le tableau des investisseurs investor. En principe, cela peut être fait de telle sorte que le gaz requis pour exécuter la boucle for dépasse la limite de gaz du bloc, rendant essentiellement la fonction de distribution distribute inopérante.

Opérations du propriétaire

Un autre modèle courant est celui où les propriétaires ont des privilèges spécifiques dans les contrats et doivent effectuer certaines tâches pour que le contrat passe à l’état suivant. Un exemple serait un contrat d’offre initiale de pièces (ICO) qui oblige le propriétaire à finaliser (finalize) le contrat, ce qui permet ensuite aux jetons d’être transférables. Par example:

bool public isFinalized = false;
address public owner; // est défini quelque part

function finalize() public {
    require(msg.sender == owner);
    isFinalized == true;
}

// ... fonctionnalité ICO supplémentaire

// fonction de transfert surchargée
function transfer(address _to, uint _value) returns (bool) {
    require(isFinalized);
    super.transfer(_to,_value)
}

...

Dans de tels cas, si l’utilisateur privilégié perd ses clés privées ou devient inactif, l’ensemble du contrat de jeton devient inopérant. Dans ce cas, si le propriétaire ne peut pas appeler finalize, aucun jeton ne peut être transféré ; l’ensemble du fonctionnement de l’écosystème de jetons repose sur une seule adresse.

État de progression basé sur les appels externes

Les contrats sont parfois rédigés de telle sorte que la progression vers un nouvel état nécessite l’envoi d’ether à une adresse ou l’attente d’une entrée d’une source externe. Ces modèles peuvent conduire à des attaques DoS lorsque l’appel externe échoue ou est empêché pour des raisons externes. Dans l’exemple de l’envoi d’ether, un utilisateur peut créer un contrat qui n’accepte pas l’ether. Si un contrat exige que l’ether soit retiré afin de passer à un nouvel état (considérez un contrat à verrouillage temporel qui exige que tout l’ether soit retiré avant d’être à nouveau utilisable), le contrat n’atteindra jamais le nouvel état, car l’ether ne peut jamais être envoyé au contrat de l’utilisateur qui n’accepte pas l’ether.

Techniques préventives

Dans le premier exemple, les contrats ne doivent pas boucler sur des structures de données pouvant être artificiellement manipulées par des utilisateurs externes. Un modèle de retrait est recommandé, dans lequel chacun des investisseurs appelle une fonction de retrait pour réclamer des jetons de manière indépendante.

Dans le deuxième exemple, un utilisateur privilégié devait modifier l’état du contrat. Dans de tels exemples, une sécurité intégrée peut être utilisée dans le cas où le propriétaire deviendrait incapable. Une solution consiste à faire du propriétaire un contrat multisig. Une autre solution consiste à utiliser un verrouillage temporel: dans l’exemple donné, le require à la ligne 5 pourrait inclure un mécanisme basé sur le temps, tel que require(msg.sender == owner || now > unlockTime) , qui permet à tout utilisateur de finaliser après une période de temps spécifiée par unlockTime. Ce type de technique d’atténuation peut également être utilisé dans le troisième exemple. Si les appels externes doivent progresser vers un nouvel état, tenez compte de leur échec possible et ajoutez éventuellement une progression d’état basée sur le temps dans le cas où l’appel souhaité n’arrive jamais.

Note

Bien sûr, il existe des alternatives centralisées à ces suggestions : on peut ajouter un maintenanceUser qui peut intervenir et résoudre les problèmes avec les vecteurs d’attaque basés sur DoS si besoin est. Généralement, ces types de contrats posent des problèmes de confiance, en raison du pouvoir d’une telle entité.

Exemples concrets : GouvernMental

GovernMental était un ancien schéma de Ponzi qui accumulait une assez grande quantité d’ether (1 100 ether, à un moment donné). Malheureusement, il était sensible aux vulnérabilités DoS mentionnées dans cette section. Un post Reddit par etherik décrit comment le contrat exigeait la suppression d’un grand mappage afin de retirer l’ether. La suppression de cette cartographie avait un coût en gaz qui dépassait la limite de gaz du bloc à l’époque, et il n’était donc pas possible de retirer les 1 100 ethers . L’adresse du contrat est 0xF45717552f12Ef7cb65e95476F217Ea008167Ae3, et vous pouvez voir dans la transaction 0x0d80d67202bd9cb6773df8dd2020e719 0a1b0793e8ec4fc105257e8128f0506b que les 1 100 ether ont finalement été obtenus via 2.5M en frais de gaz (et ce quand la limite du gaz fût augmenté).

Blocage de la manipulation de l’horodatage

Les horodatages de bloc ont historiquement été utilisés pour une variété d’applications, telles que l’entropie pour les nombres aléatoires (voir l'Illusion d’entropie pour plus de détails), le verrouillage des fonds pendant des périodes de temps et diverses instructions conditionnelles de changement d’état qui dépendent du temps. Les mineurs ont la possibilité d' ajuster légèrement les horodatages, ce qui peut s’avérer dangereux si les horodatages de bloc sont utilisés de manière incorrecte dans les contrats intelligents.

Les références utiles pour cela incluent la documentation Solidity et la question Ethereum sur Stack Exchange de Joris Bontje sur le sujet.

La vulnérabilité

block.timestamp et son alias now peuvent désormais être manipulés par les mineurs s’ils sont incités à le faire. Construisons un jeu simple, montré dans roulette.sol, qui serait vulnérable à l’exploitation des mineurs.

Example 17. roulette.sol
contract Roulette {
    uint public pastBlockTime; // force un pari par bloc

    constructor() external payable {} // initialise le financement du contrat

    // fonction de repli utilisée pour faire un pari
    function () external payable {
        require(msg.value == 10 ether); // doit envoyer 10 ether pour jouer
        require(now != pastBlockTime); // seulement 1 transaction par bloc
        pastBlockTime = now;
        if(now % 15 == 0) { // gagnant
            msg.sender.transfer(this.balance);
        }
    }
}

Ce contrat se comporte comme une simple loterie. Une transaction par bloc peut parier 10 ethers pour avoir une chance de gagner le solde du contrat. L’hypothèse ici est que les deux derniers chiffres de block.timestamp sont uniformément distribués. Si tel était le cas, il y aurait 1 chance sur 15 de gagner à cette loterie.

Cependant, comme nous le savons, les mineurs peuvent ajuster l’horodatage s’ils en ont besoin. Dans ce cas particulier, s’il y a suffisamment d’ether dans le contrat, un mineur qui résout un bloc est incité à choisir un horodatage tel que block.timestamp ou now modulo 15 est 0. Ce faisant, ils peuvent gagner l’ether verrouillé dans ce contrat avec la récompense globale. Comme il n’y a qu’une seule personne autorisée à parier par bloc, cela est également vulnérable aux attaques de dépassement (voir Conditions de course/devancement pour plus de détails).

En pratique, les horodatages de bloc augmentent de manière monotone et les mineurs ne peuvent donc pas choisir des horodatages de bloc arbitraires (ils doivent être postérieurs à leurs prédécesseurs). Ils sont également limités à définir des heures de bloc pas trop éloignées dans le futur, car ces blocs seront probablement rejetés par le réseau (les nœuds ne valideront pas les blocs dont les horodatages sont dans le futur).

Techniques préventives

Les horodatages de bloc ne doivent pas être utilisés pour l’entropie ou la génération de nombres aléatoires, c’est-à-dire qu’ils ne doivent pas être le facteur décisif (directement ou par dérivation) pour gagner un jeu ou changer un état important.

Une logique sensible au temps est parfois nécessaire ; par exemple, pour débloquer des contrats (verrou temporel), remplir une ICO après quelques semaines ou faire respecter des dates d’expiration. Il est parfois recommandé d’utiliser block.number et un temps de bloc moyen pour estimer les temps; avec un temps de bloc de 10 secondes, 1 semaine équivaut à environ 60 480 blocs. Ainsi, spécifier un numéro de bloc auquel changer un état de contrat peut être plus sûr, car les mineurs sont incapables de manipuler facilement le numéro de bloc. Le contrat BAT ICO a employé cette stratégie.

Cela peut être inutile si les contrats ne sont pas particulièrement concernés par les manipulations des mineurs de l’horodatage du bloc, mais c’est quelque chose dont il faut être conscient lors de l’élaboration des contrats.

Exemple concret : GovernMental

GovernMental, l’ancien schéma de Ponzi mentionné ci-dessus, était également vulnérable à une attaque basée sur l’horodatage. Le contrat est payé au joueur qui a été le dernier joueur à rejoindre (pendant au moins une minute) un tour. Ainsi, un mineur qui était un joueur pouvait ajuster l’horodatage (à une heure future, pour donner l’impression qu’une minute s’était écoulée) pour faire apparaître qu’il était le dernier joueur à rejoindre pendant plus d’une minute (même si ce n’était pas vrai dans la réalité). Plus de détails à ce sujet peuvent être trouvés dans le post "Historique des vulnérabilités de sécurité d’Ethereum, des piratages et de leurs correctifs" par Tanya Bahrynovska.

Constructeurs avec soin

Les constructeurs sont des fonctions spéciales qui effectuent souvent des tâches critiques et privilégiées lors de l’initialisation des contrats. Avant Solidity v0.4.22, les constructeurs étaient définis comme des fonctions portant le même nom que le contrat qui les contenait. Dans de tels cas, lorsque le nom du contrat est modifié au cours du développement, si le nom du constructeur n’est pas également modifié, il devient une fonction appelable normale. Comme vous pouvez l’imaginer, cela peut conduire (et a conduit) à des hacks de contrats intéressants.

Pour plus d’informations, le lecteur peut être intéressé à tenter les défis Ethernaut (en particulier le niveau Fallout).

La vulnérabilité

Si le nom du contrat est modifié, ou s’il y a une faute de frappe dans le nom du constructeur qui ne correspond pas au nom du contrat, le constructeur se comportera comme une fonction normale. Cela peut avoir des conséquences désastreuses, surtout si le constructeur effectue des opérations privilégiées. Considérez le contrat suivant :

contract OwnerWallet {
    address public owner;
    // constructeur
    function ownerWallet(address _owner) public {
        owner = _owner;
    }

    // Par défaut. Récupérer de l'ether.
    function () payable {}

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }
}

Ce contrat collecte de l’ether et permet uniquement au propriétaire de le retirer, en appelant la fonction de retrait withdraw. Le problème se pose car le constructeur ne porte pas exactement le même nom que le contrat : la première lettre est différente ! Ainsi, n’importe quel utilisateur peut appeler la fonction ownerWallet, se définir comme propriétaire, puis prendre tout l’ether du contrat en appelant withdraw.

Techniques préventives

Ce problème a été résolu dans la version 0.4.22 du compilateur Solidity. Cette version a introduit un mot clé constructor qui spécifie le constructeur, plutôt que d’exiger que le nom de la fonction corresponde au nom du contrat. L’utilisation de ce mot-clé pour spécifier les constructeurs est recommandée pour éviter les problèmes de nommage.

Exemple concret : Rubixi

Rubixi était un autre système pyramidal qui présentait ce type de vulnérabilité. Il s’appelait à l’origine DynamicPyramid , mais le nom du contrat a été modifié avant le déploiement sur Rubixi. Le nom du constructeur n’a pas été modifié, permettant à n’importe quel utilisateur de devenir le créateur. Des discussions intéressantes liées à ce bogue peuvent être trouvées sur Bitcointalk. En fin de compte, cela a permis aux utilisateurs de se battre pour le statut de créateur afin de réclamer les frais du système pyramidal. Plus de détails sur ce bogue particulier peuvent être trouvés dans "Historique des vulnérabilités de sécurité d’Ethereum, des piratages et de leurs correctifs".

Pointeurs de stockage non initialisés

L’EVM stocke les données sous forme de stockage (disque, par exemple) ou de mémoire. Il est fortement recommandé de comprendre exactement comment cela se fait et les types par défaut des variables locales des fonctions lors du développement de contrats. En effet, il est possible de produire des contrats vulnérables en initialisant de manière inappropriée des variables.

Pour en savoir plus sur le stockage et la mémoire dans l’EVM, consultez la documentation de Solidity sur l’emplacement des données , la disposition des variables d’état dans le stockage et la disposition dans la mémoire.

Note

Cette section est basée sur un excellent article de Stefan Beyer. D’autres lectures sur ce sujet, inspirées par Stefan, peuvent être trouvées dans ce fil Reddit.

La vulnérabilité

Les variables locales dans les fonctions sont par défaut le stockage ou la mémoire en fonction de leur type. Les variables de stockage local non initialisées peuvent contenir la valeur d’autres variables de stockage dans le contrat ; ce fait peut provoquer des vulnérabilités involontaires ou être exploité délibérément.

Considérons le contrat de bureau d’enregistrement de noms relativement simple dans NameRegistrar.sol.

Example 18. NameRegistrar.sol
// Un bureau d'enregistrement de noms verrouillé
contract NameRegistrar {

    bool public unlocked = false;  // bureau d'enregistrement verrouillé, pas de mises à jour de nom

    struct NameRecord { // mappe les hachages aux adresses
        bytes32 name;
        address mappedAddress;
    }

    // enregistre les noms
    mapping(address => NameRecord) public registeredNameRecord;
    // résout les hachages en adresses
    mapping(bytes32 => address) public resolve;

    function register(bytes32 _name, address _mappedAddress) public {
        // configure le nouveau NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress;

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord;

        require(unlocked); // n'autorise les inscriptions que si le contrat est déverrouillé
    }
}

Ce simple registraire de noms n’a qu’une seule fonction. Lorsque le contrat est unlocked (déverrouillé), il permet à quiconque d’enregistrer un nom (sous forme de hachage bytes32) et de mapper ce nom à une adresse. Le bureau d’enregistrement est initialement verrouillé et l' exigence require à la ligne 25 empêche le registre d’ajouter des enregistrements de nom avec register. Il semble que le contrat soit inutilisable, car il n’y a aucun moyen de déverrouiller le registre ! Il existe cependant une vulnérabilité qui permet l’enregistrement du nom quelle que soit la variable unlocked.

Pour discuter de cette vulnérabilité, nous devons d’abord comprendre comment fonctionne le stockage dans Solidity. En tant qu’aperçu de haut niveau (sans aucun détail technique approprié - nous vous suggérons de lire les documents Solidity pour un examen approprié), les variables d’état sont stockées séquentiellement dans les emplacements (ou slots) tels qu’ils apparaissent dans le contrat (ils peuvent être regroupés mais ne sont pas dans ce exemple, donc nous ne nous en préoccuperons pas). Ainsi, unlocked existe dans slot[0], registerNameRecord dans slot[1] , et resolve dans slot[2], etc. Chacun de ces emplacements a une taille de 32 octets (il y a des complexités supplémentaires avec les mappages, que nous ignorerons pour à présent). Le booléen unlocked ressemblera à 0x000…​0 (64 0s, à l’exclusion du 0x) pour false ou 0x000…​1 (63 0s) pour true. Comme vous pouvez le voir, il y a un important gaspillage de stockage dans cet exemple particulier .

La pièce suivante du puzzle est que Solidity place par défaut des types de données complexes, tels que des structures structs, en stockage lors de leur initialisation en tant que variables locales. Par conséquent, newRecord à la ligne 18 utilise par défaut le stockage. La vulnérabilité est causée par le fait que newRecord n’est pas initialisé. Comme il s’agit par défaut du stockage, il est mappé sur l’emplacement de stockage slot[0], qui contient actuellement un pointeur vers unlocked. Notez qu’aux lignes 19 et 20, nous définissons ensuite newRecord.name sur _name et newRecord.mappedAddress sur _mappedAddress; cela met à jour les emplacements de stockage de slot[0] et slot[1], ce qui modifie à la fois unlocked et l’emplacement de stockage associé à registerNameRecord.

Cela signifie que unlocked peut être modifié directement, simplement par le paramètre bytes32 _name de la fonction register. Par conséquent, si le dernier octet de _name est différent de zéro, il modifiera le dernier octet de storage slot[0] et changera directement unlocked en true . De telles valeurs _name entraîneront la réussite de l’appel require sur la ligne 25, car nous avons défini unlocked sur true . Essayez ceci dans Remix. Notez que la fonction passera si vous utilisez un _name du formulaire :

0x0000000000000000000000000000000000000000000000000000000000000001

Techniques préventives

Solidity affiche un avertissement pour les variables de stockage non initialisées ; les développeurs doivent prêter une attention particulière à ces avertissements lors de la création de contrats intelligents. La version actuelle de Mist (0.10) ne permet pas de compiler ces contrats. Il est souvent recommandé d’utiliser explicitement les spécificateurs de mémoire memory ou de stockage storage lorsqu’il s’agit de types complexes, pour s’assurer qu’ils se comportent comme prévu.

Exemples concrets : Pots de miel OpenAddressLottery et CryptoRoulette

Un pot de miel nommé OpenAddressLotterya été déployé qui a utilisé cette bizarrerie de variable de stockage non initialisée pour collecter de l’ether auprès de certains pirates potentiels. Le contrat est plutôt impliqué, nous laisserons donc l’analyse au fil Reddit où l’attaque est assez clairement expliquée.

Un autre pot de miel, CryptoRoulette, a également utilisé cette astuce pour essayer de collecter de l’ether. Si vous ne pouvez pas comprendre comment fonctionne l’attaque, consultez « Une analyse de quelques contrats de pot de miel Ethereum » pour un aperçu de ce contrat et d’autres.

Virgule flottante et de précision

Au moment d’écrire ces lignes (v0.4.24), Solidity ne prend pas en charge les nombres à virgule fixe et à virgule flottante. Cela signifie que les représentations en virgule flottante doivent être construites avec des types entiers dans Solidity. Cela peut entraîner des erreurs et des vulnérabilités s’il n’est pas mis en œuvre correctement.

Note

Pour en savoir plus, consultez le wiki Ethereum Contract Security Techniques and Tips.

La vulnérabilité

Comme il n’y a pas de type à virgule fixe dans Solidity, les développeurs doivent implémenter le leur en utilisant les types de données entiers standard. Il existe un certain nombre de pièges que les développeurs peuvent rencontrer au cours de ce processus. Nous allons essayer d’en souligner quelques-uns dans cette section.

Commençons par un exemple de code (nous ignorerons les problèmes de soupassement/dépassement, abordés plus haut dans ce chapitre, pour plus de simplicité) :

contract FunWithNumbers {
    uint constant public tokensPerEth = 10;
    uint constant public weiPerEth = 1e18;
    mapping(address => uint) public balances;

    function buyTokens() external payable {
        // convertit wei en eth, puis multiplie par le taux de jeton
        uint tokens = msg.value/weiPerEth*tokensPerEth;
        balances[msg.sender] += tokens;
    }

    function sellTokens(uint tokens) public {
        require(balances[msg.sender] >= tokens);
        uint eth = tokens/tokensPerEth;
        balances[msg.sender] -= tokens;
        msg.sender.transfer(eth*weiPerEth);
    }
}

Ce simple contrat d’achat/vente de jetons présente des problèmes évidents. Bien que les calculs mathématiques pour l’achat et la vente de jetons soient corrects, l’absence de nombres à virgule flottante donnera des résultats erronés. Par exemple, lors de l’achat de jetons sur la ligne 8, si la valeur est inférieure à 1 ether, la division initiale donnera 0, laissant le résultat de la multiplication finale à 0 (par exemple, 200 wei divisé par 1e18 weiPerEth est égal à 0). De même, lors de la vente de jetons, tout nombre de jetons inférieur à 10 entraînera également 0 ether. En fait, l’arrondi ici est toujours inférieur, donc la vente de 29 jetons se traduira par 2 ether.

Le problème avec ce contrat est que la précision n’est qu’à l’ether le plus proche (c’est-à-dire 1e18 wei ). Cela peut devenir délicat lorsqu’il s’agit de décimales dans les jetons ERC20 lorsque vous avez besoin d’une plus grande précision.

Techniques préventives

Garder la bonne précision dans vos contrats intelligents est très important, en particulier lorsqu’il s’agit de ratios et de taux qui reflètent des décisions économiques.

Vous devez vous assurer que tous les ratios ou taux que vous utilisez permettent de grands numérateurs dans les fractions. Par exemple, nous avons utilisé le taux tokensPerEth dans notre exemple. Il aurait été préférable d’utiliser weiPerTokens, qui serait un grand nombre. Pour calculer le nombre correspondant de jetons, nous pourrions faire msg.value/weiPerTokens. Cela donnerait un résultat plus précis.

Une autre tactique à garder à l’esprit est l’ordre des opérations. Dans notre exemple, le calcul pour acheter des jetons était msg.value/weiPerEth*tokenPerEth. Notez que la division se produit avant la multiplication. (Solidity, contrairement à certains langages, garantit d’effectuer les opérations dans l’ordre dans lequel elles sont écrites.) Cet exemple aurait atteint une plus grande précision si le calcul effectuait d’abord la multiplication puis la division; c’est-à-dire msg.value*tokenPerEth/weiPerEth.

Enfin, lors de la définition d’une précision arbitraire pour les nombres, il peut être judicieux de convertir les valeurs en une précision supérieure, d’effectuer toutes les opérations mathématiques, puis de les reconvertir finalement à la précision requise pour la sortie. Généralement, les uint256 sont utilisés (car ils sont optimaux pour l’utilisation du gaz) ; ceux-ci donnent environ 60 ordres de grandeur dans leur gamme, dont certains peuvent être dédiés à la précision des opérations mathématiques. Il se peut qu’il soit préférable de conserver toutes les variables en haute précision dans Solidity et de les reconvertir en précisions inférieures dans les applications externes (c’est essentiellement ainsi que fonctionne la variable décimale dans les contrats de jeton ERC20). Pour voir un exemple de la façon dont cela peut être fait, nous vous recommandons de regarder DS-Math. Il utilise des noms funky ("wads" et "rays"), mais le concept est utile.

Exemple concret : Ethstick

Le contrat Ethstickn’utilise pas la précision étendue; cependant, il traite de wei. Donc, ce contrat aura des problèmes d’arrondi, mais seulement au niveau de précision wei. Il a quelques défauts plus graves, mais ceux-ci sont liés à la difficulté d’obtenir de l’entropie sur la chaîne de blocs (voir Illusion d’entropie). Pour une discussion plus approfondie du contrat Ethstick, nous vous renverrons à un autre article de Peter Vessenes, "Les contrats Ethereum vont être des bonbons pour les pirates".

Authentification Tx.Origin

Solidity a une variable globale, tx.origin, qui parcourt toute la pile d’appels et contient l’adresse du compte qui a initialement envoyé l’appel (ou la transaction). L’utilisation de cette variable pour l’authentification dans un contrat intelligent rend le contrat vulnérable à une attaque de type hameçonnage.

Note

Pour en savoir plus, consultez la question Ethereum sur Stack Exchange de dbryson , "Tx.Origin and Ethereum Oh My!" de Peter Vessenes et "Solidity : Tx Origin Attacks" de Chris Coverdale.

La vulnérabilité

Les contrats qui autorisent les utilisateurs à utiliser la variable tx.origin sont généralement vulnérables aux attaques de phishing qui peuvent inciter les utilisateurs à effectuer des actions authentifiées sur le contrat vulnérable. Considérez le contrat simple dans Phishable.sol.

Example 19. Phishable.sol
contract Phishable {
    address public owner;

    constructor (address _owner) {
        owner = _owner;
    }

    function () external payable {} // recevoir ether

    function withdrawAll(address _recipient) public {
        require(tx.origin == owner);
        _recipient.transfer(this.balance);
    }
}

Notez qu’à la ligne 11 le contrat autorise la fonction withdrawAll en utilisant tx.origin. Ce contrat permet à un attaquant de créer un contrat d’attaque de la forme :

import "Phishable.sol";

contract AttackContract {

    Phishable phishableContract;
    address attacker; // L'adresse de l'attaquant pour recevoir les fonds

    constructor (Phishable _phishableContract, address _attackerAddress) {
        phishableContract = _phishableContract;
        attacker = _attackerAddress;
    }

    function () payable {
        phishableContract.withdrawAll(attacker);
    }
}

L’attaquant pourrait déguiser ce contrat en sa propre adresse privée et organiser socialement la victime (le propriétaire du contrat Phishable ) pour envoyer une forme de transaction à l’adresse, peut-être en envoyant ce contrat une certaine quantité d’ether. La victime, à moins d’être prudente, peut ne pas remarquer qu’il y a du code à l’adresse de l’attaquant, ou l’attaquant peut le faire passer pour un portefeuille multisignature ou un portefeuille de stockage avancé (rappelez-vous que le code source des contrats publics n’est pas disponible par défaut) .

Dans tous les cas, si la victime envoie une transaction avec suffisamment de gaz à l' adresse AttackContract, elle invoquera la fonction de secours, qui à son tour appellera la fonction withdrawAll du contrat Phishable avec le paramètre attacker. Cela entraînera le retrait de tous les fonds du contrat Phishable à l’adresse de l' attaquant attacker. C’est parce que l’adresse qui a initialement initialisé l’appel était la victime (c’est-à-dire le propriétaire du contrat Phishable). Par conséquent, tx.origin sera égal à owner et l' exigence require à la ligne 11 du contrat Phishable passera.

Techniques préventives

tx.origin ne doit pas être utilisé pour l’autorisation dans les contrats intelligents. Cela ne veut pas dire que la variable tx.origin ne doit jamais être utilisée. Il existe des cas d’utilisation légitimes dans les contrats intelligents. Par exemple, si l’on voulait empêcher les contrats externes d’appeler le contrat actuel, on pourrait implémenter un require de la forme require(tx.origin==msg.sender). Cela empêche l’utilisation de contrats intermédiaires pour appeler le contrat en cours, limitant le contrat à des adresses sans code régulier.

Contrats de bibliothèques

De nombreux codes existants sont disponibles pour être réutilisés, à la fois déployés en chaîne en tant que bibliothèques appelables et hors chaîne en tant que bibliothèques de modèles de code. Les bibliothèques sur plate-forme, ayant été déployées, existent sous forme de contrats intelligents en code intermédiaire (bytecode), il faut donc faire très attention avant de les utiliser en production. Cependant, l’utilisation de bibliothèques sur plate-forme existantes bien établies présente de nombreux avantages, tels que la possibilité de bénéficier des dernières mises à jour, et vous permet d’économiser de l’argent et profite à l’écosystème Ethereum en réduisant le nombre total de contrats en direct dans Ethereum.

Dans Ethereum, la ressource la plus largement utilisée est la suite OpenZeppelin, une vaste bibliothèque de contrats allant des implémentations de jetons ERC20 et ERC721, à de nombreuses variantes de modèles de crowdsale , en passant par des comportements simples couramment trouvés dans les contrats, tels que Ownable, Pausable ou LimitBalance. Les contrats de ce référentiel ont été largement testés et, dans certains cas, fonctionnent même comme des implémentations standard de facto . Ils sont libres d' utilisation et sont construits et maintenus par Zeppelin avec une liste sans cesse croissante de contributeurs externes.

Également de Zeppelin est ZeppelinOS, une plate-forme à source libre de services et d’outils pour développer et gérer en toute sécurité des applications de contrat intelligent. ZeppelinOS fournit une couche au-dessus de l’EVM qui permet aux développeurs de lancer facilement des DApps évolutives liées à une bibliothèque en chaîne de contrats bien testés qui sont eux-mêmes évolutifs. Différentes versions de ces bibliothèques peuvent coexister sur la plate-forme Ethereum, et un système de caution permet aux utilisateurs de proposer ou de pousser des améliorations dans différentes directions. Un ensemble d’outils hors chaîne pour déboguer, tester, déployer et surveiller les applications décentralisées est également fourni par la plate-forme.

Le projet ethpm vise à organiser les différentes ressources qui se développent dans l’écosystème en proposant un système de gestion de packages. En tant que tel, leur registre fournit plus d’exemples à parcourir :

Conclusion

Tout développeur travaillant dans le domaine des contrats intelligents a beaucoup à savoir et à comprendre. En suivant les meilleures pratiques dans la conception de votre contrat intelligent et la rédaction de votre code, vous éviterez de nombreux pièges et attrappes.

Le principe de sécurité logicielle le plus fondamental est peut-être de maximiser la réutilisation du code de confiance. En cryptographie, c’est si important qu’il a été condensé en un adage : "Ne lancez pas votre propre crypto." Dans le cas des contrats intelligents, cela revient à tirer le meilleur parti possible des bibliothèques librement disponibles qui ont été soigneusement contrôlées par la communauté.

Jetons

Bannière Amazon du livre Maîtriser Ethereum

Le mot "token" (jeton) dérive du vieil anglais "tācen", signifiant un signe ou un symbole. Il est couramment utilisé pour désigner des objets de type pièce de monnaie à usage spécial émis par des particuliers et ayant une valeur intrinsèque insignifiante, tels que des token ou jetons de transport, des jetons de blanchisserie et des jetons de jeux d’arcade.

De nos jours, les "tokens" ou "jetons" (nous utiliserons le terme français "jeton" pour le restant du chapitre) administrés sur les chaînes de blocs redéfinissent le mot pour désigner des abstractions basées sur la chaîne de blocs qui peuvent être possédées et qui représentent des actifs, des devises ou des droits d’accès.

L’association entre le mot "jeton" et la valeur insignifiante a beaucoup à voir avec l’utilisation limitée des versions physiques des jetons. Souvent limités à des entreprises, des organisations ou des emplacements spécifiques, les jetons physiques ne sont pas facilement échangeables et n’ont généralement qu’une seule fonction. Avec les jetons de chaîne de blocs, ces restrictions sont levées ou, plus précisément, complètement redéfinissables. De nombreux jetons de chaîne de blocs ont plusieurs objectifs à l’échelle mondiale et peuvent être échangés les uns contre les autres ou contre d’autres devises sur les marchés liquides mondiaux. Avec la disparition des restrictions d’utilisation et de propriété, l’attente d’une "valeur insignifiante" appartient également au passé.

Dans ce chapitre, nous examinons les différentes utilisations des jetons et comment ils sont créés. Nous discutons également des attributs des jetons tels que la fongibilité et l’intrinsèque. Enfin, nous examinons les normes et les technologies sur lesquelles ils sont basés et expérimentons en créant nos propres jetons.

Comment les jetons sont utilisés

L’utilisation la plus évidente des jetons est celle des monnaies privées numériques. Cependant, ce n’est qu’une utilisation possible. Les jetons peuvent être programmés pour remplir de nombreuses fonctions différentes, qui se chevauchent souvent. Par exemple, un jeton peut simultanément transmettre un droit de vote, un droit d’accès et la propriété d’une ressource. Comme le montre la liste suivante, la devise n’est que la première "application" :

Monnaie

Un jeton peut servir de forme de monnaie, avec une valeur déterminée par le commerce privé.

Ressource

Un jeton peut représenter une ressource gagnée ou produite dans une économie de partage ou un environnement de partage de ressources ; par exemple, un jeton de stockage ou de processeur représentant des ressources qui peuvent être partagées sur un réseau.

Actif

un jeton peut représenter la propriété d’un actif intrinsèque ou extrinsèque, tangible ou intangible ; par exemple, de l’or, de l’immobilier, une voiture, du pétrole, de l’énergie, des objets MMOG, etc.

Accès

un jeton peut représenter des droits d’accès et accorder l’accès à une propriété numérique ou physique, telle qu’un forum de discussion, un site Web exclusif, une chambre d’hôtel ou une voiture de location.

Équité

Un jeton peut représenter l’équité des actionnaires dans une organisation numérique (par exemple, un DAO) ou une entité juridique (par exemple, une société).

Vote

Un jeton peut représenter des droits de vote dans un système numérique ou juridique.

Objet de collection

Un jeton peut représenter un objet de collection numérique (par exemple, CryptoPunks) ou un objet de collection physique (par exemple, une peinture).

Identité

Un jeton peut représenter une identité numérique (par exemple, un avatar) ou une identité légale (par exemple, une carte d’identité nationale).

Attestation

Un jeton peut représenter une certification ou une attestation de fait par une autorité ou par un système de réputation décentralisé (par exemple, acte de mariage, certificat de naissance, diplôme universitaire).

Utilitaire

Un jeton peut être utilisé pour accéder ou payer un service.

Souvent, un seul jeton englobe plusieurs de ces fonctions. Parfois, il est difficile de les distinguer, car les équivalents physiques ont toujours été inextricablement liés. Par exemple, dans le monde physique, un permis de conduire (attestation) est aussi un document d’identité (identité) et les deux ne peuvent pas être séparés. Dans le domaine numérique, les fonctions précédemment mélangées peuvent être séparées et développées indépendamment (par exemple, une attestation anonyme).

Jetons et fongibilité

Wikipedia dit : " En économie, la fongibilité est la propriété d’un bien ou d’une marchandise dont les unités individuelles sont essentiellement interchangeables."

Les jetons sont fongibles lorsque nous pouvons remplacer n’importe quelle unité du jeton par une autre sans aucune différence dans sa valeur ou sa fonction.

À proprement parler, si la provenance historique d’un jeton peut être suivie, il n’est pas entièrement fongible. La possibilité de suivre la provenance peut conduire à la mise sur liste noire et à la liste blanche, réduisant ou éliminant la fongibilité.

Les jetons non fongibles sont des jetons qui représentent chacun un élément tangible ou intangible unique et ne sont donc pas interchangeables. Par exemple, un jeton qui représente la propriété d’un tableau spécifique de Van Gogh n’est pas équivalent à un autre jeton qui représente un Picasso, même s’ils peuvent faire partie du même système de "jeton de propriété d’art". De même, un jeton représentant un objet de collection numérique spécifique tel qu’un CryptoKitty spécifique n’est pas interchangeable avec un autre CryptoKitty. Chaque jeton non fongible est associé à un identifiant unique, tel qu’un numéro de série.

Nous verrons des exemples de jetons fongibles et non fongibles plus loin dans ce chapitre.

Note

Notez que "fongible" est souvent utilisé pour signifier "directement échangeable contre de l’argent" (par exemple, un jeton de casino peut être "encaissé", contrairement aux jetons de blanchisserie). Ce n’est pas le sens dans lequel nous utilisons le mot ici.

Risque de contrepartie

Le risque de contrepartie est le risque que l'autre partie à une transaction ne respecte pas ses obligations. Certains types de transactions présentent un risque de contrepartie supplémentaire car il y a plus de deux parties impliquées. Par exemple, si vous détenez un certificat de dépôt pour un métal précieux et que vous le vendez à quelqu’un, il y a au moins trois parties dans cette transaction : le vendeur, l’acheteur et le dépositaire du métal précieux. Quelqu’un détient l’actif physique ; par nécessité, ils deviennent partie à l’exécution de la transaction et ajoutent un risque de contrepartie à toute transaction impliquant cet actif. En général, lorsqu’un actif est négocié indirectement via l’échange d’un jeton de propriété, il existe un risque de contrepartie supplémentaire de la part du dépositaire de l’actif. En ont-ils l’atout ? Reconnaîtront-ils (ou autoriseront-ils) le transfert de propriété basé sur le transfert d’un jeton (tel qu’un certificat, un acte, un titre ou un jeton numérique) ? Dans le monde des jetons numériques représentant des actifs, comme dans le monde non numérique, il est important de comprendre qui détient l’actif représenté par le jeton et quelles règles s’appliquent à cet actif sous-jacent.

Tokens et Intrinsicité

Le mot "intrinsèque" dérive du latin "intra", qui signifie "de l’intérieur".

Certains jetons représentent des éléments numériques intrinsèques à la chaîne de blocs. Ces actifs numériques sont régis par des règles consensuelles, tout comme les jetons eux-mêmes. Cela a une implication importante : les jetons qui représentent des actifs intrinsèques ne comportent pas de risque de contrepartie supplémentaire. Si vous détenez les clés d’un CryptoKitty, aucune autre partie ne détient ce CryptoKitty pour vous - vous le possédez directement. Les règles de consensus de la chaîne de blocs s’appliquent et votre propriété (c’est-à-dire votre contrôle) des clés privées équivaut à la propriété de l’actif, sans aucun intermédiaire.

À l’inverse, de nombreux jetons sont utilisés pour représenter des choses extrinsèques, telles que l’immobilier, les actions avec droit de vote des entreprises, les marques et les lingots d’or. La propriété de ces éléments, qui ne sont pas "dans" la chaîne de blocs, est régie par la loi, la coutume et la politique, distinctes des règles consensuelles qui régissent le jeton. En d’autres termes, les émetteurs et propriétaires de jetons peuvent toujours dépendre de contrats réels non intelligents. En conséquence, ces actifs extrinsèques comportent un risque de contrepartie supplémentaire car ils sont détenus par des dépositaires, enregistrés dans des registres externes ou contrôlés par des lois et des politiques en dehors de l’environnement de la chaîne de blocs.

L’une des ramifications les plus importantes des jetons basés sur la chaîne de blocs est la capacité de convertir des actifs extrinsèques en actifs intrinsèques et ainsi de supprimer le risque de contrepartie. Un bon exemple est le passage de l’équité dans une société (extrinsèque) à une équité ou à un jeton de vote dans un DAO ou une organisation similaire (intrinsèque).

Utilisation de jetons : utilité ou équité

Presque tous les projets d’Ethereum sont lancés aujourd’hui avec une sorte de jeton. Mais est-ce que tous ces projets ont vraiment besoin de jetons ? Y a-t-il des inconvénients à utiliser un jeton, ou verrons-nous le slogan "tokéniser toutes les choses" se concrétiser ? En principe, l’utilisation de jetons peut être considérée comme l’ultime outil de gestion ou d’organisation. Dans la pratique, l’intégration des plateformes de chaîne de blocs, y compris Ethereum, dans les structures existantes de la société signifie que, jusqu’à présent, il existe de nombreuses limites à leur applicabilité.

Commençons par clarifier le rôle d’un jeton dans un nouveau projet. La majorité des projets utilisent des jetons de deux manières : soit en tant que « jetons utilitaires », soit en tant que « jetons d’équité ». Très souvent, ces deux rôles sont confondus.

Les jetons utilitaires sont ceux où l’utilisation du jeton est nécessaire pour accéder à un service, une application ou une ressource. Des exemples de jetons utilitaires incluent des jetons qui représentent des ressources telles que le stockage partagé ou l’accès à des services tels que les réseaux de médias sociaux.

Les jetons d’équité sont ceux qui représentent des parts dans le contrôle ou la propriété de quelque chose, comme une startup. Les jetons d’équité peuvent être aussi limités que les actions sans droit de vote pour la distribution des dividendes et des bénéfices, ou aussi expansifs que les actions avec droit de vote dans une organisation autonome décentralisée, où la gestion de la plate-forme se fait par un système de gouvernance complexe basé sur les votes des détenteurs de jetons.

C’est un canard !

De nombreuses startups sont confrontées à un problème difficile : les jetons sont un excellent mécanisme de collecte de fonds, mais offrir des titres (actions) au public est une activité réglementée dans la plupart des juridictions. En déguisant les jetons d’équité en jetons utilitaires, de nombreuses startups espèrent contourner ces restrictions réglementaires et lever des fonds à partir d’une offre publique tout en la présentant comme une prévente de "bons d’accès au service" ou, comme nous les appelons, de jetons utilitaires. Reste à savoir si ces offres d’actions à peine déguisées pourront contourner les régulateurs.

Comme le dit le dicton populaire : "Si ça marche comme un canard et cancane comme un canard, c’est un canard." Les régulateurs ne risquent pas d’être distraits par ces contorsions sémantiques ; bien au contraire, ils sont plus susceptibles de considérer ce sophisme juridique comme une tentative de tromper le public.

Jetons utilitaires : qui en a besoin ?

Le vrai problème est que les jetons utilitaires introduisent des risques importants et des obstacles à l’adoption pour les startups. Peut-être que dans un avenir lointain, "tokéniser toutes les choses" deviendra réalité, mais à l’heure actuelle, l’ensemble des personnes qui comprennent et souhaitent utiliser un jeton est un sous-ensemble du marché déjà petit de la crypto-monnaie.

Pour une startup, chaque innovation représente un risque et un filtre de marché. L’innovation, c’est emprunter le chemin le moins fréquenté, s’éloigner du chemin de la tradition. C’est déjà une promenade solitaire. Si une startup essaie d’innover dans un nouveau domaine technologique, tel que le partage de stockage sur des réseaux P2P, c’est une voie assez solitaire. L’ajout d’un jeton utilitaire à cette innovation et l’obligation pour les utilisateurs d’adopter des jetons afin d’utiliser le service aggravent le risque et augmentent les obstacles à l’adoption. C’est sortir de la piste déjà solitaire de l’innovation de stockage P2P et dans le désert.

Considérez chaque innovation comme un filtre. Cela limite l’adoption au sous-ensemble du marché qui peut devenir les premiers à adopter cette innovation. L’ajout d’un deuxième filtre aggrave cet effet, limitant davantage le marché adressable. Vous demandez à vos premiers utilisateurs d’adopter non pas une mais deux technologies entièrement nouvelles : la nouvelle application/plate-forme/service que vous avez créée et l’économie des jetons.

Pour une startup, chaque innovation introduit des risques qui augmentent les chances d’échec de la startup. Si vous prenez votre idée de démarrage déjà risquée et ajoutez un jeton utilitaire, vous ajoutez tous les risques de la plate-forme sous-jacente (Ethereum), de l’économie plus large (échanges, liquidité), de l’environnement réglementaire (régulateurs des actions/matières premières) et de la technologie (contrats intelligents , normes symboliques). C’est beaucoup de risques pour une startup.

Les partisans de la "tokénisation de toutes les choses" rétorqueront probablement qu’en adoptant des jetons, ils héritent également de l’enthousiasme du marché, des premiers utilisateurs, de la technologie, de l’innovation et de la liquidité de l’ensemble de l’économie des jetons. C’est vrai aussi. La question est de savoir si les avantages et l’enthousiasme l’emportent sur les risques et les incertitudes.

Néanmoins, certaines des idées commerciales les plus innovantes se déroulent effectivement dans le domaine de la cryptographie. Si les régulateurs ne sont pas assez rapides pour adopter des lois et soutenir de nouveaux modèles commerciaux, les entrepreneurs et les talents associés chercheront à opérer dans d’autres juridictions plus favorables à la cryptographie. Cela se produit déjà.

Enfin, au début de ce chapitre, lors de l’introduction des jetons, nous avons discuté de la signification familière du « jeton » comme « quelque chose de valeur insignifiante ». La raison sous-jacente de la valeur insignifiante de la plupart des jetons est qu’ils ne peuvent être utilisés que dans un contexte très étroit : une compagnie d’autobus, une buanderie automatique, une arcade, un hôtel ou un magasin d’entreprise. Une liquidité limitée, une applicabilité limitée et des coûts de conversion élevés réduisent la valeur des jetons jusqu’à ce qu’ils n’aient plus qu’une valeur « symbolique ». Ainsi, lorsque vous ajoutez un jeton utilitaire à votre plateforme, mais que le jeton ne peut être utilisé que sur votre seule plateforme avec un petit marché, vous recréez les conditions qui ont rendu les jetons physiques sans valeur. Cela peut en effet être la bonne façon d’intégrer la tokenisation dans votre projet. Cependant, si pour utiliser votre plate-forme, un utilisateur doit convertir quelque chose en votre jeton utilitaire, l’utiliser, puis reconvertir le reste en quelque chose de plus généralement utile, vous avez créé un certificat d’entreprise. Les coûts de commutation d’un jeton numérique sont des ordres de grandeur inférieurs à ceux d’un jeton physique sans marché, mais ils ne sont pas nuls. Les jetons utilitaires qui fonctionnent dans tout un secteur industriel seront très intéressants et probablement très précieux. Mais si vous configurez votre startup pour qu’elle doive amorcer une norme industrielle entière pour réussir, vous avez peut-être déjà échoué.

Note

L’un des avantages du déploiement de services sur des plates-formes à usage général comme Ethereum est de pouvoir connecter des contrats intelligents (et donc l’utilité des jetons) à travers les projets, augmentant ainsi le potentiel de liquidité et l’utilité des jetons.

Prenez cette décision pour les bonnes raisons. Adoptez un jeton car votre application ne peut pas fonctionner sans jeton. Adoptez-le car le jeton lève une barrière fondamentale du marché ou résout un problème d’accès. N’introduisez pas de jeton utilitaire car c’est le seul moyen de collecter des fonds rapidement et vous devez prétendre qu’il ne s’agit pas d’une offre publique de titres .

Jetons sur Ethereum

Les jetons de chaîne de blocs existaient avant Ethereum. D’une certaine manière, la première monnaie chaîne de blocs, Bitcoin, est un jeton lui-même. De nombreuses plateformes de jetons ont également été développées sur Bitcoin et d’autres crypto-monnaies avant Ethereum. Cependant, l’introduction de la première norme de jeton sur Ethereum a conduit à une explosion de jetons.

Vitalik Buterin a suggéré les jetons comme l’une des applications les plus évidentes et les plus utiles d’une chaîne de blocs programmable généralisée telle qu’Ethereum. En fait, au cours de la première année d’Ethereum, il était courant de voir Vitalik et d’autres porter des T-shirts arborant le logo Ethereum et un échantillon de contrat intelligent au dos. Il y avait plusieurs variantes de ce T-shirt, mais la plus courante montrait une implémentation d’un jeton.

Avant de nous plonger dans les détails de la création de jetons sur Ethereum, il est important d’avoir un aperçu du fonctionnement des jetons sur Ethereum. Les jetons sont différents de l’ether car le protocole Ethereum ne sait rien d’eux. L’envoi d’ether est une action intrinsèque de la plateforme Ethereum, mais l’envoi ou même la possession de jetons ne l’est pas. Le solde d’ether des comptes Ethereum est géré au niveau du protocole, tandis que le solde de jetons des comptes Ethereum est géré au niveau du contrat intelligent. Afin de créer un nouveau jeton sur Ethereum, vous devez créer un nouveau contrat intelligent. Une fois déployé, le contrat intelligent gère tout, y compris la propriété, les transferts et les droits d’accès. Vous pouvez rédiger votre contrat intelligent pour effectuer toutes les actions nécessaires comme vous le souhaitez, mais il est probablement plus sage de suivre une norme existante. Nous examinerons ensuite ces normes. Nous discutons des avantages et des inconvénients des normes suivantes à la fin du chapitre.

La norme de jeton ERC20

La première norme a été introduite en novembre 2015 par Fabian Vogelsteller en tant que demande de commentaires Ethereum (ERC). Il s’est automatiquement vu attribuer le numéro de problème GitHub 20, donnant lieu au nom de "jeton ERC20". La grande majorité des jetons sont actuellement basés sur la norme ERC20. La demande de commentaires ERC20 est finalement devenue la proposition d’amélioration Ethereum 20 (EIP-20), mais elle est encore principalement désignée par le nom d’origine, ERC20.

ERC20 est une norme pour les jetons fongibles, ce qui signifie que différentes unités d’un token ERC20 sont interchangeables et n’ont pas de propriétés uniques.

La norme ERC20 définit une interface commune pour les contrats mettant en œuvre un jeton, de sorte que tout jeton compatible est accessible et utilisable de la même manière. L’interface se compose d’un certain nombre de fonctions qui doivent être présentes dans chaque implémentation de la norme, ainsi que de certaines fonctions et attributs facultatifs qui peuvent être ajoutés par les développeurs.

Fonctions et événements requis par ERC20

Un contrat de jeton conforme à ERC20 doit fournir au moins les fonctions et événements suivants :

totalSupply

Renvoie le nombre total d’unités de ce jeton qui existent actuellement. Les jetons ERC20 peuvent avoir une offre fixe ou variable.

balanceOf

Étant donné une adresse, renvoie le solde du jeton de cette adresse.

transfer

Étant donné une adresse et un montant, transfère ce montant de jetons à cette adresse, à partir du solde de l’adresse qui a exécuté le transfert.

transferFrom

Étant donné un expéditeur, un destinataire et un montant, transfère des jetons d’un compte à un autre. Utilisé en combinaison avec approve.

approve

Étant donné une adresse et un montant de destinataire, autorise cette adresse à exécuter plusieurs virements jusqu’à ce montant, à partir du compte qui a émis l’approbation.

allowance

Étant donné une adresse de propriétaire et une adresse de dépensier, renvoie le montant restant que le dépensier est autorisé à retirer au propriétaire.

Transfer

Evénement déclenché lors d’un transfert réussi (appel à transfer ou transferFrom)(même pour les transferts de valeur nulle).

Approval

Événement enregistré lors d’un appel réussi à approve.

Fonctions optionnelles ERC20

En plus des fonctions requises listées dans la section précédente, les fonctions optionnelles suivantes sont également définies par la norme :

name

Renvoie le nom lisible par l’homme (par exemple, "US Dollars") du jeton.

symbol

Renvoie un symbole lisible par l’homme (par exemple, "USD") pour le jeton.

decimals

Renvoie le nombre de décimales utilisées pour diviser les quantités de jetons. Par exemple, si decimals vaut 2, alors le montant du jeton est divisé par 100 pour obtenir sa représentation d’usage.

L’interface ERC20 définie dans Solidity

Voici à quoi ressemble une spécification d’interface ERC20 dans Solidity :

contract ERC20 {
   function totalSupply() constant returns (uint theTotalSupply);
   function balanceOf(address _owner) constant returns (uint balance);
   function transfer(address _to, uint _value) returns (bool success);
   function transferFrom(address _from, address _to, uint _value) returns
      (bool success);
   function approve(address _spender, uint _value) returns (bool success);
   function allowance(address _owner, address _spender) constant returns
      (uint remaining);
   event Transfer(address indexed _from, address indexed _to, uint _value);
   event Approval(address indexed _owner, address indexed _spender, uint _value);
}
Structures de données ERC20

Si vous examinez une implémentation ERC20, vous verrez qu’elle contient deux structures de données, une pour suivre les soldes et une pour suivre indemnités. Dans Solidity, ils sont implémentés avec un data mapping ou mappage de données.

Le premier mappage de données implémente une table interne des soldes de jetons, par propriétaire. Cela permet au contrat de jeton de garder une trace de qui possède les jetons. Chaque transfert est une déduction d’un solde et un ajout à un autre solde :

mapping(address => uint256) balances;

La deuxième structure de données est un mappage de données d’allocations. Comme nous le verrons dans la section suivante, avec les jetons ERC20, le propriétaire d’un jeton peut déléguer l’autorité à un dépensier, lui permettant de dépenser un montant spécifique (allocation) à partir du solde du propriétaire. Le contrat ERC20 assure le suivi des allocations avec un mappage bidimensionnel, la clé primaire étant l’adresse du propriétaire du jeton, mappée à une adresse de dépense et un montant d’allocation :

mapping (address => mapping (address => uint256)) public allowed;
Flux de travail ERC20 : "transfer" et "approve et transferFrom"

La norme de jeton ERC20 a deux fonctions de transfert. Vous vous demandez peut-être pourquoi.

ERC20 permet deux flux de travail différents. Le premier est un flux de travail simple et à transaction unique utilisant la fonction transfer. Ce flux de travail est celui utilisé par les portefeuilles pour envoyer des jetons à d’autres portefeuilles. La grande majorité des transactions de jetons se produisent avec le flux de travail transfer.

L’exécution du contrat de transfert est très simple. Si Alice veut envoyer 10 jetons à Bob, son portefeuille envoie une transaction à l’adresse du contrat de jeton, en appelant la fonction transfer avec l’adresse de Bob et 10 comme arguments. Le contrat de jeton ajuste le solde d’Alice (–10) et le solde de Bob (+10) et émet un événement Transfer.

Le deuxième flux de travail est un processus à deux transactions qui utilise approve suivi de transferFrom. Ce workflow permet à un propriétaire de jeton de déléguer son contrôle à une autre adresse. Il est le plus souvent utilisé pour déléguer le contrôle à un contrat de distribution de jetons, mais il peut également être utilisé par les échanges.

Par exemple, si une entreprise vend des tokens pour une ICO (Initial Coin Offerings ou "offres initiales de pièces"), elle peut approve (approuver) une adresse de contrat de crowdsale pour distribuer une certaine quantité de jetons. Le contrat de crowdsale peut alors transferFrom le solde du propriétaire du contrat de jeton à chaque acheteur du jeton, comme illustré dans Le flux de travail d’approbation et de transfert en deux étapes des jetons ERC20.

Note

("Initial Coin Offerings (ICOs)","défini"Une Initial Coin Offering (ICO) ou Offres initiales de pièces est un mécanisme de financement participatif utilisé par les entreprises et les organisations pour collecter des fonds en vendant des jetons. Le terme est dérivé de l’offre publique initiale (IPO), qui est le processus par lequel une société publique propose des actions à vendre aux investisseurs en bourse. Contrairement aux marchés des introductions en bourse hautement réglementés, les ICO sont ouverts, mondiaux et désordonnés. Les exemples et les explications des ICO dans ce livre ne constituent pas une approbation de ce type de collecte de fonds.

Le flux de travail d’approbation et de transfert en deux étapes des jetons ERC20
Figure 38. Le flux de travail d’approbation et de transfert en deux étapes des jetons ERC20

Pour le flux de travail approve et transferFrom, deux transactions sont nécessaires. Disons qu’Alice veut autoriser le contrat AliceICO à vendre 50 % de tous les jetons AliceCoin à des acheteurs comme Bob et Charlie. Tout d’abord, Alice lance le contrat AliceCoin ERC20, émettant tous les AliceCoin à sa propre adresse. Ensuite, Alice lance le contrat AliceICO qui peut vendre des jetons contre de l’ether. Ensuite, Alice lance le flux de travail approve et transferFrom. Elle envoie une transaction au contrat AliceCoin, en appelant approve avec l’adresse du contrat AliceICO et 50 % du totalSupply comme arguments. Cela déclenchera l’événement Approbation. Désormais, le contrat AliceICO peut vendre AliceCoin.

Lorsque le contrat AliceICO reçoit de l’ether de Bob, il doit envoyer des AliceCoin à Bob en retour. Dans le contrat AliceICO se trouve un taux de change entre AliceCoin et de l’Ether. Le taux de change qu’Alice a fixé lors de la création du contrat AliceICO détermine le nombre de jetons que Bob recevra pour la quantité d’ether envoyé au contrat AliceICO. Lorsque le contrat AliceICO appelle la fonction AliceCoin transferFrom, il définit l’adresse d’Alice comme expéditeur et l’adresse de Bob comme destinataire, et utilise le taux de change pour déterminer le nombre de jetons AliceCoin qui seront transférés à Bob dans le champ value. Le contrat AliceCoin transfère le solde de l’adresse d’Alice à l’adresse de Bob et déclenche un événement Transfer. Le contrat AliceICO peut appeler transferFrom un nombre illimité de fois, tant qu’il ne dépasse pas la limite d’approbation définie par Alice. Le contrat AliceICO peut suivre le nombre de jetons AliceCoin qu’il peut vendre en appelant la fonction allowance.

Implémentations ERC20

Bien qu’il soit possible d’implémenter un jeton compatible ERC20 dans environ 30 lignes de code Solidity, la plupart des implémentations sont plus complexes. Ceci pour tenir compte des vulnérabilités de sécurité potentielles. Deux implémentations sont mentionnées dans la norme EIP-20 :

Consensys EIP20

Une implémentation simple et facile à lire d’un jeton compatible ERC20.

OpenZeppelin StandardToken

Cette implémentation est compatible ERC20, avec des précautions de sécurité supplémentaires. Il constitue la base des bibliothèques OpenZeppelin implémentant des jetons compatibles ERC20 plus complexes avec des plafonds de collecte de fonds, des enchères, des calendriers d’acquisition et d’autres fonctionnalités.

Lancement de notre propre jeton ERC20

Créons et lançons notre propre jeton. Pour cet exemple, nous utiliserons le cadre de développement (framework) Truffle. L’exemple suppose que vous avez déjà installé truffle et que vous l’avez configuré, et que vous êtes familiarisé avec son fonctionnement de base (pour plus de détails, voir Truffle).

Nous appellerons notre jeton "Mastering Ethereum Token", avec le symbole "MET".

Note

Vous pouvez trouver cet exemple dans le référentiel GitHub du livre.

Commençons par créer et initialiser un répertoire de projet Truffle. Exécutez ces quatre commandes et acceptez les réponses par défaut à toutes les questions :

$ mkdir METoken
$ cd METoken
METoken $ truffle init
METoken $ npm init

Vous devriez maintenant avoir la structure de répertoires suivante :

METoken/
+---- contracts
|   `---- Migrations.sol
+---- migrations
|   `---- 1_initial_migration.js
+---- package.json
+---- test
+---- truffle-config.js
`---- truffle.js

Modifiez le fichier de configuration truffle.js ou truffle-config.js pour configurer votre environnement Truffle, ou copiez ce dernier depuis le référentiel.

Si vous utilisez l’exemple truffle-config.js, pensez à créer un fichier .env dans le dossier METoken contenant vos clés privées de test pour le test et le déploiement sur les réseaux publics de test Ethereum, tels que Ropsten ou Kovan. Vous pouvez exporter votre clé privée de réseau de test à partir de MetaMask.

Après cela, votre répertoire devrait ressembler à :

METoken/
+---- contracts
|   `---- Migrations.sol
+---- migrations
|   `---- 1_initial_migration.js
+---- package.json
+---- test
+---- truffle-config.js
+---- truffle.js
`---- .env *new file*
Warning

N’utilisez que des clés de test ou des mnémoniques de test qui ne sont pas utilisés pour détenir des fonds sur le réseau Ethereum principal. Ne jamais utiliser des clés contenant de l’argent réel pour les tests.

Pour notre exemple, nous allons importer la bibliothèque OpenZeppelin, qui implémente des contrôles de sécurité importants et est facile à étendre :

$ npm install [email protected]

+ [email protected]
added 1 package from 1 contributor and audited 2381 packages in 4.074s

Le module de dévelopement openzeppelin-solidity ajoutera environ 250 fichiers sous le répertoire node_modules. La bibliothèque OpenZeppelin comprend bien plus que le jeton ERC20, mais nous n’en utiliserons qu’une petite partie.

Ensuite, écrivons notre contrat de jeton. Créez un nouveau fichier, METoken.sol, et copiez l’exemple de code depuis GitHub.

Notre contrat, illustré en METoken.sol : Un contrat Solidity implémentant un token ERC20, est très simple, car il hérite toutes ses fonctionnalités de la bibliothèque OpenZeppelin.

Example 20. METoken.sol : Un contrat Solidity implémentant un token ERC20
pragma solidity ^0.4.21;

import 'openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

contract METoken is StandardToken {
    string public constant name = 'Mastering Ethereum Token';
    string public constant symbol = 'MET';
    uint8 public constant decimals = 2;
    uint constant _initial_supply = 2100000000;

    function METoken() public {
        totalSupply_ = _initial_supply;
        balances[msg.sender] = _initial_supply;
        emit Transfer(address(0), msg.sender, _initial_supply);
    }
}

Ici, nous définissons les variables optionnelles name, symbol et decimals. Nous définissons également une variable _initial_supply, définie sur 21 millions de jetons ; avec deux décimales de subdivision qui donne 2,1 milliards d’unités au total. Dans la fonction d’initialisation (constructeur) du contrat, nous définissons le totalSupply égal à _initial_supply et allouons tout le _initial_supply au solde du compte (msg.sender)qui crée le contrat METoken.

Nous utilisons maintenant truffle pour compiler le code METoken :

$ truffle compile
Compiling ./contracts/METoken.sol...
Compiling ./contracts/Migrations.sol...
Compiling openzeppelin-solidity/contracts/math/SafeMath.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/BasicToken.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20Basic.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol...

Comme vous pouvez le voir, truffle intègre les dépendances nécessaires des bibliothèques OpenZeppelin et compile également ces contrats.

Configurons un script de migration pour déployer le contrat METoken. Créez un nouveau fichier appelé 2_deploy_contracts.js, dans le dossier METoken/migrations. Copiez le contenu de l’exemple dans le référentiel GitHub :

2_deploy_contracts : Migration pour déployer METoken
var METoken = artifacts.require("METoken");

module.exports = function(deployer) {
  // Déployer le contrat METoken comme seule tâche
  deployer.deploy(METoken);
};

Avant de déployer sur l’un des réseaux de test Ethereum, démarrons une chaîne de blocs locale pour tout tester. Démarrez la chaîne de blocs ganache, soit depuis la ligne de commande avec ganache-cli, soit depuis l’interface utilisateur graphique.

Une fois ganache lancé, nous pouvons déployer notre contrat METoken et voir si tout fonctionne comme prévu :

$ truffle migrate --network ganache
Using network 'ganache'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xb2e90a056dc6ad8e654683921fc613c796a03b89df6760ec1db1084ea4a084eb
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying METoken...
  ... 0xbe9290d59678b412e60ed6aefedb17364f4ad2977cfb2076b9b8ad415c5dc9f0
  METoken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

Sur la console ganache, nous devrions voir que notre déploiement a créé quatre nouvelles transactions, comme illustré dans Déploiement METoken sur ganache.

Déploiement de METoken sur Ganache
Figure 39. Déploiement METoken sur ganache
Interagir avec METoken à l’aide de la console Truffle

Nous pouvons interagir avec notre contrat sur la chaîne de blocs ganache en utilisant la console Truffle. Il s’agit d’un environnement JavaScript interactif qui donne accès à l’environnement Truffle et, via web3, à la chaîne de blocs. Dans ce cas, nous allons connecter la console Truffle à la chaîne de blocs ganache :

$ truffle console --network ganache
truffle(ganache)>

L’invite truffle(ganache)&gt; indique que nous sommes connectés à la chaîne de blocs ganache et que nous sommes prêts à taper nos commandes. La console Truffle prend en charge toutes les commandes truffle, nous pourrions donc compiler et migrer depuis la console. Nous avons déjà exécuté ces commandes, allons donc directement au contrat lui-même. Le contrat METoken existe en tant qu’objet JavaScript dans l’environnement Truffle. Tapez METoken** à l’invite et il affichera l’intégralité de la définition du contrat :

truffle(ganache)> METoken
{ [Function: TruffleContract]
  _static_methods:

[...]

currentProvider:
 HttpProvider {
   host: 'http://localhost:7545',
   timeout: 0,
   user: undefined,
   password: undefined,
   headers: undefined,
   send: [Function],
   sendAsync: [Function],
   _alreadyWrapped: true },
network_id: '5777' }

L’objet METoken expose également plusieurs attributs, tels que l’adresse du contrat (telle que déployée par la commande migrate) :

truffle(ganache)> METoken.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'

Si nous voulons interagir avec le contrat déployé, nous devons utiliser un appel asynchrone, sous la forme d’une "promesse" JavaScript. Nous utilisons la fonction deployed pour obtenir l’instance de contrat, puis appelons la fonction totalSupply :

truffle(ganache)> METoken.deployed().then(instance => instance.totalSupply())
BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

Ensuite, utilisons les comptes créés par ganache pour vérifier notre solde de METoken et envoyer des METoken à une autre adresse. Commençons par obtenir les adresses de compte :

truffle(ganache)> let accounts
undefined
truffle(ganache)> web3.eth.getAccounts((err,res) => { accounts = res })
undefined
truffle(ganache)> accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'

La liste accounts contient maintenant tous les comptes créés par ganache, et account[0] est le compte qui a déployé le contrat METoken. Il devrait avoir un solde de METoken, car notre constructeur METoken donne l’intégralité de l’offre de jetons à l’adresse qui l’a créé. Allons vérifier:

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

Enfin, transférons 1000.00 METoken de account[0] vers account[1], en appelant la fonction transfer du contrat :

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.transfer(accounts[1], 100000) })
undefined
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2099900000 ] }
undefined
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[1]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }

Tip

METoken a 2 décimales de précision, ce qui signifie que 1 METoken correspond à 100 unités dans le contrat. Lorsque nous transférons 1 000 METoken, nous spécifions la valeur comme 100000 dans l’appel à la fonction transfer.

Comme vous pouvez le voir, dans la console, account[0] a maintenant 20 999 000 MET, et account[1] a 1 000 MET.

Si vous passez à l’interface utilisateur graphique ganache, comme indiqué dans Transfert METoken sur ganache, vous verrez la transaction qui a appelé la fonction transfer.

Transfert METoken sur Ganache
Figure 40. Transfert METoken sur ganache
Envoi de jetons ERC20 aux adresses contractuelles

Jusqu’à présent, nous avons configuré un jeton ERC20 et transféré certains jetons d’un compte à un autre. Tous les comptes que nous avons utilisés pour ces démonstrations sont des comptes externes, ce qui signifie qu’ils sont contrôlés par une clé privée et non par un contrat. Que se passe-t-il si nous envoyons MET à une adresse contractuelle ? Découvrons-le!

Tout d’abord, déployons un autre contrat dans notre environnement de test. Pour cet exemple, nous utiliserons notre premier contrat, Faucet.sol. Ajoutons-le au projet METoken en le copiant dans le répertoire contracts. Notre répertoire devrait ressembler à ceci :

METoken/
+---- contracts
|   +---- Faucet.sol
|   +---- METoken.sol
|   `---- Migrations.sol

Nous ajouterons également une migration, pour déployer Faucet séparément de METoken :

var Faucet = artifacts.require("Faucet");

module.exports = function(deployer) {
  // Déployer le contrat Faucet comme seule tâche
  deployer.deploy(Faucet);
};

Compilons et migrons les contrats depuis la console Truffle :

$ truffle console --network ganache
truffle(ganache)> compile
Compiling ./contracts/Faucet.sol...
Writing artifacts to ./build/contracts

truffle(ganache)> migrate
Using network 'ganache'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x89f6a7bd2a596829c60a483ec99665c7af71e68c77a417fab503c394fcd7a0c9
  Migrations: 0xa1ccce36fb823810e729dce293b75f40fb6ea9c9
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Replacing METoken...
  ... 0x28d0da26f48765f67e133e99dd275fac6a25fdfec6594060fd1a0e09a99b44ba
  METoken: 0x7d6bf9d5914d37bcba9d46df7107e71c59f3791f
Saving artifacts...
Running migration: 3_deploy_faucet.js
  Deploying Faucet...
  ... 0x6fbf283bcc97d7c52d92fd91f6ac02d565f5fded483a6a0f824f66edc6fa90c3
  Faucet: 0xb18a42e9468f7f1342fa3c329ec339f254bc7524
Saving artifacts...

Génial. Envoyons maintenant du MET au contrat Faucet :

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.transfer(Faucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(Faucet.address).then(console.log)})
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }

D’accord, nous avons transféré 1 000 MET au contrat Faucet. Maintenant, comment retirer ces jetons ?

N’oubliez pas que Faucet.sol est un contrat assez simple. Il n’a qu’une seule fonction, withdraw, qui sert à retirer de l'ether. Il n’a pas de fonction pour retirer le MET, ou tout autre jeton ERC20. Si nous utilisons withdraw, il essaiera d’envoyer de l’ether, mais comme Faucet n’a pas encore de solde d’ether, il échouera.

Le contrat METoken sait que Faucet a un solde, mais le seul moyen de transférer ce solde est de recevoir un appel transfer de l’adresse du contrat. D’une manière ou d’une autre, nous devons faire en sorte que le contrat Faucet appelle la fonction transfer dans METoken.

Si vous vous demandez quoi faire ensuite, ne le faites pas. Il n’y a pas de solution à ce problème. Le MET envoyé à Faucet est bloqué, pour toujours. Seul le contrat Faucet peut le transférer, et le contrat Faucet n’a pas de code pour appeler la fonction transfer d’un contrat de jeton ERC20.

Peut-être avez-vous anticipé ce problème. Très probablement, vous ne l’avez pas fait. En fait, des centaines d’utilisateurs d’Ethereum qui ont accidentellement transféré divers jetons vers des contrats qui n’avaient aucune capacité ERC20 n’en ont pas non plus fait. Selon certaines estimations, des jetons d’une valeur de plus d’environ 2,5 millions de dollars (au moment de la rédaction) sont restés "bloqués" comme ça et sont perdus à jamais.

L’une des façons dont les utilisateurs de jetons ERC20 peuvent perdre par inadvertance leurs jetons lors d’un transfert, c’est lorsqu’ils tentent de transférer vers un échange ou un autre service. Ils copient une adresse Ethereum à partir du site Web d’un échange, pensant qu’ils peuvent simplement lui envoyer des jetons. Cependant, de nombreux échanges publient des adresses de réception qui sont en fait des contrats ! Ces contrats sont uniquement destinés à recevoir de l’ether, pas des jetons ERC20, balayant le plus souvent tous les fonds qui leur sont envoyés vers un « stockage à froid » ou un autre portefeuille centralisé. Malgré les nombreux avertissements indiquant "n’envoyez pas de jetons à cette adresse", de nombreux jetons sont perdus de cette façon.

Démonstration du flux de travail "approuver et transférer de"

Notre contrat Faucet ne pouvait pas gérer les jetons ERC20. L’envoi de jetons à l’aide de la fonction + transfer + a entraîné la perte de ces jetons. Réécrivons maintenant le contrat et faisons en sorte qu’il gère les jetons ERC20. Plus précisément, nous allons le transformer en un robinet qui donne du MET à quiconque le demande.

Pour cet exemple, nous allons faire une copie du répertoire du projet truffle (nous l’appellerons METoken_METFaucet), initialiser truffle et npm, installer les dépendances OpenZeppelin et copier le contrat METoken.sol. Voir notre premier exemple, dans Lancement de notre propre jeton ERC20, pour les instructions détaillées.

Notre nouveau contrat de robinetterie, METFaucet.sol, ressemblera à METFaucet.sol : Un robinet pour METoken.

Example 21. METFaucet.sol : Un robinet pour METoken
// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.4.19;

import 'openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol';


// Un robinet pour le jeton ERC20 MET
contract METFaucet {

	StandardToken public METoken;
	address public METOwner;

	// Constructeur METFaucet, indique l'adresse du contrat METoken et
	// l'adresse du propriétaire que nous serons autorisés pour transferFrom
	function METFaucet(address _METoken, address _METOwner) public {

		// Initialise le METoken à partir de l'adresse fournie
		METoken = StandardToken(_METoken);
		METOwner = _METOwner;
	}

	function withdraw(uint withdraw_amount) public {

		// Limiter le montant du retrait à 10 MET
    	require(withdraw_amount <= 1000);

		// Utiliser la fonction transferFrom de METoken
		METoken.transferFrom(METOwner, msg.sender, withdraw_amount);
    }

	// REJETER tout éther entrant
	function () external payable { revert(); }

}

Nous avons apporté quelques modifications à l’exemple de base de Faucet. Puisque METFaucet utilisera la fonction transferFrom dans METoken, il aura besoin de deux variables supplémentaires. L’un contiendra l’adresse du contrat METoken déployé. L’autre détiendra l’adresse du propriétaire du MET, qui approuvera les retraits du robinet. Le contrat METFaucet appellera METoken.transferFrom et lui demandera de déplacer le MET du propriétaire vers l’adresse d’où provient la demande de retrait du robinet.

Nous déclarons ici ces deux variables :

StandardToken public METoken;
address public METOwner;

Étant donné que notre robinet doit être initialisé avec les adresses correctes pour METoken et METOwner, nous devons déclarer un constructeur personnalisé :

// Constructeur METFaucet - fournir l'adresse du contrat METoken et
// l'adresse du propriétaire que nous serons autorisés à transférer avec transferFrom
function METFaucet(address _METoken, address _METOwner) public {

	// Initialise le METoken à partir de l'adresse fournie
	METoken = StandardToken(_METoken);
	METOwner = _METOwner;
}

Le prochain changement concerne la fonction withdraw. Au lieu d’appeler transfer, METFaucet utilise la fonction transferFrom dans METoken et demande à METoken de transférer MET au destinataire du robinet :

// Utiliser la fonction transferFrom de METoken
METoken.transferFrom(METOwner, msg.sender, withdraw_amount);

Enfin, puisque notre robinet n’envoie plus d’ether, nous devrions probablement empêcher quiconque d’envoyer de l’ether à METFaucet, car nous ne voudrions pas qu’il reste bloqué. Nous modifions la fonction de paiement de secours pour rejeter l’ether entrant, en utilisant la fonction revert pour annuler tout paiement entrant :

// REJETER tout ether entrant
function () external payable { revert(); }

Maintenant que notre code METFaucet.sol est prêt, nous devons modifier le script de migration pour le déployer. Ce script de migration sera un peu plus complexe, car METFaucet dépend de l’adresse de METoken. Nous utiliserons une promesse JavaScript pour déployer les deux contrats en séquence. Créez 2_deploy_contracts.js comme suit :

var METoken = artifacts.require("METoken");
var METFaucet = artifacts.require("METFaucet");
var owner = web3.eth.accounts[0];

module.exports = function(deployer) {

	// Déployer d'abord le contrat METoken
	deployer.deploy(METoken, {from: owner}).then(function() {
		// Ensuite, déployer METFaucet et passez l'adresse de METoken et
		// l'adresse du propriétaire de tous les MET qui agréera METFaucet
		return deployer.deploy(METFaucet, METoken.address, owner);
  	});
}

Maintenant, nous pouvons tout tester dans la console Truffle. Tout d’abord, nous utilisons migrate pour déployer les contrats. Lorsque METoken est déployé, il alloue tout le MET au compte qui l’a créé, web3.eth.accounts[0]. Ensuite, nous appelons la fonction approve dans METoken pour approuver METFaucet afin d’envoyer jusqu’à 1 000 MET au nom de web3.eth.accounts[0]. Enfin, pour tester notre faucet, nous appelons METFaucet.withdraw depuis web3.eth.accounts[1] et essayons de retirer 10 MET. Voici les commandes de la console :

$ truffle console --network ganache
truffle(ganache)> migrate
Using network 'ganache'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x79352b43e18cc46b023a779e9a0d16b30f127bfa40266c02f9871d63c26542c7
  Migrations: 0xaa588d3737b611bafd7bd713445b314bd453a5c8
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Replacing METoken...
  ... 0xc42a57f22cddf95f6f8c19d794c8af3b2491f568b38b96fef15b13b6e8bfff21
  METoken: 0xf204a4ef082f5c04bb89f7d5e6568b796096735a
  Replacing METFaucet...
  ... 0xd9615cae2fa4f1e8a377de87f86162832cf4d31098779e6e00df1ae7f1b7f864
  METFaucet: 0x75c35c980c0d37ef46df04d31a140b65503c0eed
Saving artifacts...
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.approve(METFaucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
truffle(ganache)> BigNumber { s: 1, e: 0, c: [ 0 ] }
truffle(ganache)> METFaucet.deployed().then(instance =>
                  { instance.withdraw(1000, {from:web3.eth.accounts[1]}) } )
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
truffle(ganache)> BigNumber { s: 1, e: 3, c: [ 1000 ] }

Comme vous pouvez le voir sur les résultats, nous pouvons utiliser le flux de travail approve et transferFrom pour autoriser un contrat à transférer des jetons définis dans un autre jeton. S’ils sont correctement utilisés, les jetons ERC20 peuvent être utilisés par les EOA et d’autres contrats.

Cependant, la charge de gérer correctement les jetons ERC20 est transmise à l’interface utilisateur. Si un utilisateur tente à tort de transférer des jetons ERC20 vers une adresse de contrat et que ce contrat n’est pas équipé pour recevoir des jetons ERC20, les jetons seront perdus.

Problèmes avec les jetons ERC20

L’adoption de la norme de jeton ERC20 a été vraiment explosive. Des milliers de jetons ont été lancés, à la fois pour expérimenter de nouvelles capacités et pour lever des fonds dans diverses enchères et "financements participatifs" ICO. Cependant, il existe des pièges potentiels, comme nous l’avons vu avec la question du transfert de jetons vers des adresses contractuelles.

L’un des problèmes les moins évidents avec les jetons ERC20 est qu’ils exposent des différences subtiles entre les jetons et l’ether lui-même. Lorsque l’ether est transféré par une transaction qui a une adresse de destinataire comme destination, les transferts de jeton se produisent dans l'état spécifique du contrat de jeton et ont le contrat de jeton comme destination, et non l’adresse du destinataire. Le contrat de jeton suit les soldes et émet des événements. Dans un transfert de jeton, aucune transaction n’est réellement envoyée au destinataire du jeton. Au lieu de cela, l’adresse du destinataire est ajoutée à un tableau dans le contrat de jeton lui-même. Une transaction envoyant de l’ether à une adresse modifie l’état d’une adresse. Une transaction transférant un jeton à une adresse ne modifie que l’état du contrat de jeton, pas l’état de l’adresse du destinataire. Même un portefeuille prenant en charge les jetons ERC20 ne prend pas connaissance d’un solde de jetons à moins que l’utilisateur n’ajoute explicitement un contrat de jeton spécifique à « surveiller ». Certains portefeuilles surveillent les contrats de jetons les plus populaires pour détecter les soldes détenus par les adresses qu’ils contrôlent, mais cela est limité à une petite fraction des contrats ERC20 existants.

En fait, il est peu probable qu’un utilisateur veuille suivre tous les soldes de tous les contrats de jetons ERC20 possibles. De nombreux jetons ERC20 ressemblent plus à du courrier indésirable qu’à des jetons utilisables. Ils créent automatiquement des soldes pour les comptes qui ont une activité ether, afin d’attirer les utilisateurs. Si vous avez une adresse Ethereum avec une longue histoire d’activité, surtout si elle a été créée lors de la prévente, vous la trouverez pleine de jetons "indésirables" qui sont apparus de nulle part. Bien sûr, l’adresse n’est pas vraiment pleine de jetons ; ce sont les contrats symboliques qui contiennent votre adresse. Vous ne voyez ces soldes que si ces contrats de jetons sont surveillés par l’explorateur de blocs ou le portefeuille que vous utilisez pour afficher votre adresse.

Les jetons ne se comportent pas de la même manière que l’ether. L’ether est envoyé avec la fonction send et accepté par toute fonction payante dans un contrat ou toute adresse détenue en externe. Les jetons sont envoyés à l’aide des fonctions transfer ou approve et transferFrom qui n’existent que dans le contrat ERC20, et ne déclenchent (du moins dans ERC20) aucune fonction payante dans un contrat destinataire. Les jetons sont censés fonctionner comme une cryptomonnaie telle que l’ether, mais ils présentent certaines différences qui brisent cette illusion.

Envisagez un autre problème. Pour envoyer de l’ether ou utiliser un contrat Ethereum, vous avez besoin d’ether pour payer le gaz. Pour envoyer des jetons, vous avez également besoin d’ether. Vous ne pouvez pas payer le gaz d’une transaction avec un jeton et le contrat de jeton ne peut pas payer le gaz pour vous. Cela peut changer à un moment donné dans un avenir lointain, mais entre-temps, cela peut entraîner des expériences utilisateur plutôt étranges. Par exemple, supposons que vous utilisiez un échange ou ShapeShift pour convertir du bitcoin en jeton. Vous "recevez" le jeton dans un portefeuille qui suit le contrat de ce jeton et affiche votre solde. Il ressemble à toutes les autres crypto-monnaies que vous avez dans votre portefeuille. Essayez d’envoyer le jeton et votre portefeuille vous informera que vous avez besoin d’ether pour le faire. Vous pourriez être confus - après tout, vous n’aviez pas besoin d’ether pour recevoir le jeton. Peut-être que vous n’avez pas d’ether. Peut-être ne saviez-vous même pas que le jeton était un jeton ERC20 sur Ethereum; peut-être pensiez-vous qu’il s’agissait d’une cryptomonnaie avec sa propre chaîne de blocs. L’illusion vient de se briser.

Certains de ces problèmes sont spécifiques aux jetons ERC20. D’autres sont des problèmes plus généraux liés à l’abstraction et aux limites d’interface au sein d’Ethereum. Certains peuvent être résolus en modifiant l’interface du jeton, tandis que d’autres peuvent nécessiter des modifications des structures fondamentales au sein d’Ethereum (comme la distinction entre EOA et contrats, et entre transactions et messages). Certains peuvent ne pas être exactement "solvable" et peuvent nécessiter une conception d’interface utilisateur pour masquer les nuances et rendre l’expérience utilisateur cohérente, quelles que soient les distinctions sous-jacentes.

Dans les prochaines sections, nous examinerons diverses propositions qui tentent de résoudre certains de ces problèmes.

ERC223 : une norme d’interface de contrat de jeton proposée

La proposition ERC223 tente de résoudre le problème du transfert par inadvertance de jetons vers un contrat (qui peut ou non prendre en charge jetons) en détectant si l’adresse de destination est un contrat ou non. ERC223 exige que les contrats conçus pour accepter des jetons implémentent une fonction nommée tokenFallback. Si la destination d’un transfert est un contrat et que le contrat ne prend pas en charge les jetons (c’est-à-dire qu’il n’implémente pas tokenFallback), le transfert échoue.

Pour détecter si l’adresse de destination est un contrat, l’implémentation de référence ERC223 utilise un petit segment de code intermédiaire en ligne d’une manière plutôt créative :

function isContract(address _addr) private view returns (bool is_contract) {
  uint length;
    assembly {
       // récupère la taille du code sur l'adresse cible ; cela nécessite un assemblage
       length := extcodesize(_addr)
    }
    return (length>0);
}

La spécification de l’interface de contrat ERC223 est :

interface ERC223Token {
  uint public totalSupply;
  function balanceOf(address who) public view returns (uint);

  function name() public view returns (string _name);
  function symbol() public view returns (string _symbol);
  function decimals() public view returns (uint8 _decimals);
  function totalSupply() public view returns (uint256 _supply);

  function transfer(address to, uint value) public returns (bool ok);
  function transfer(address to, uint value, bytes data) public returns (bool ok);
  function transfer(address to, uint value, bytes data, string custom_fallback)
      public returns (bool ok);

  event Transfer(address indexed from, address indexed to, uint value,
                 bytes indexed data);
}

ERC223 n’est pas largement mis en œuvre, et il y a un débat dans le fil de discussion ERC sur la rétrocompatibilité et les compromis entre la mise en œuvre des changements au niveau de l’interface du contrat par rapport à l’interface utilisateur. Le débat continue.

ERC777 : une norme d’interface de contrat de jeton proposée

Une autre proposition pour une norme de contrat de jeton améliorée est ERC777. Cette proposition a plusieurs objectifs, notamment :

  • Offrir une interface compatible ERC20

  • Pour transférer des jetons à l’aide d’une fonction send, similaire aux transferts d’ether

  • Pour être compatible avec ERC820 pour l’enregistrement de contrat de jeton

  • Pour permettre aux contrats et aux adresses de contrôler les jetons qu’ils envoient via une fonction tokensToSend qui est appelée avant l’envoi

  • Pour permettre aux contrats et aux adresses d’être informés de la réception des jetons en appelant une fonction tokensReceived dans le destinataire, et pour réduire la probabilité que les jetons soient verrouillés dans des contrats en exigeant que les contrats fournissent une fonction tokensReceived

  • Pour permettre aux contrats existants d’utiliser des contrats par procuration pour les fonctions tokensToSend et tokensReceived

  • Pour fonctionner de la même manière que vous envoyiez vers un contrat ou un EOA

  • Pour fournir des événements spécifiques pour la frappe et la gravure de jetons

  • Pour permettre aux opérateurs (tiers de confiance, destinés à être des contrats vérifiés) de déplacer des jetons pour le compte d’un détenteur de jetons

  • Pour fournir des métadonnées sur les transactions de transfert de jetons dans les champs userData et operatorData

La discussion en cours sur ERC777 peut être trouvée sur GitHub.

La spécification de l’interface de contrat ERC777 est :

interface ERC777Token {
    function name() public constant returns (string);
    function symbol() public constant returns (string);
    function totalSupply() public constant returns (uint256);
    function granularity() public constant returns (uint256);
    function balanceOf(address owner) public constant returns (uint256);

    function send(address to, uint256 amount, bytes userData) public;

    function authorizeOperator(address operator) public;
    function revokeOperator(address operator) public;
    function isOperatorFor(address operator, address tokenHolder)
        public constant returns (bool);
    function operatorSend(address from, address to, uint256 amount,
                          bytes userData,bytes operatorData) public;

    event Sent(address indexed operator, address indexed from,
               address indexed to, uint256 amount, bytes userData,
               bytes operatorData);
    event Minted(address indexed operator, address indexed to,
                 uint256 amount, bytes operatorData);
    event Burned(address indexed operator, address indexed from,
                 uint256 amount, bytes userData, bytes operatorData);
    event AuthorizedOperator(address indexed operator,
                             address indexed tokenHolder);
    event RevokedOperator(address indexed operator, address indexed tokenHolder);
}
Crochets ERC777

La spécification de crochet d’expéditeur de jetons ERC777 est :

interface ERC777TokensSender {
    function tokensToSend(address operator, address from, address to,
                          uint value, bytes userData, bytes operatorData) public;
}

La mise en place de cette interface est nécessaire pour toute adresse souhaitant être notifiée, gérer ou empêcher le débit de jetons. L’adresse pour laquelle le contrat implémente cette interface doit être enregistrée via ERC820, que le contrat implémente l’interface pour lui-même ou pour une autre adresse.

La spécification du crochet du destinataire des jetons ERC777 est :

interface ERC777TokensRecipient {
  function tokensReceived(
     address operator, address from, address to,
    uint amount, bytes userData, bytes operatorData
  ) public;
}

La mise en place de cette interface est nécessaire pour toute adresse souhaitant être notifiée, traiter ou rejeter la réception de jetons. La même logique et les mêmes exigences s’appliquent au destinataire des jetons qu’à l’interface de l’expéditeur des jetons, avec la contrainte supplémentaire que les contrats destinataires doivent implémenter cette interface pour empêcher le verrouillage des jetons. Si le contrat destinataire n’enregistre pas d’adresse implémentant cette interface, le transfert des jetons échouera.

Un aspect important est qu’un seul expéditeur de jeton et un seul destinataire de jeton peuvent être enregistrés par adresse. Par conséquent, pour chaque transfert de jeton ERC777, les mêmes fonctions de crochet sont appelées lors du débit et de la réception de chaque transfert de jeton ERC777. Un jeton spécifique peut être identifié dans ces fonctions à l’aide de l’expéditeur du message, qui est l’adresse de contrat de jeton spécifique, pour gérer un cas d’utilisation particulier.

D’autre part, les mêmes hooks d’expéditeur et de destinataire de jeton peuvent être enregistrés pour plusieurs adresses et les hooks peuvent distinguer qui sont l’expéditeur et le destinataire prévu à l’aide des paramètres "from" et "to".

Une implémentation de référence d’ERC777 est liée dans la proposition. ERC777 dépend d’une proposition parallèle de contrat de registre, spécifiée dans ERC820. Une partie du débat sur ERC777 porte sur la complexité de l’adoption simultanée de deux grands changements : une nouvelle norme de jeton et une norme de registre. La discussion continue.

ERC721 : norme de jeton non fongible (acte)

Toutes les normes de jeton que nous avons examinées jusqu’à présent concernent les jetons fungible, ce qui signifie que les unités de un jeton sont interchangeables. La norme de jeton ERC20 ne suit que le solde final de chaque compte et ne suit pas (explicitement) la provenance d’un jeton.

La proposition ERC721 concerne une norme pour les jetons non fongibles, également appelés actes.

Extrait du dictionnaire Oxford :

acte : un document juridique qui est signé et remis, en particulier un document concernant la propriété d’un bien ou des droits légaux.

L’utilisation du mot "acte" est destinée à refléter la partie "propriété d’un bien", même si ceux-ci ne sont pas reconnus comme des "documents juridiques" dans aucune juridiction - pour le moment. Il est probable qu’à un moment donné dans le futur, la propriété légale basée sur des signatures numériques sur une plate-forme chaîne de blocs sera légalement reconnue.

Les jetons non fongibles suivent la propriété d’une chose unique. L’objet possédé peut être un objet numérique, tel qu’un objet de jeu ou un objet de collection numérique ; ou la chose peut être un objet physique dont la propriété est suivie par un jeton, comme une maison, une voiture ou une œuvre d’art. Les actes peuvent également représenter des choses de valeur négative, telles que des prêts (dettes), des privilèges, des servitudes, etc. La norme ERC721 n’impose aucune limitation ou attente sur la nature de la chose dont la propriété est suivie par un acte et exige seulement qu’elle puisse être identifié de manière unique, ce qui, dans le cas de cette norme, est obtenu par un identifiant de 256 bits .

Les détails de la norme et de la discussion sont suivis sur deux fils GitHub différents:

Pour saisir la différence fondamentale entre ERC20 et ERC721, il suffit de regarder la structure de données interne utilisée dans ERC721 :

// Mappage de l'ID de l'acte au propriétaire
mapping (uint256 => address) private deedOwner;

Alors que l’ERC20 suit les soldes appartenant à chaque propriétaire, le propriétaire étant la clé primaire du mappage, l’ERC721 suit chaque ID d’acte et son propriétaire, l’ID d’acte étant la clé primaire du mappage. De cette différence fondamentale découlent toutes les propriétés d’un jeton non fongible.

La spécification de l’interface de contrat ERC721 est :

interface ERC721 /* is ERC165 */ {
    event Transfer(address indexed _from, address indexed _to, uint256 _deedId);
    event Approval(address indexed _owner, address indexed _approved,
                   uint256 _deedId);
    event ApprovalForAll(address indexed _owner, address indexed _operator,
                         bool _approved);

    function balanceOf(address _owner) external view returns (uint256 _balance);
    function ownerOf(uint256 _deedId) external view returns (address _owner);
    function transfer(address _to, uint256 _deedId) external payable;
    function transferFrom(address _from, address _to, uint256 _deedId)
        external payable;
    function approve(address _approved, uint256 _deedId) external payable;
    function setApprovalForAll(address _operateor, boolean _approved) payable;
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

ERC721 prend également en charge deux interfaces optionnelles, une pour les métadonnées et une pour l’énumération des actes et des propriétaires.

L’interface optionnelle ERC721 pour les métadonnées est :

interface ERC721Metadata /* is ERC721 */ {
    function name() external pure returns (string _name);
    function symbol() external pure returns (string _symbol);
    function deedUri(uint256 _deedId) external view returns (string _deedUri);
}

L’interface optionnelle ERC721 pour l’énumération est :

interface ERC721Enumerable /* is ERC721 */ {
    function totalSupply() external view returns (uint256 _count);
    function deedByIndex(uint256 _index) external view returns (uint256 _deedId);
    function countOfOwners() external view returns (uint256 _count);
    function ownerByIndex(uint256 _index) external view returns (address _owner);
    function deedOfOwnerByIndex(address _owner, uint256 _index) external view
        returns (uint256 _deedId);
}

Utilisation des normes de jeton

Dans la section précédente, nous avons examiné plusieurs normes proposées et quelques normes largement déployées pour les contrats de jetons. A quoi servent exactement ces normes ? Devez-vous utiliser ces normes ? Comment devriez-vous les utiliser ? Devriez-vous ajouter des fonctionnalités au-delà de ces normes ? Quelles normes devez-vous utiliser ? Nous examinerons ensuite certaines de ces questions.

Que sont les normes de jeton ? Quel est leur but ?

Les normes de jetons sont les spécifications minimales pour une implémentation. Cela signifie que pour être conforme, par exemple, à ERC20, vous devez au minimum implémenter les fonctions et le comportement spécifiés par la norme ERC20. Vous êtes également libre d'ajouter à la fonctionnalité en implémentant des fonctions qui ne font pas partie de la norme.

L’objectif principal de ces normes est d’encourager l'interopérabilité entre les contrats. Ainsi, tous les portefeuilles, échanges, interfaces utilisateur et autres composants d’infrastructure peuvent s’interfacer de manière prévisible avec tout contrat qui suit la spécification. En d’autres termes, si vous déployez un contrat qui suit la norme ERC20, tous les utilisateurs de portefeuille existants peuvent commencer à échanger votre jeton de manière transparente sans aucune mise à niveau de portefeuille ni effort de votre part.

Les normes sont censées être descriptives plutôt que prescriptives. La façon dont vous choisissez de mettre en œuvre ces fonctions dépend de vous - le fonctionnement interne du contrat n’est pas pertinent pour la norme. Ils ont des exigences fonctionnelles, qui régissent le comportement dans des circonstances spécifiques, mais ils ne prescrivent pas de mise en œuvre. Un exemple de ceci est le comportement d’une fonction transfer si la valeur est définie sur zéro.

Devriez-vous utiliser ces normes ?

Compte tenu de tous ces standards, chaque développeur est confronté à un dilemme : utiliser les standards existants ou innover au-delà des contraintes qu’ils imposent ?

Ce dilemme n’est pas facile à résoudre. Les normes restreignent nécessairement votre capacité à innover, en créant une "ornière" étroite que vous devez suivre. D’autre part, les normes de base ont émergé de l’expérience de centaines d’applications et s’adaptent souvent bien à la grande majorité des cas d’utilisation.

Dans le cadre de cette considération, il y a un problème encore plus important : la valeur de l’interopérabilité et de l’adoption à grande échelle. Si vous choisissez d’utiliser une norme existante, vous gagnez la valeur de tous les systèmes conçus pour fonctionner avec cette norme. Si vous choisissez de vous écarter de la norme, vous devez prendre en compte le coût de la construction de toute l’infrastructure de support par vous-même ou persuader les autres de prendre en charge votre mise en œuvre en tant que nouvelle norme. La tendance à forger votre propre chemin et à ignorer les normes existantes est connue sous le nom de syndrome "Not Invented Here" (Non inventé ici) et est contraire à la culture open source. D’autre part, le progrès et l’innovation dépendent parfois de l’abandon de la tradition. C’est un choix délicat, alors réfléchissez bien !

Note

Selon Wikipedia, "Not Invented Here" est une position adoptée par les cultures sociales, d’entreprise ou institutionnelles qui évitent d’utiliser ou d’acheter des produits, des recherches, des normes ou des connaissances déjà existants en raison de leurs origines externes et de leurs coûts, tels que les redevances.

Sécurité par maturité

Au-delà du choix de la norme, il y a le choix parallèle de la mise en oeuvre. Lorsque vous décidez d’utiliser une norme telle que ERC20, vous devez ensuite décider comment mettre en œuvre une conception compatible. Il existe un certain nombre d’implémentations "de référence" existantes qui sont largement utilisées dans l’écosystème Ethereum, ou vous pouvez écrire la vôtre à partir de zéro. Encore une fois, ce choix représente un dilemme qui peut avoir de graves implications en matière de sécurité.

Les implémentations existantes sont "testées au combat". Bien qu’il soit impossible de prouver qu’ils sont sécurisés, beaucoup d’entre eux sous-tendent des millions de dollars de jetons. Ils ont été attaqués, à plusieurs reprises et vigoureusement. Jusqu’à présent, aucune vulnérabilité significative n’a été découverte. Rédiger le vôtre n’est pas facile - il existe de nombreuses façons subtiles de compromettre un contrat. Il est beaucoup plus sûr d’utiliser une implémentation largement testée et largement utilisée. Dans nos exemples, nous avons utilisé l’implémentation OpenZeppelin de la norme ERC20, car cette implémentation est entièrement axée sur la sécurité.

Si vous utilisez une implémentation existante, vous pouvez également l’étendre. Encore une fois, cependant, soyez prudent avec cette impulsion. La complexité est l’ennemie de la sécurité. Chaque ligne de code que vous ajoutez étend la surface d’attaque de votre contrat et pourrait représenter une vulnérabilité en attente. Vous ne remarquerez peut-être pas un problème jusqu’à ce que vous accordiez beaucoup de valeur au contrat et que quelqu’un le rompe.

Tip

Les normes et les choix de mise en œuvre sont des éléments importants de la conception globale des contrats intelligents sécurisés, mais ce ne sont pas les seules considérations. Voir Sécurité des contrats intelligents.

Extensions aux normes d’interface de jeton

Les normes de jetons abordées dans ce chapitre fournissent une interface très minimale, avec des fonctionnalités limitées. De nombreux projets ont créé des implémentations étendues pour prendre en charge les fonctionnalités dont ils ont besoin pour leurs applications. Certaines de ces fonctionnalités incluent :

Contrôle du propriétaire

La possibilité de donner des adresses spécifiques ou des ensembles d’adresses (c’est-à-dire des schémas multisignatures), des capacités spéciales, telles que la liste noire, la liste blanche, la frappe, la récupération, etc.

Brûlure

La capacité de détruire délibérément ("brûler") des jetons en les transférant à une adresse inutilisable ou en effaçant un solde et en réduisant l’approvisionnement.

Frappe

La possibilité d’ajouter à l’offre totale de jetons, à un rythme prévisible ou par "fiat" du créateur du jeton.

Financement participatif

La possibilité de proposer des jetons à la vente, par exemple par le biais d’une vente aux enchères, d’une vente sur le marché, d’une vente aux enchères inversée, etc.

Limites

La possibilité de fixer des limites prédéfinies et immuables sur l’offre totale (à l’opposé de la fonction "frappe").

Portes dérobées de récupération

Fonctions pour récupérer des fonds, annuler des transferts ou démanteler le jeton qui peuvent être activés par une adresse désignée ou un ensemble d’adresses.

Liste blanche

La possibilité de restreindre les actions (telles que les transferts de jetons) à des adresses spécifiques. Le plus souvent utilisé pour offrir des jetons à des "investisseurs accrédités" après vérification par les règles de différentes juridictions. Il existe généralement un mécanisme de mise à jour de la liste blanche.

Liste noire

La possibilité de restreindre les transferts de jetons en interdisant des adresses spécifiques. Il existe généralement une fonction de mise à jour de la liste noire.

Il existe des implémentations de référence pour bon nombre de ces fonctions, par exemple dans la bibliothèque OpenZeppelin. Certains d’entre eux sont spécifiques à un cas d’utilisation et ne sont implémentés que dans quelques jetons. Il n’existe pas, à l’heure actuelle, de normes largement acceptées pour les interfaces avec ces fonctions.

Comme indiqué précédemment, la décision d’étendre une norme de jeton avec des fonctionnalités supplémentaires représente un compromis entre innovation/risque et interopérabilité/sécurité.

Jetons et ICO

Les jetons ont connu un développement explosif dans l’écosystème Ethereum. Il est probable qu’ils deviendront un élément très important de toutes les plateformes de contrats intelligents comme Ethereum.

Néanmoins, l’importance et l’impact futur de ces normes ne doivent pas être confondus avec une approbation des offres de jetons actuelles. Comme dans toute technologie à un stade précoce, la première vague de produits et d’entreprises échouera presque toutes, et certaines échoueront de manière spectaculaire. La plupart des jetons proposés dans Ethereum aujourd’hui sont des escroqueries à peine déguisées, des systèmes pyramidaux et des prises d’argent.

L’astuce consiste à séparer la vision et l’impact à long terme de cette technologie, qui risque d’être énorme, de la bulle à court terme des ICO symboliques, qui sont en proie à la fraude. Les normes de jetons et la plate-forme survivront à la manie actuelle des jetons, puis ils changeront probablement le monde.

Conclusion

Les jetons sont un concept très puissant dans Ethereum et peuvent constituer la base de nombreuses applications décentralisées importantes. Dans ce chapitre, nous avons examiné les différents types de jetons et les normes de jeton, et vous avez construit votre premier jeton et l’application associée. Nous reviendrons à nouveau sur les jetons dans Applications décentralisées (DApps), où vous utiliserez un jeton non fongible comme base pour un DApp d’enchères.

Oracles

Bannière Amazon du livre Maîtriser Ethereum

Dans ce chapitre, nous discutons des oracles, qui sont des systèmes qui peuvent fournir des sources de données externes aux contrats intelligents Ethereum. Le terme « oracle » vient de la mythologie grecque, où il faisait référence à une personne en communication avec les dieux qui pouvait avoir des visions du futur. Dans le contexte des chaînes de blocs, un oracle est un système qui peut répondre à des questions externes à Ethereum. Idéalement, les oracles sont des systèmes sans confiance, ce qui signifie qu’ils n’ont pas besoin d’être fiables car ils fonctionnent selon des principes décentralisés.

Pourquoi les oracles sont nécessaires

Un composant clé de la plate-forme Ethereum est la machine virtuelle Ethereum, avec sa capacité à exécuter des programmes et à mettre à jour l’état d’Ethereum, contraint par des règles de consensus, sur n’importe quel nœud du réseau décentralisé. Afin de maintenir le consensus, l’exécution d’EVM doit être totalement déterministe et basée uniquement sur le contexte partagé de l’état Ethereum et des transactions signées. Cela a deux conséquences particulièrement importantes : la première est qu’il ne peut y avoir de source intrinsèque d’aléatoire avec laquelle l’EVM et les contrats intelligents peuvent fonctionner ; la seconde est que les données extrinsèques ne peuvent être introduites que comme données utiles d’une transaction.

Déballons ces deux conséquences plus loin. Pour comprendre l’interdiction d’une véritable fonction aléatoire dans l’EVM pour fournir un caractère aléatoire aux contrats intelligents, considérez l’effet sur les tentatives de parvenir à un consensus après l’exécution d’une telle fonction : le nœud A exécuterait la commande et stockerait 3 au nom du contrat intelligent dans son stockage, tandis que le nœud B, exécutant le même contrat intelligent, stockerait 7 à la place. Ainsi, les nœuds A et B arriveraient à des conclusions différentes sur ce que devrait être l’état résultant, bien qu’ils aient exécuté exactement le même code dans le même contexte. En effet, il se pourrait qu’un état résultant différent soit atteint à chaque fois que le contrat intelligent est évalué. En tant que tel, il n’y aurait aucun moyen pour le réseau, avec sa multitude de nœuds fonctionnant indépendamment dans le monde, de parvenir à un consensus décentralisé sur ce que devrait être l’état résultant. En pratique, cela deviendrait bien pire que cet exemple très rapidement, car les effets d’entraînement, y compris les transferts d’ether, s’accumuleraient de façon exponentielle.

Notez que les fonctions pseudo-aléatoires, telles que les fonctions de hachage cryptographiquement sécurisées (qui sont déterministes et peuvent donc faire, et font en effet, partie de l’EVM), ne suffisent pas pour de nombreuses applications. Prenez un jeu de hasard qui simule des lancers de pièces pour résoudre les paiements de pari, qui doivent randomiser pile ou face – un mineur peut obtenir un avantage en jouant au jeu et en n’incluant ses transactions que dans les blocs pour lesquels il gagnera. Alors comment contourner ce problème ? Eh bien, tous les nœuds peuvent s’entendre sur le contenu des transactions signées, de sorte que les informations extrinsèques, y compris les sources aléatoires, les informations sur les prix, les prévisions météorologiques, etc., peuvent être introduites en tant que partie des données des transactions envoyées au réseau. Cependant, ces données ne sont tout simplement pas fiables, car elles proviennent de sources invérifiables. En tant que tel, nous venons de reporter le problème. Nous utilisons des oracles pour tenter de résoudre ces problèmes, dont nous parlerons en détail dans la suite de ce chapitre.

Cas d’utilisation Oracle et exemples

Les oracles, idéalement, fournissent un moyen sans confiance (ou du moins presque sans confiance) d’obtenir des informations extrinsèques (c’est-à-dire "du monde réel" ou hors chaîne), tels que les résultats des matchs de football, le prix de l’or ou des nombres vraiment aléatoires, sur la plate-forme Ethereum pour les contrats intelligents à utiliser. Ils peuvent également être utilisés pour relayer directement les données en toute sécurité vers les interfaces DApp. Les oracles peuvent donc être considérés comme un mécanisme permettant de combler le fossé entre le monde hors chaîne et les contrats intelligents. Permettre aux contrats intelligents d’appliquer des relations contractuelles basées sur des événements et des données du monde réel élargit considérablement leur portée. Cependant, cela peut également introduire des risques externes pour le modèle de sécurité d’Ethereum. Considérez un contrat « testamentaire intelligent » qui distribue les actifs lorsqu’une personne décède. C’est quelque chose de fréquemment discuté dans l’espace des contrats intelligents, et met en évidence les risques d’un oracle de confiance. Si le montant de l’héritage contrôlé par un tel contrat est suffisamment élevé, l’incitation à pirater l’oracle et à déclencher la distribution des actifs avant le décès du propriétaire est très élevée.

Notez que certains oracles fournissent des données propres à une source de données privée spécifique, telles que des certificats universitaires ou des identifiants gouvernementaux. La source de ces données, comme une université ou un département gouvernemental, est entièrement fiable et la vérité des données est subjective (la vérité n’est déterminée qu’en faisant appel à l’autorité de la source). De telles données ne peuvent donc pas être fournies sans confiance, c’est-à-dire sans faire confiance à une source, car il n’y a pas de vérité objective vérifiable de manière indépendante. En tant que tels, nous incluons ces sources de données dans notre définition de ce qui compte comme des "oracles", car elles fournissent également un pont de données pour les contrats intelligents. Les données qu’ils fournissent prennent généralement la forme d’attestations, telles que des passeports ou des certificats de réussite. Les attestations deviendront une grande partie du succès des plates-formes de chaîne de blocs à l’avenir, en particulier en ce qui concerne les problèmes connexes de vérification de l’identité ou de la réputation, il est donc important d’explorer comment elles peuvent être servies par les plates-formes de chaîne de blocs.

Voici d’autres exemples de données pouvant être fournies par les oracles :

  • Nombres aléatoires/entropie provenant de sources physiques telles que les processus quantiques/thermiques : par exemple, pour sélectionner équitablement un gagnant dans un contrat intelligent de loterie

  • Déclencheurs paramétriques indexés sur les aléas naturels : par exemple, déclenchement de contrats intelligents d’obligation catastrophe, tels que les mesures à l’échelle de Richter pour une obligation tremblement de terre

  • Données de taux de change : par exemple, pour un rattachement précis des cryptomonnaies à la monnaie fiduciaire

  • Données sur les marchés des capitaux : par exemple, prix des paniers d’actifs/titres symboliques

  • Données de référence de tests de performance : par exemple, intégration des taux d’intérêt dans les dérivés financiers intelligents

  • Données statiques/pseudostatiques : identifiants de sécurité, codes pays, codes devises, etc.

  • Données de temps et d’intervalle : pour les déclencheurs d’événements fondés sur des mesures de temps précises

  • Données météorologiques : par exemple, calculs de primes d’assurance basés sur les prévisions météorologiques

  • Événements politiques : pour la résolution du marché de prédiction

  • Événements sportifs : pour la résolution du marché de la prédiction et les contrats de sports fantastiques

  • Données de géolocalisation : par exemple, telles qu’elles sont utilisées dans le suivi de la chaîne d’approvisionnement

  • Vérification des dommages : pour les contrats d’assurance

  • Événements survenant sur d’autres chaînes de blocs : fonctions d’interopérabilité

  • Prix du marché de l’ether : par exemple, oracles pour le prix du gaz en fiat

  • Statistiques de vol : par exemple, telles qu’elles sont utilisées par les groupes et les clubs pour la mise en commun des billets d’avion

Dans les sections suivantes, nous examinerons certaines des façons dont les oracles peuvent être implémentés, y compris les modèles d’oracle de base, les oracles de calcul, les oracles décentralisés et les implémentations de client oracle dans Solidity.

Modèles de conception Oracle

Tous les oracles fournissent quelques fonctions clés, par définition. Ceux-ci incluent la capacité de :

  • Recueillir des données à partir d’une source hors chaîne.

  • Transférez les données en chaîne avec un message signé.

  • Rendre les données disponibles en les plaçant dans le stockage d’un contrat intelligent.

Une fois que les données sont disponibles dans le stockage d’un contrat intelligent, elles peuvent être consultées par d’autres contrats intelligents via des appels de message qui invoquent une fonction "retrieve" (récupérer) du contrat intelligent de l’oracle ; il est également accessible directement aux nœuds Ethereum ou aux clients activés par le réseau en "recherchant" le stockage de l’oracle.

Les trois principales façons de configurer un oracle peuvent être classées comme request–response (requête-réponse), publish-subscribe (publication-abonnement) et immediate-read (immédiat-lecture).

En commençant par le plus simple, les oracles à lecture immédiate sont ceux qui fournissent des données qui ne sont nécessaires que pour une décision immédiate, comme "Quelle est l’adresse de ethereumbook.info?" ou "Cette personne a-t-elle plus de 18 ans ?" Ceux qui souhaitent interroger ce type de données ont tendance à le faire sur une base « juste à temps » ; la recherche est effectuée lorsque l’information est nécessaire et peut-être plus jamais après. Des exemples de tels oracles incluent ceux qui contiennent des données sur ou émises par des organisations, telles que des certificats académiques, des codes de numérotation, des adhésions institutionnelles, des identifiants d’aéroport, des identifiants auto-souverains, etc. Ce type d’oracle stocke les données une fois dans son stockage contractuel, d’où tout un autre contrat intelligent peut le rechercher à l’aide d’un appel de demande au contrat oracle. Il peut être mis à jour. Les données dans le stockage de l’oracle sont également disponibles pour une recherche directe par des applications compatibles avec la chaîne de blocs (c’est-à-dire, Ethereum connecté au client) sans avoir à passer par la palabre et à encourir les coûts de gaz liés à l’émission d’une transaction. Un magasin voulant vérifier l’âge d’un client souhaitant acheter de l’alcool pourrait utiliser un oracle de cette manière. Ce type d’oracle est attrayant pour une organisation ou une entreprise qui pourrait autrement avoir à exécuter et à entretenir des serveurs pour répondre à de telles demandes de données. Notez que les données stockées par l’oracle ne sont probablement pas les données brutes que l’oracle sert, par exemple, pour des raisons d’efficacité ou de confidentialité. Une université peut mettre en place un oracle pour les certificats de réussite scolaire des anciens étudiants. Cependant, stocker tous les détails des certificats (qui pourraient s’étendre sur des pages de cours suivis et de notes obtenues) serait excessif. Au lieu de cela, un hachage du certificat est suffisant. De même, un gouvernement pourrait souhaiter mettre les identifiants des citoyens sur la plate-forme Ethereum, où il est clair que les détails inclus doivent rester confidentiels. Encore une fois, hacher les données (plus soigneusement, dans les arbres Merkle avec des sels) et ne stocker que le hachage racine dans le stockage du contrat intelligent serait un moyen efficace d’organiser un tel service.

La configuration suivante est publication-abonnement (publish–subscribe), où un oracle qui fournit effectivement un service de diffusion pour les données qui devraient changer de valeurs (peut-être à la fois régulièrement et fréquemment) est soit interrogé par un contrat intelligent sur la chaîne, soit surveillé par un processus hors chaîne pour les mises à jour. Cette catégorie a un modèle similaire aux flux RSS, WebSub, etc., où l’oracle est mis à jour avec de nouvelles informations et un indicateur signale que de nouvelles données sont disponibles pour ceux qui se considèrent comme "abonnés". Les parties intéressées doivent soit interroger l’oracle pour vérifier si les dernières informations ont changé, soit écouter les mises à jour des contrats oracle et agir lorsqu’elles se produisent. Les exemples incluent les flux de prix, les informations météorologiques, les statistiques économiques ou sociales, les données de trafic, etc. Le sondage est très inefficace dans le monde des serveurs Web, mais pas dans le contexte pair à pair des plateformes en chaîne de blocs : les clients Ethereum doivent suivre avec tous les changements d’état, y compris les changements de stockage de contrat, de sorte que l’interrogation des changements de données est un appel local à un client synchronisé. Les journaux d’événements Ethereum permettent aux applications de rechercher particulièrement, et ce facilement, les mises à jour d’un oracle, et donc ce modèle peut même être considéré à certains égards comme un service "push" (d’envoi). Cependant, si l’interrogation est effectuée à partir d’un contrat intelligent, ce qui peut être nécessaire pour certaines applications décentralisées (par exemple, lorsque les incitations à l’activation ne sont pas possibles), des dépenses importantes en gaz peuvent être engagées.

La catégorie request–response (requête-réponse) est la plus compliquée : c’est lorsque la quantité de données est trop grande pour être stocké dans un contrat intelligent et les utilisateurs ne devraient avoir besoin que d’une petite partie de l’ensemble de données globale à la fois. C’est également un modèle applicable pour les entreprises de fournisseurs de données. Concrètement, un tel oracle pourrait être mis en œuvre comme un système de contrats intelligents en chaîne et une infrastructure hors chaîne utilisée pour surveiller les demandes et récupérer et renvoyer des données. Une demande de données à partir d’une application décentralisée serait généralement un processus asynchrone impliquant un certain nombre d’étapes. Dans ce modèle, premièrement, un EOA traite avec une application décentralisée, ce qui entraîne une interaction avec une fonction définie dans le contrat intelligent de l’oracle. Cette fonction initie la demande à l’oracle, avec les arguments associés détaillant les données demandées en plus d’informations supplémentaires pouvant inclure des fonctions de rappel et des paramètres de planification. Une fois cette transaction validée, la requête oracle peut être observée comme un événement EVM émis par le contrat oracle, ou comme un changement d’état ; les arguments peuvent être récupérés et utilisés pour effectuer la requête réelle de la source de données hors chaîne. L’oracle peut également exiger un paiement pour le traitement de la demande, c’est à dire le paiement du gaz pour le rappel et les autorisations d’accès aux données demandées. Enfin, les données résultantes sont signées par le propriétaire de l’oracle, attestant de la validité des données à un moment donné, et livrées dans une transaction à l’application décentralisée qui a fait la demande, soit directement, soit via le contrat de l’oracle. En fonction des paramètres de planification, l’oracle peut diffuser d’autres transactions mettant à jour les données à intervalles régulières (par exemple, des informations de tarification en fin de journée).

Les étapes d’un oracle de requête-réponse peuvent être résumées comme suit :

  1. Recevez une requête d’un DApp.

  2. Analysez la requête.

  3. Vérifiez que les autorisations de paiement et d’accès aux données sont fournies.

  4. Récupérez les données pertinentes d’une source hors chaîne (et cryptez-les si nécessaire).

  5. Signez la ou les transactions avec les données incluses.

  6. Diffusez la ou les transactions sur le réseau.

  7. Planifiez toutes les autres transactions nécessaires, telles que les notifications, etc.

Une gamme d’autres régimes sont également possibles; par exemple, les données peuvent être demandées et renvoyées directement par un EOA, supprimant ainsi le besoin d’un contrat intelligent d’oracle. De même, la demande et la réponse pourraient être faites vers et depuis un capteur matériel compatible avec l’Internet des objets. Par conséquent, les oracles peuvent être humains, logiciels ou matériels.

Le modèle requête-réponse décrit ici est couramment observé dans les architectures client-serveur. Bien qu’il s’agisse d’un modèle de messagerie utile qui permet aux applications d’avoir une conversation bidirectionnelle, il est peut-être inapproprié dans certaines conditions. Par exemple, une obligation intelligente nécessitant un taux d’intérêt auprès d’un oracle pourrait devoir demander les données quotidiennement selon un modèle demande-réponse afin de s’assurer que le taux est toujours correct. Étant donné que les taux d’intérêt changent rarement, un modèle de publication-abonnement peut être plus approprié ici, en particulier si l’on tient compte de la bande passante limitée d’Ethereum.

La publication-abonnement est un modèle dans lequel les éditeurs (dans ce contexte, les oracles) n’envoient pas de messages directement aux destinataires, mais classent plutôt les messages publiés dans des classes distinctes. Les abonnés peuvent exprimer leur intérêt pour une ou plusieurs classes et récupérer uniquement les messages qui les intéressent. Dans un tel modèle, un oracle pourrait écrire le taux d’intérêt dans sa propre mémoire interne à chaque fois qu’il change. Plusieurs DApps abonnés peuvent simplement le lire à partir du contrat oracle, réduisant ainsi l’impact sur la bande passante du réseau tout en minimisant les coûts de stockage.

Dans un modèle de diffusion ou de multidiffusion, un oracle publierait tous les messages sur un canal et les contrats d’abonnement écouteraient le canal sous une variété de modes d’abonnement. Par exemple, un oracle peut publier des messages sur un canal de taux de change de cryptomonnaie. Un contrat intelligent d’abonnement pourrait demander le contenu complet de la chaîne s’il avait besoin de la série chronologique pour, par exemple, un calcul de moyenne mobile ; un autre peut n’exiger que le dernier taux pour un calcul du prix au comptant. Un modèle de diffusion est approprié lorsque l’oracle n’a pas besoin de connaître l’identité du contrat d’abonnement.

Authentification des données

Si nous supposons que la source de les données interrogées par une DApp font à la fois autorité et sont dignes de confiance (une hypothèse non négligeable), une question reste en suspens : étant donné que l’oracle et le mécanisme de requête-réponse peuvent être exploités par des entités distinctes, comment pouvons-nous faire confiance à ce mécanisme ? Il existe une réelle possibilité que les données soient altérées en transit, il est donc essentiel que les méthodes hors chaîne soient en mesure d’attester de l’intégrité des données renvoyées. Deux approches courantes de l’authentification des données sont les preuves d’authenticité et les environnements d’exécution de confiance (TEE).

Les preuves d’authenticité sont des garanties cryptographiques que les données n’ont pas été falsifiées. Basées sur une variété de techniques d’attestation (par exemple, des preuves signées numériquement), elles transfèrent efficacement la confiance du support de données vers l’attestateur (c’est-à-dire le fournisseur de l’attestation). En vérifiant la preuve d’authenticité en chaîne, les contrats intelligents sont en mesure de vérifier l’intégrité des données avant de les exploiter. Oraclize est un exemple de service oracle exploitant une variété de preuves d’authenticité. Une de ces preuves qui est actuellement disponible pour les requêtes de données à partir du réseau principal Ethereum est la preuve TLSNotary. Les preuves TLSNotary permettent à un client de fournir la preuve à un tiers que le trafic Web HTTPS s’est produit entre le client et un serveur. Bien que HTTPS soit lui-même sécurisé, il ne prend pas en charge la signature des données. Par conséquent, les preuves TLSNotary reposent sur les signatures TLSNotary (via PageSigner). Les preuves TLSNotary s’appuient sur le protocole Transport Layer Security (TLS), permettant à la clé principale TLS, qui signe les données après leur accès, d’être répartie entre trois parties : le serveur (l’oracle), un audité (Oraclize) et un Auditeur. Oraclize utilise une instance de machine virtuelle Amazon Web Services (AWS) comme auditeur, qui peut être vérifiée comme n’ayant pas été modifiée depuis l’instanciation. Cette instance AWS stocke le secret TLSNotary, lui permettant de fournir des preuves d’honnêteté. Bien qu’elle offre des garanties plus élevées contre la falsification des données qu’un simple mécanisme de requête-réponse, cette approche nécessite l’hypothèse qu’Amazon lui-même ne falsifiera pas l’instance de VM.

Town Crier est un système oracle de flux de données authentifié basé sur l’approche TEE ; ces méthodes utilisent des enclaves sécurisées basées sur le matériel pour garantir l’intégrité des données. Town Crier utilise Intel Software Guard eXtensions (SGX) pour s’assurer que les réponses des requêtes HTTPS peuvent être vérifiées comme authentiques . SGX offre des garanties d’intégrité, garantissant que les applications s’exécutant dans une enclave sont protégées par le CPU contre toute altération par tout autre processus. Il assure également la confidentialité, garantissant que l’état d’une application est opaque pour les autres processus lors de son exécution dans l’enclave. Et enfin, SGX permet l’attestation, en générant une preuve signée numériquement qu’une application, identifiée de manière sécurisée par un hachage de sa version, s’exécute réellement dans une enclave. En vérifiant cette signature numérique, il est possible pour une application décentralisée de prouver qu’une instance Town Crier fonctionne en toute sécurité dans une enclave SGX. Ceci, à son tour, prouve que l’instance n’a pas été falsifiée et que les données émises par Town Crier sont donc authentiques. La propriété de confidentialité permet en outre à Town Crier de gérer des données privées en autorisant le chiffrement des requêtes de données à l’aide de la clé publique de l’instance Town Crier. L’utilisation du mécanisme de requête-réponse d’un oracle dans une enclave telle que SGX nous permet de penser qu’il s’exécute en toute sécurité sur du matériel tiers de confiance, garantissant que les données demandées sont renvoyées sans altération (en supposant que nous faisons confiance à Intel/SGX).

Oracles de calcul

Jusqu’à présent, nous n’avons discuté des oracles que dans le contexte de la demande et de la livraison de données. Cependant, les oracles peuvent également être utilisés pour effectuer des calculs arbitraires, une fonction qui peut être particulièrement utile compte tenu de la limite de gaz de bloc inhérente à Ethereum et des coûts de calcul relativement élevés. Plutôt que de simplement relayer les résultats d’une requête, les oracles de calcul peuvent être utilisés pour effectuer des calculs sur un ensemble d’entrées et renvoyer un résultat calculé qu’il aurait peut-être été impossible de calculer en chaîne. Par exemple, on peut utiliser un oracle pour effectuer un calcul de régression intensif afin d’estimer le rendement d’un contrat obligataire.

Si vous êtes prêt à faire confiance à un service centralisé mais auditable, vous pouvez revenir à Oraclize. Ils fournissent un service qui permet aux applications décentralisées de demander la sortie d’un calcul effectué dans une machine virtuelle AWS en bac à sable. L’instance AWS crée un conteneur exécutable à partir d’un fichier Dockerfile configuré par l’utilisateur, compressé dans une archive qui est téléchargée sur le système de fichiers décentralisé (IPFS ; voir Stockage de données). Sur demande, Oraclize récupère cette archive à l’aide de son hachage, puis initialise et exécute le conteneur Docker sur AWS, en transmettant tous les arguments fournis à l’application en tant que variables d’environnement. L’application conteneurisée effectue le calcul, soumis à une contrainte de temps, et écrit le résultat sur la sortie standard, où il peut être récupéré par Oraclize et renvoyé à l’application décentralisée. Oraclize propose actuellement ce service sur une instance AWS t2.micro auditable, donc si le calcul a une valeur non triviale, il est possible de vérifier que le bon conteneur Docker a été exécuté. Néanmoins, ce n’est pas une solution véritablement décentralisée.

Le concept de "cryptlet" en tant que norme pour les vérités d’oracle vérifiables a été formalisé dans le cadre plus large de l’ESC Framework de Microsoft. Les cryptlets s’exécutent dans une capsule chiffrée qui fait abstraction de l’infrastructure, telle que les Entrées/Sorties, et auquel le CryptoDelegate est attaché afin que les messages entrants et sortants soient signés, validés et prouvés automatiquement. Les cryptlets prennent en charge les transactions distribuées afin que la logique de contrat puisse prendre en charge des transactions complexes à plusieurs étapes, à plusieurs chaînes de blocs et de systèmes externes de manière ACID. Cela permet aux développeurs de créer des résolutions portables, isolées et privées de la vérité à utiliser dans les contrats intelligents. Les cryptlets suivent le format indiqué ici :

public class SampleContractCryptlet : Cryptlet
  {
        public SampleContractCryptlet(Guid id, Guid bindingId, string name,
            string address, IContainerServices hostContainer, bool contract)
            : base(id, bindingId, name, address, hostContainer, contract)
        {
            MessageApi = new CryptletMessageApi(GetType().FullName,
                new SampleContractConstructor())

Pour une solution plus décentralisée, nous pouvons nous tourner vers TrueBit, qui offre une solution de calcul hors chaîne évolutive et vérifiable. Ils utilisent un système de solveurs et de vérificateurs qui sont incités à effectuer des calculs et à vérifier ces calculs, respectivement. Si une solution est contestée, un processus de vérification itératif sur des sous-ensembles du calcul est effectué en chaîne, une sorte de « jeu de vérification ». Le jeu se déroule à travers une série de tours, chacun vérifiant de manière récursive un sous-ensemble de plus en plus petit du calcul. Le jeu atteint finalement un tour final, où le défi est suffisamment trivial pour que les juges - les mineurs d’Ethereum - puissent rendre une décision finale sur la question de savoir si le défi a été relevé, en chaîne. En effet, TrueBit est une implémentation d’un marché de calcul, permettant aux applications décentralisées de payer pour un calcul vérifiable à effectuer en dehors du réseau, mais s’appuyant sur Ethereum pour faire respecter les règles du jeu de vérification. En théorie, cela permet aux contrats intelligents sans confiance d’effectuer en toute sécurité n’importe quelle tâche de calcul.

Un large éventail d’applications existe pour des systèmes comme TrueBit, allant de l’apprentissage automatique à la vérification de la preuve de travail. Un exemple de ce dernier est le pont Doge-Ethereum, qui utilise TrueBit pour vérifier la preuve de travail de Dogecoin (Scrypt), qui est une fonction gourmande en mémoire et en calcul qui ne peut pas être calculée dans la limite de gaz du bloc Ethereum. En effectuant cette vérification sur TrueBit, il a été possible de vérifier en toute sécurité les transactions Dogecoin dans un contrat intelligent sur le testnet Rinkeby d’Ethereum.

Oracles décentralisés

Alors que les données centralisées ou les oracles de calcul suffisent pour de nombreuses applications, ils représentent des points de défaillance uniques dans le réseau Ethereum. Un certain nombre de schémas ont été proposés autour de l’idée d’oracles décentralisés comme moyen d’assurer la disponibilité des données et la création d’un réseau de fournisseurs de données individuels avec un système d’agrégation de données en chaîne.

ChainLink a proposé un réseau oracle décentralisé composé de trois contrats intelligents clés : un contrat de réputation, un contrat de correspondance des commandes et un contrat d’agrégation — et un registre hors chaîne des fournisseurs de données. Le contrat de réputation est utilisé pour suivre les performances des fournisseurs de données. Les scores du contrat de réputation sont utilisés pour remplir le registre hors chaîne. Le contrat d’appariement des commandes sélectionne les offres des oracles à l’aide du contrat de réputation. Il finalise ensuite un accord de niveau de service, qui inclut les paramètres de requête et le nombre d’oracles requis. Cela signifie que l’acheteur n’a pas besoin de traiter directement avec les oracles individuels. Le contrat d’agrégation collecte les réponses (soumises à l’aide d’un schéma de validation-révélation) de plusieurs oracles, calcule le résultat collectif final de la requête et réinjecte finalement les résultats dans le contrat de réputation.

L’un des principaux défis d’une telle approche décentralisée est la formulation de la fonction d’agrégation. ChainLink propose de calculer une réponse pondérée, permettant de rapporter un score de validité pour chaque réponse oracle. La détection d’un score «invalide» ici n’est pas triviale, car elle repose sur la prémisse que les points de données aberrants, mesurés par les écarts par rapport aux réponses fournies par les pairs, sont incorrects. Le calcul d’un score de validité basé sur l’emplacement d’une réponse oracle parmi une distribution de réponses risque de pénaliser les réponses correctes par rapport aux réponses moyennes. Par conséquent, ChainLink propose un ensemble standard de contrats d’agrégation, mais permet également de spécifier des contrats d’agrégation personnalisés.

Une idée connexe est le protocole SchellingCoin. Ici, plusieurs participants rapportent des valeurs et la médiane est considérée comme la « bonne » réponse. Les déclarants sont tenus de fournir un acompte qui est redistribué en faveur de valeurs plus proches de la médiane, incitant ainsi à déclarer des valeurs similaires aux autres. Une valeur commune, également connue sous le nom de point de Schelling, que les répondants pourraient considérer comme la cible naturelle et évidente autour de laquelle se coordonner devrait être proche de la valeur réelle.

Jason Teutsch de TrueBit a récemment proposé une nouvelle conception pour un oracle décentralisé de disponibilité des données hors chaîne. Cette conception s’appuie sur une chaîne de blocs de preuve de travail dédiée qui est capable de signaler correctement si les données enregistrées sont disponibles ou non à une époque donnée. Les mineurs tentent de télécharger, stocker et propager toutes les données actuellement enregistrées, garantissant ainsi que les données sont disponibles localement. Bien qu’un tel système soit coûteux dans le sens où chaque nœud de minage stocke et propage toutes les données enregistrées, le système permet de réutiliser le stockage en libérant les données après la fin de la période d’enregistrement.

Interfaces clients Oracle en Solidity

Utilisation d’Oraclize pour mettre à jour le taux de change ETH/USD à partir d’une source externe est un exemple Solidity démontrant comment Oraclize peut être utilisé pour interroger en continu le prix ETH/USD à partir d’une API et stocker le résultat de manière utilisable.

Example 22. Utilisation d’Oraclize pour mettre à jour le taux de change ETH/USD à partir d’une source externe
/*
Défileur de prix ETH/USD tirant parti de l'API CryptoCompare

Ce contrat garde en mémoire un prix ETH/USD mis à jour,
qui est mis à jour toutes les 10 minutes.
*/

pragma solidity ^0.4.1;
import "github.com/oraclize/ethereum-api/oraclizeAPI.sol";

/*
   "oraclize_" prepended methods indicate inheritance from "usingOraclize"
*/
contract EthUsdPriceTicker is usingOraclize {

    uint public ethUsd;

    event newOraclizeQuery(string description);
    event newCallbackResult(string result);

    function EthUsdPriceTicker() payable {
        // signale la génération et le stockage de la preuve TLSN sur IPFS
        oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS);

        // demande de requête
        queryTicker();
    }

    function __callback(bytes32 _queryId, string _result, bytes _proof) public {
        if (msg.sender != oraclize_cbAddress()) throw;
        newCallbackResult(_result);

        /*
        * Analyse la chaîne de résultat en un entier non signé pour une utilisation en chaîne.
        * Utilise l'assistant "parseInt" hérité de "usingOraclize", permettant
        * un résultat de chaîne tel que "123.45" à convertir en uint 12345.
        */
        ethUsd = parseInt(_result, 2);

        // appelé depuis le rappel puisque nous interrogeons le prix
        queryTicker();
    }

    function queryTicker() external payable {
        if (oraclize_getPrice("URL") > this.balance) {
            newOraclizeQuery("Oraclize query was NOT sent, please add some ETH
                to cover for the query fee");
        } else {
            newOraclizeQuery("Oraclize query was sent, standing by for the
                answer...");

            // les paramètres de la requête sont (délai en secondes, type de source de données,
            // argument de source de données)
            // spécifie JSONPath, pour récupérer une partie spécifique du résultat de l'API JSON
            oraclize_query(60 * 10, "URL",
                "json(https://min-api.cryptocompare.com/data/price?\
                fsym=ETH&tsyms=USD,EUR,GBP).USD");
        }
    }
}

Pour s’intégrer à Oraclize, le contrat EthUsdPriceTicker doit être un enfant de usingOraclize ; le contrat usingOraclize est défini dans le fichier oraclizeAPI. La demande de données est effectuée à l’aide de la fonction oraclize_query, qui est héritée du contrat usingOraclize. Il s’agit d’une fonction surchargée qui attend au moins deux arguments :

  • La source de données prise en charge à utiliser, telle que URL, WolframAlpha, IPFS ou calculation

  • L’argument de la source de données donnée, qui peut inclure l’utilisation d’assistants d’analyse JSON ou XML

La requête de prix est effectuée dans la fonction queryTicker. Afin d’effectuer la requête, Oraclize exige le paiement d’une somme modique en ether, couvrant le coût du gaz pour traiter le résultat et le transmettre à la fonction __callback et un supplément d’accompagnement pour le service. Ce montant dépend de la source de données et, le cas échéant, du type de preuve d’authenticité requis. Une fois les données récupérées, la fonction __callback est appelée par un compte contrôlé par Oraclize autorisé à effectuer le rappel ; il transmet la valeur de réponse et un argument queryId unique, qui, par exemple, peut être utilisé pour gérer et suivre plusieurs rappels en attente d’Oraclize.

Le fournisseur de données financières Thomson Reuters fournit également un service oracle pour Ethereum, appelé BlockOne IQ, permettant de demander des données de marché et de référence par des contrats intelligents exécutés sur des réseaux privés ou autorisés. Contrat faisant appel au service BlockOne IQ pour les données de marché montre l’interface pour l’oracle, et un contrat client qui fera la demande.

Example 23. Contrat faisant appel au service BlockOne IQ pour les données de marché
pragma solidity ^0.4.11;

contract Oracle {
    uint256 public divisor;
    function initRequest(
       uint256 queryType, function(uint256) external onSuccess,
       function(uint256
    ) external onFailure) public returns (uint256 id);
    function addArgumentToRequestUint(uint256 id, bytes32 name, uint256 arg) public;
    function addArgumentToRequestString(uint256 id, bytes32 name, bytes32 arg)
        public;
    function executeRequest(uint256 id) public;
    function getResponseUint(uint256 id, bytes32 name) public constant
        returns(uint256);
    function getResponseString(uint256 id, bytes32 name) public constant
        returns(bytes32);
    function getResponseError(uint256 id) public constant returns(bytes32);
    function deleteResponse(uint256 id) public constant;
}

contract OracleB1IQClient {

    Oracle private oracle;
    event LogError(bytes32 description);

    function OracleB1IQClient(address addr) external payable {
        oracle = Oracle(addr);
        getIntraday("IBM", now);
    }

    function getIntraday(bytes32 ric, uint256 timestamp) public {
        uint256 id = oracle.initRequest(0, this.handleSuccess, this.handleFailure);
        oracle.addArgumentToRequestString(id, "symbol", ric);
        oracle.addArgumentToRequestUint(id, "timestamp", timestamp);
        oracle.executeRequest(id);
    }

    function handleSuccess(uint256 id) public {
        assert(msg.sender == address(oracle));
        bytes32 ric = oracle.getResponseString(id, "symbol");
        uint256 open = oracle.getResponseUint(id, "open");
        uint256 high = oracle.getResponseUint(id, "high");
        uint256 low = oracle.getResponseUint(id, "low");
        uint256 close = oracle.getResponseUint(id, "close");
        uint256 bid = oracle.getResponseUint(id, "bid");
        uint256 ask = oracle.getResponseUint(id, "ask");
        uint256 timestamp = oracle.getResponseUint(id, "timestamp");
        oracle.deleteResponse(id);
        // Faire quelque chose avec les données de prix
    }

    function handleFailure(uint256 id) public {
        assert(msg.sender == address(oracle));
        bytes32 error = oracle.getResponseError(id);
        oracle.deleteResponse(id);
        emit LogError(error);
    }

}

La demande de données est initiée à l’aide de la fonction initRequest, qui permet de spécifier le type de requête (dans cet exemple, une demande de prix infrajournalier), en plus de deux fonctions de rappel. Cela renvoie un identifiant uint256 qui peut ensuite être utilisé pour fournir des arguments supplémentaires. La fonction addArgumentToRequestString permet de spécifier le Reuters Instrument Code (RIC), ici pour le stock IBM, et addArgumentToRequestUint permet de spécifier l’horodatage. Maintenant, passer un alias pour block.timestamp récupérera le prix actuel pour IBM. La requête est alors exécutée par la fonction executeRequest. Une fois la requête traitée, le contrat oracle appellera la fonction callback onSuccess avec l’identifiant de la requête, permettant de récupérer les données résultantes ; en cas d’échec de la récupération, le rappel onFailure renverra un code d’erreur à la place. Les champs disponibles qui peuvent être récupérés en cas de succès incluent les prix open, high, low, close (OHLC) et bid/ask.

Conclusion

Comme vous pouvez le constater, les oracles fournissent un service crucial aux contrats intelligents : ils apportent des faits externes à l’exécution du contrat. Avec cela, bien sûr, les oracles présentent également un risque important - s’ils sont des sources fiables et peuvent être compromis, ils peuvent entraîner une exécution compromise des contrats intelligents qu’ils alimentent.

Généralement, lorsque vous envisagez l’utilisation d’un oracle, faites très attention au modèle de confiance. Si vous supposez que l’oracle est digne de confiance, vous risquez de compromettre la sécurité de votre contrat intelligent en l’exposant à des entrées potentiellement fausses. Cela dit, les oracles peuvent être très utiles si les hypothèses de sécurité sont soigneusement prises en compte.

Les oracles décentralisés peuvent résoudre certains de ces problèmes et proposer des contrats intelligents Ethereum sans confiance en données externes. Choisissez avec soin et vous pourrez commencer à explorer le pont entre Ethereum et le "monde réel" qu’offrent les oracles.

Applications décentralisées (DApps)

Bannière Amazon du livre Maîtriser Ethereum

Dans ce chapitre, nous allons explorer le monde des applications décentralisées, ou DApps. Dès les premiers jours d’Ethereum, la vision des fondateurs était bien plus large que les "contrats intelligents" : rien de moins que de réinventer le web et de créer un nouveau monde de DApps, appelé à juste titre web3. Les contrats intelligents sont un moyen de décentraliser la logique de contrôle et les fonctions de paiement des applications. Les DApps Web3 consistent à décentraliser tous les autres aspects d’une application : stockage, messagerie, nommage, etc. (voir Web3 : Un web décentralisé utilisant des contrats intelligents et des technologies P2P).

Web3 : un Web décentralisé utilisant des contrats intelligents et les technologies P2P
Figure 41. Web3 : Un web décentralisé utilisant des contrats intelligents et des technologies P2P
Warning

Alors que les "applications décentralisées" sont une vision audacieuse de l’avenir, le terme "DApp" est souvent appliqué à tout contrat intelligent avec une interface Web. Certaines de ces soi-disant DApps sont des applications hautement centralisées (CApps ?). Méfiez-vous des faux DApps !

Dans ce chapitre, nous allons développer et déployer un exemple de DApp : une plateforme d’enchères. Vous pouvez trouver le code source dans le référentiel du livre sous code/auction_dapp. Nous examinerons chaque aspect d’une application d’enchères et verrons comment nous pouvons décentraliser l’application autant que possible. Cependant, examinons d’abord de plus près les caractéristiques et les avantages déterminants des DApps.

Qu’est-ce qu’un DApp ?

Une DApp est une application qui est principalement ou entièrement décentralisée.

Considérez tous les aspects possibles d’une application qui peut être décentralisée :

  • Logiciel dorsale (logique d’application)

  • Logiciel frontal

  • Stockage de données

  • Communications par messages

  • Résolution de nom

Chacun d’eux peut être quelque peu centralisé ou quelque peu décentralisé. Par exemple, une interface peut être développée comme une application Web qui s’exécute sur un serveur centralisé ou comme une application mobile qui s’exécute sur votre appareil. Le backend et le stockage peuvent être sur des serveurs privés et des bases de données propriétaires, ou vous pouvez utiliser un contrat intelligent et un stockage P2P.

La création d’une DApp présente de nombreux avantages qu’une architecture centralisée typique ne peut pas offrir :

Élasticité

Parce que la logique applicative est contrôlée par un contrat intelligent, une DApp dorsale sera entièrement distribué et géré sur une plateforme chaîne de blocs. Contrairement à une application déployée sur un serveur centralisé, une DApp n’aura pas de temps d’arrêt et restera disponible tant que la plateforme fonctionnera.

Transparence

La nature en chaîne d’une DApp permet à chacun d’inspecter le code et d’être plus sûr de sa fonction. Toute interaction avec la DApp sera stockée pour toujours dans la chaîne de blocs.

Résistance à la censure

Tant qu’un utilisateur a accès à un nœud Ethereum (en exécutant un si nécessaire), l’utilisateur pourra toujours interagir avec une DApp sans interférence de tout contrôle centralisé. Aucun fournisseur de services, ni même le propriétaire du contrat intelligent, ne peut modifier le code une fois qu’il est déployé sur le réseau.

Dans l’écosystème Ethereum tel qu’il se présente aujourd’hui, il existe très peu d’applications véritablement décentralisées, la plupart reposant encore sur des services et des serveurs centralisés pour une partie de leur fonctionnement. À l’avenir, nous prévoyons qu’il sera possible pour chaque partie de n’importe quelle DApp d’être exploitée de manière entièrement décentralisée.

Environnement dorsale (contrat intelligent)

Dans une DApp, les contrats intelligents sont utilisés pour stocker la logique applicative (code de programme) et l’état correspondant de votre application. Vous pouvez penser à un contrat intelligent remplaçant un composant côté serveur (alias "backend" ou "environnement dorsale") dans une application standard. C’est une simplification excessive, bien sûr. L’une des principales différences est que tout calcul exécuté dans un contrat intelligent est très coûteux et doit donc être réduit au minimum. Il est donc important d’identifier les aspects de l’application qui nécessitent une plateforme d’exécution fiable et décentralisée.

Les contrats intelligents Ethereum vous permettent de créer des architectures dans lesquelles un réseau de contrats intelligents appellent et transmettent des données entre eux, lisant et écrivant leurs propres variables d’état au fur et à mesure, leur complexité n’étant limitée que par la limite de gaz de bloc. Après avoir déployé votre contrat intelligent, votre logique applicative pourrait bien être utilisée par de nombreux autres développeurs à l’avenir.

L’une des principales considérations de la conception d’une architecture de contrat intelligent est l’impossibilité de modifier le code d’un contrat intelligent une fois qu’il est déployé. Il peut être supprimé s’il est programmé avec un opcode SELFDESTRUCT accessible, mais à part une suppression complète, le code ne peut en aucun cas être modifié.

La deuxième considération majeure de la conception de l’architecture des contrats intelligents est la taille du DApp. Un très gros contrat intelligent monolithique peut coûter beaucoup de gaz à déployer et à utiliser. Par conséquent, certaines applications peuvent choisir d’avoir un calcul hors chaîne et une source de données externe. Gardez à l’esprit, cependant, que le fait que la logique applicative de base de la DApp dépende de données externes (par exemple, à partir d’un serveur centralisé) signifie que vos utilisateurs devront faire confiance à ces ressources externes.

Environnement frontale (interface utilisateur Web)

Contrairement à la logique applicative de la DApp, qui nécessite qu’un développeur comprenne l’EVM et de nouveaux langages comme Solidity, l’interface côté client d’une DApp peut utiliser des technologies web standards (HTML, CSS, JavaScript, etc.). Cela permet à un développeur Web traditionnel d’utiliser des outils, des bibliothèques et des frameworks familiers. Les interactions avec Ethereum, telles que la signature de messages, l’envoi de transactions et la gestion de clés, sont souvent effectuées via le navigateur Web, via une extension telle que MetaMask (voir Les bases d’Ethereum).

Bien qu’il soit également possible de créer un DApp mobile, il existe actuellement peu de ressources pour aider à créer des interfaces DApp mobiles, principalement en raison du manque de clients mobiles pouvant servir de client léger avec une fonctionnalité de gestion des clés.

L’interface est généralement liée à Ethereum via la bibliothèque JavaScript web3.js, qui est fournie avec les ressources de l’interface et servie à un navigateur par un serveur Web.

Stockage de données

En raison des coûts élevés du gaz et de la limite de gaz actuellement faible, les contrats intelligents ne sont pas bien adapté au stockage ou au traitement de grandes quantités de données. Par conséquent, la plupart des DApps utilisent des services de stockage de données hors chaîne, ce qui signifie qu’ils stockent les données volumineuses de la chaîne Ethereum, sur une plate-forme de stockage de données. Cette plate-forme de stockage de données peut être centralisée (par exemple, une base de données cloud typique), ou les données peuvent être décentralisées, stockées sur une plate-forme P2P telle que l’IPFS ou la propre plate-forme Swarm d’Ethereum.

Le stockage P2P décentralisé est idéal pour stocker et distribuer de gros actifs statiques tels que des images, des vidéos et les ressources de l’interface Web frontale de l’application (HTML, CSS, JavaScript, etc.). Nous examinerons ensuite quelques-unes des options.

IPFS

Le Inter-Planetary File System (IPFS) est un système de stockage décentralisé adressable par le contenu qui distribue les objets stockés entre pairs dans un réseau P2P. "Contenu adressable" signifie que chaque élément de contenu (fichier) est haché et que le hachage est utilisé pour identifier ce fichier. Vous pouvez ensuite récupérer n’importe quel fichier de n’importe quel nœud IPFS en le demandant par son hachage.

IPFS vise à remplacer HTTP comme protocole de choix pour la livraison d’applications Web. Au lieu de stocker une application Web sur un seul serveur, les fichiers sont stockés sur IPFS et peuvent être récupérés à partir de n’importe quel nœud IPFS.

Plus d’informations sur IPFS peuvent être trouvées sur https://ipfs.io.

Swarm

Swarm est un autre système de stockage P2P adressable par le contenu , similaire à IPFS. Swarm a été créé par la Fondation Ethereum, dans le cadre de la suite d’outils Go-Ethereum. Comme IPFS, il vous permet de stocker des fichiers qui sont diffusés et répliqués par les nœuds Swarm. Vous pouvez accéder à n’importe quel fichier Swarm en y faisant référence par un hachage. Swarm vous permet d’accéder à un site Web à partir d’un système P2P décentralisé, au lieu d’un serveur Web central.

La page d’accueil de Swarm est elle-même stockée sur Swarm et accessible sur votre nœud Swarm ou une passerelle : https://swarm-gateways.net/bzz:/theswarm.eth/.

Protocoles de communication de messages décentralisés

Un autre composant majeur de toute application est la communication inter-processus. Cela signifie pouvoir échanger des messages entre applications, entre différentes instances de l’application ou entre utilisateurs de l’application. Traditionnellement, cela est réalisé en s’appuyant sur un serveur centralisé. Cependant, il existe une variété d’alternatives décentralisées aux protocoles basés sur serveur, offrant une messagerie sur un réseau P2P. Le protocole de messagerie P2P le plus notable pour les DApps est Whisper, qui fait partie de la suite d’outils Go-Ethereum de la Fondation Ethereum.

Le dernier aspect d’une application qui peut être décentralisée est la résolution de noms. Nous examinerons de près le service de noms d’Ethereum plus loin dans ce chapitre ; maintenant, cependant, montrons un exemple.

Un exemple de DApp de base : DApp d’enchères

Dans cette section, nous allons commencer à construire un exemple de DApp, pour explorer les différents outils de décentralisation. Notre DApp mettra en place une enchère décentralisée.

La DApp d’enchères permet à un utilisateur d’enregistrer un jeton "acte", qui représente un actif unique, comme une maison, une voiture, une marque, etc. Une fois qu’un jeton a été enregistré, la propriété du jeton est transférée à la DApp d’enchères, ce qui lui permet d’être mis en vente. Le DApp d’enchères répertorie chacun des jetons enregistrés, permettant aux autres utilisateurs de placer des offres. Au cours de chaque enchère, les utilisateurs peuvent rejoindre une salle de discussion créée spécifiquement pour cette enchère. Une fois qu’une enchère est finalisée, la propriété du jeton d’acte est transférée au gagnant de l’enchère.

Le processus global d’enchères peut être vu dans Auction DApp : un exemple simple de DApp d’enchères.

Les principaux composants de notre DApp d’enchères sont :

  • Un contrat intelligent implémentant des jetons "acte" non fongibles ERC721 (DeedRepository)

  • Un contrat intelligent mettant en place une enchère (AuctionRepository)pour vendre les actes

  • Une interface web utilisant le cadre de développement JavaScript Vue/Vuetify

  • La bibliothèque web3.js pour se connecter aux chaînes Ethereum (via MetaMask ou d’autres clients)

  • Un client Swarm, pour stocker des ressources telles que des images

  • Un client Whisper, pour créer des salons de discussion par enchère pour tous les participants

DApp d’enchères : un exemple simple de DApp d’enchères
Figure 42. Auction DApp : un exemple simple de DApp d’enchères

Vous pouvez trouver le code source de la DApp d’enchères dans le référentiel du livre.

DApp d’enchères : contrats intelligents dorsaux

Notre exemple Auction DApp est pris en charge par deux contrats intelligents que nous devons déployer sur une chaîne de blocs Ethereum afin de prendre en charge l’application : AuctionRepository et DeedRepository.

Commençons par regarder DeedRepository, montré dans DeedRepository.sol : un jeton d’acte ERC721 à utiliser dans une vente aux enchères. Ce contrat est un jeton non fongible compatible ERC721 (voir ERC721 : norme de jeton non fongible (acte)).

Example 24. DeedRepository.sol : un jeton d’acte ERC721 à utiliser dans une vente aux enchères
pragma solidity ^0.5.16;
import "./ERC721/ERC721Token.sol";

/**
 * @title Repository of ERC721 Deeds
 * This contract contains the list of deeds registered by users.
 * This is a demo to show how tokens (deeds) can be minted and added 
 * to the repository.
 */
contract DeedRepository is ERC721Token {


    /**
    * @dev Created a DeedRepository with a name and symbol
    * @param _name string represents the name of the repository
    * @param _symbol string represents the symbol of the repository
    */
    constructor(string memory _name, string memory _symbol) 
        public ERC721Token(_name, _symbol) {}
    
    /**
    * @dev Public function to register a new deed
    * @dev Call the ERC721Token minter
    * @param _tokenId uint256 represents a specific deed
    * @param _uri string containing metadata/uri
    */
    function registerDeed(uint256 _tokenId, string memory _uri) public {
        _mint(msg.sender, _tokenId);
        addDeedMetadata(_tokenId, _uri);
        emit DeedRegistered(msg.sender, _tokenId);
    }

    /**
    * @dev Public function to add metadata to a deed
    * @param _tokenId represents a specific deed
    * @param _uri text which describes the characteristics of a given deed
    * @return whether the deed metadata was added to the repository
    */
    function addDeedMetadata(uint256 _tokenId, string memory _uri) public returns(bool){
        _setTokenURI(_tokenId, _uri);
        return true;
    }

    /**
    * @dev Event is triggered if deed/token is registered
    * @param _by address of the registrar
    * @param _tokenId uint256 represents a specific deed
    */
    event DeedRegistered(address _by, uint256 _tokenId);
}

Comme vous pouvez le constater, le contrat DeedRepository est une implémentation simple d’un jeton compatible ERC721.

Notre DApp d’enchères utilise le contrat DeedRepository pour émettre et suivre les jetons pour chaque enchère. L’enchère elle-même est orchestrée par le contrat AuctionRepository. Ce contrat est trop long pour être inclus ici dans son intégralité, mais AuctionRepository.sol : le principal contrat intelligent Auction DApp montre la définition principale du contrat et des structures de données. L’intégralité du contrat est disponible dans le référentiel GitHub du livre.

Example 25. AuctionRepository.sol : le principal contrat intelligent Auction DApp
contract AuctionRepository {

    // Tableau avec toutes les enchères
    Auction[] public auctions;

    // Mappage de l'index d'enchères aux offres des utilisateurs
    mapping(uint256 => Bid[]) public auctionBids;

    // Mappage du propriétaire à une liste d'enchères détenues
    mapping(address => uint[]) public auctionOwner;

    // Structure d'enchères pour retenir l'enchérisseur et le montant
    struct Bid {
        address from;
        uint256 amount;
    }

    // Structure d'enchères contenant toutes les informations requises
    struct Auction {
        string name;
        uint256 blockDeadline;
        uint256 startPrice;
        string metadata;
        uint256 deedId;
        address deedRepositoryAddress;
        address owner;
        bool active;
        bool finalized;
    }

Le contrat AuctionRepository gère toutes les enchères avec les fonctions suivantes :

getCount()
getBidsCount(uint _auctionId)
getAuctionsOf(address _owner)
getCurrentBid(uint _auctionId)
getAuctionsCountOfOwner(address _owner)
getAuctionById(uint _auctionId)
createAuction(address _deedRepositoryAddress, uint256 _deedId,
              string _auctionTitle, string _metadata, uint256 _startPrice,
              uint _blockDeadline)
approveAndTransfer(address _from, address _to, address _deedRepositoryAddress,
                   uint256 _deedId)
cancelAuction(uint _auctionId)
finalizeAuction(uint _auctionId)
bidOnAuction(uint _auctionId)

Vous pouvez déployer ces contrats sur la chaîne de blocs Ethereum de votre choix (par exemple, Ropsten) en utilisant truffle dans le référentiel du livre :

$ cd code/auction_dapp/backend
$ truffle init
$ truffle compile
$ truffle migrate --network ropsten

DApp de gouvernance

Si vous lisez les deux contrats intelligents de la DApp d’enchères, vous remarquerez quelque chose d’important : il n’y a pas de compte ou de rôle spécial qui dispose de privilèges spéciaux sur la DApp. Chaque enchère a un propriétaire avec des capacités spéciales, mais la DApp d’enchères elle-même n’a pas d’utilisateur privilégié.

Il s’agit d’un choix délibéré de décentraliser la gouvernance de la DApp et de renoncer à tout contrôle une fois celle-ci déployée. Certains DApp, en comparaison, ont un ou plusieurs comptes privilégiés avec des capacités spéciales, telles que la possibilité de résilier le contrat DApp, de remplacer ou de modifier sa configuration, ou d’opposer son veto à certaines opérations. Habituellement, ces fonctions de gouvernance sont introduites dans la DApp afin d’éviter des problèmes inconnus qui pourraient survenir en raison d’un bogue.

La question de la gouvernance est particulièrement difficile à résoudre car elle représente une arme à double tranchant. D’un côté, les comptes privilégiés sont dangereux ; s’ils sont compromis, ils peuvent compromettre la sécurité du DApp. De l’autre côté, sans compte privilégié, il n’y a pas d’options de récupération si un bogue est trouvé. Nous avons vu ces deux risques se manifester dans les DApp Ethereum. Dans le cas de The DAO (Exemple concret : le DAO et Historique de la fourche Ethereum), il y avait des comptes privilégiés appelés les "conservateurs", mais ils étaient très limités dans leurs capacités. Ces comptes n’ont pas pu annuler le retrait des fonds par l’attaquant DAO. Dans un cas plus récent, l’échange décentralisé Bancor a subi un vol massif parce qu’un compte de gestion privilégié a été compromis. Il s’avère que Bancor n’était pas aussi décentralisé qu’on le supposait initialement.

Lors de la construction d’une DApp, vous devez décider si vous voulez rendre les contrats intelligents vraiment indépendants, les lancer et n’avoir ensuite aucun contrôle, ou créer des comptes privilégiés et courir le risque que ceux-ci soient compromis. L’un ou l’autre choix comporte des risques, mais à long terme, les vraies DApps ne peuvent pas avoir un accès spécialisé pour les comptes privilégiés - ce n’est pas décentralisé.

DApp d’enchères : interface utilisateur frontale

Une fois les contrats de la DApp d’enchères déployés, vous pouvez interagir avec eux à l’aide de votre console JavaScript préférée et de web3.js, ou d’une autre bibliothèque web3. Cependant, la plupart des utilisateurs auront besoin d’une interface facile à utiliser. Notre interface utilisateur de la DApp d’enchères est construite à l’aide du cadre de développement (framework) JavaScript Vue2/Vuetify de Google.

Vous pouvez trouver le code de l’interface utilisateur dans le dossier code/auction_dapp/frontend dans https://github.com/ethereumbook/ethereumbook [le référentiel du livre]. Le répertoire a la structure et le contenu suivants :

frontend/
|-- build
|   |-- build.js
|   |-- check-versions.js
|   |-- logo.png
|   |-- utils.js
|   |-- vue-loader.conf.js
|   |-- webpack.base.conf.js
|   |-- webpack.dev.conf.js
|   `-- webpack.prod.conf.js
|-- config
|   |-- dev.env.js
|   |-- index.js
|   `-- prod.env.js
|-- index.html
|-- package.json
|-- package-lock.json
|-- README.md
|-- src
|   |-- App.vue
|   |-- components
|   |   |-- Auction.vue
|   |   `-- Home.vue
|   |-- config.js
|   |-- contracts
|   |   |-- AuctionRepository.json
|   |   `-- DeedRepository.json
|   |-- main.js
|   |-- models
|   |   |-- AuctionRepository.js
|   |   |-- ChatRoom.js
|   |   `-- DeedRepository.js
|   `-- router
|       `-- index.js

Une fois que vous avez déployé les contrats, modifiez la configuration du frontend dans frontend/src/config.js et entrez les adresses des contrats DeedRepository et AuctionRepository, tels que déployés. L’application frontale a également besoin d’accéder à un nœud Ethereum offrant une interface JSON-RPC et WebSockets. Une fois que vous avez configuré l’interface, lancez-la avec un serveur Web sur votre machine locale :

$ npm install
$ npm run dev

L’interface Auction DApp sera lancée et sera accessible via n’importe quel navigateur Web à l’adresse http://localhost:8080.

Si tout se passe bien, vous devriez voir l’écran affiché dans Interface utilisateur de la DApp d’enchères, qui illustre l’exécution de la DApp d’enchères dans un navigateur Web.

Interface utilisateur de la DApp d’enchères
Figure 43. Interface utilisateur de la DApp d’enchères

Décentraliser davantage la DApp d’enchères

Notre DApp est déjà assez décentralisé, mais nous pouvons améliorer les choses.

Le contrat AuctionRepository fonctionne indépendamment de tout contrôle, ouvert à tous. Une fois déployé, il ne peut pas être arrêté, et aucune enchère ne peut être contrôlée. Chaque enchère a une salle de chat séparée qui permet à quiconque de communiquer sur l’enchère sans censure ni identification. Les différents actifs d’enchères, tels que la description et l’image associée, sont stockés sur Swarm, ce qui les rend difficiles à censurer ou à bloquer.

N’importe qui peut interagir avec la DApp en créant des transactions manuellement ou en exécutant l’interface Vue sur sa machine locale. Le code de la DApp lui-même est à source libre et développé en collaboration sur un référentiel public.

Il y a deux choses que nous pouvons faire pour rendre cette DApp décentralisée et résiliente :

  • Stockez tout le code d’application sur Swarm ou IPFS.

  • Accédez au DApp par référence à un nom, en utilisant le service de nom Ethereum.

Nous explorerons la première option dans la section suivante, et nous approfondirons la seconde dans Le service de noms Ethereum (ENS).

Stockage de la DApp d’enchères sur Swarm

Nous avons introduit Swarm dans Swarm, plus haut dans ce chapitre. Notre DApp d’enchères utilise déjà Swarm pour stocker l’image de l’icône pour chaque enchère. C’est une solution beaucoup plus efficace que de tenter de stocker des données sur Ethereum, ce qui coûte cher. Il est également beaucoup plus résistant que si ces images étaient stockées dans un service centralisé comme un serveur Web ou un serveur de fichiers.

Mais on peut aller plus loin. Nous pouvons stocker l’intégralité de l’interface du DApp lui-même dans Swarm et l’exécuter directement à partir d’un nœud Swarm, au lieu d’exécuter un serveur Web.

Préparation de Swarm

Pour commencer, vous devez installer Swarm et initialiser votre nœud Swarm. Swarm fait partie de la suite d’outils Go-Ethereum de la Fondation Ethereum. Reportez-vous aux instructions d’installation de Go-Ethereum dans Go-Ethereum (Geth), ou pour installer une version binaire Swarm, suivez les instructions de la documentation Swarm.

Une fois que vous avez installé Swarm, vous pouvez vérifier qu’il fonctionne correctement en le lançant avec la commande version :

$ swarm version
Version: 0.3
Git Commit: 37685930d953bcbe023f9bc65b135a8d8b8f1488
Go Version: go1.10.1
OS: linux

Pour commencer à exécuter Swarm, vous devez lui dire comment se connecter à une instance de Geth, pour accéder à l’API JSON-RPC. Commencez en suivant les instructions du Guide de démarrage.

Lorsque vous démarrez Swarm, vous devriez voir quelque chose comme ceci :

Maximum peer count                       ETH=25 LES=0 total=25
Starting peer-to-peer node               instance=swarm/v0.3.1-225171a4/linux...
connecting to ENS API                    url=http://127.0.0.1:8545
swarm[5955] : [189B blob data]
Starting P2P networking
UDP listener up                          self=enode://f50c8e19ff841bcd5ce7d2d...
Updated bzz local addr                   oaddr=9c40be8b83e648d50f40ad3... uaddr=e
Starting Swarm service
9c40be8b hive starting
detected an existing store. trying to load peers
hive 9c40be8b: peers loaded
Swarm network started on bzz address: 9c40be8b83e648d50f40ad3d35f...
Pss started
Streamer started
IPC endpoint opened                      url=/home/ubuntu/.ethereum/bzzd.ipc
RLPx listener up                         self=enode://f50c8e19ff841bcd5ce7d2d...

Vous pouvez vérifier que votre nœud Swarm fonctionne correctement en vous connectant à l’interface Web de la passerelle Swarm locale : http://localhost:8500.

Vous devriez voir un écran comme celui de Passerelle Swarm sur localhost et pouvoir interroger n’importe quel hachage Swarm ou nom ENS.

Passerelle Swarm sur localhost
Figure 44. Passerelle Swarm sur localhost

Télécharger des fichiers sur Swarm

Une fois que votre nœud et votre passerelle Swarm locaux sont en cours d’exécution, vous pouvez télécharger vers Swarm et les fichiers seront accessible sur n’importe quel nœud Swarm, simplement par référence au hachage du fichier.

Testons cela en téléchargeant un fichier :

$ swarm up code/auction_dapp/README.md
ec13042c83ffc2fb5cb0aa8c53f770d36c9b3b35d0468a0c0a77c97016bb8d7c

Swarm a téléchargé le fichier README.md et renvoyé un hachage que vous pouvez utiliser pour accéder au fichier à partir de n’importe quel nœud Swarm. Par exemple, vous pouvez utiliser la https://bit.ly/2znWUP9 [passerelle Swarm publique].

Bien que le téléchargement d’un fichier soit relativement simple, il est un peu plus complexe de télécharger une interface DApp entière. En effet, les différentes ressources DApp (HTML, CSS, JavaScript, bibliothèques, etc.) ont des références intégrées les unes aux autres. Normalement, un serveur Web traduit les URL en fichiers locaux et fournit les bonnes ressources. Nous pouvons obtenir la même chose pour Swarm en empaquetant notre DApp.

Dans la DApp d’enchères, il existe un script pour regrouper toutes les ressources :

$ cd code/auction_dapp/frontend
$ npm run build

> [email protected] build /home/aantonop/Dev/ethereumbook/code/auction_dapp/frontend
> node build/build.js

Hash: 9ee134d8db3c44dd574d
Version: webpack 3.10.0
Time: 25665ms
Asset     Size
static/js/vendor.77913f316aaf102cec11.js  1.25 MB
static/js/app.5396ead17892922422d4.js   502 kB
static/js/manifest.87447dd4f5e60a5f9652.js  1.54 kB
static/css/app.0e50d6a1d2b1ed4daa03d306ced779cc.css  1.13 kB
static/css/app.0e50d6a1d2b1ed4daa03d306ced779cc.css.map  2.54 kB
static/js/vendor.77913f316aaf102cec11.js.map  4.74 MB
static/js/app.5396ead17892922422d4.js.map   893 kB
static/js/manifest.87447dd4f5e60a5f9652.js.map  7.86 kB
index.html  1.15 kB

Build complete.

Le résultat de cette commande sera un nouveau répertoire, code/auction_dapp/frontend/dist, qui contient l’intégralité de l’interface Auction DApp, regroupée :

dist/
|-- index.html
`-- static
    |-- css
    |   |-- app.0e50d6a1d2b1ed4daa03d306ced779cc.css
    |   `-- app.0e50d6a1d2b1ed4daa03d306ced779cc.css.map
    `-- js
        |-- app.5396ead17892922422d4.js
        |-- app.5396ead17892922422d4.js.map
        |-- manifest.87447dd4f5e60a5f9652.js
        |-- manifest.87447dd4f5e60a5f9652.js.map
        |-- vendor.77913f316aaf102cec11.js
        `-- vendor.77913f316aaf102cec11.js.map

Vous pouvez maintenant télécharger l’intégralité du DApp sur Swarm, en utilisant la commande up et l’option --recursive. Ici, nous disons également à Swarm que index.html est le defaultpath pour charger cette DApp :

$ swarm --bzzapi http://localhost:8500 --recursive \
  --defaultpath dist/index.html up dist/

ab164cf37dc10647e43a233486cdeffa8334b026e32a480dd9cbd020c12d4581

Désormais, l’intégralité de notre DApp d’enchères est hébergée sur Swarm et accessible par l’URL Swarm :

  • bzz://ab164cf37dc10647e43a233486cdeffa8334b026e32a480dd9cbd020c12d4581

Nous avons fait quelques progrès dans la décentralisation de notre DApp, mais nous l’avons rendu plus difficile à utiliser. Une URL comme celle-là est beaucoup moins conviviale qu’un joli nom comme auction_dapp.com. Sommes-nous obligés de sacrifier l’utilisabilité pour gagner en décentralisation ? Pas nécessairement. Dans la section suivante, nous examinerons le service de noms d’Ethereum, qui nous permet d’utiliser des noms faciles à lire tout en préservant la nature décentralisée de notre application.

Le service de noms Ethereum (ENS)

Vous pouvez concevoir le meilleur contrat intelligent au monde , mais si vous ne fournissez pas une bonne interface aux utilisateurs, ils ne pourront pas y accéder.

Sur Internet traditionnel, le système de noms de domaine (DNS) nous permet d’utiliser des noms lisibles par l’homme dans le navigateur tout en résolvant ces noms en adresses IP ou autres identifiants dans les coulisses. Sur la chaîne de blocs Ethereum, le Ethereum Naming System (ENS) résout le même problème, mais de manière décentralisée.

Par exemple, l’adresse de don de la Fondation Ethereum est 0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359 ; dans un portefeuille qui prend en charge ENS, c’est simplement ethereum.eth.

ENS est plus qu’un contrat intelligent ; c’est une DApp fondamentale en elle-même, offrant un service de noms décentralisé. En outre, ENS est pris en charge par un certain nombre de DApps pour l’enregistrement, la gestion et les enchères de noms enregistrés. ENS démontre comment les DApps peuvent fonctionner ensemble : c’est une DApp conçue pour servir d’autres DApps, prise en charge par un écosystème de DApps, intégrée dans d’autres DApps, etc.

Dans cette section, nous verrons comment fonctionne ENS. Nous montrerons comment vous pouvez configurer votre propre nom et le lier à un portefeuille ou à une adresse Ethereum, comment vous pouvez intégrer ENS dans un autre DApp et comment vous pouvez utiliser ENS pour nommer vos ressources DApp afin de les rendre plus faciles à utiliser.

Histoire des services de noms Ethereum

L’enregistrement de noms a été la première application non monétaire des chaînes de blocs, lancée par Namecoin. L’Ethereum White Paper a donné un système d’enregistrement de type Namecoin à deux lignes comme l’un de ses exemples d’applications.

Les premières versions de Geth et du client C++ Ethereum avaient un contrat namereg intégré (qui n’est plus utilisé), et de nombreuses propositions et ERC pour les services de noms ont été faites, mais ce n’est que lorsque Nick Johnson a commencé à travailler pour la Fondation Ethereum en 2016. et a pris le projet sous son aile que le travail sérieux sur un registraire a commencé.

ENS a été lancé le Star Wars Day, le 4 mai 2017 (après une tentative infructueuse de le lancer le Pi Day, le 15 mars).

La spécification ENS

ENS est spécifié principalement dans trois propositions d’amélioration d’Ethereum : EIP-137, qui spécifie les fonctions de base d’ENS ; EIP-162, qui décrit le système d’enchères pour la racine .eth ; et EIP-181, qui spécifie la résolution inverse des adresses.

ENS suit une philosophie de conception "sandwich" : une couche très simple en bas, suivie de couches de code plus complexes mais remplaçables, avec une couche supérieure très simple qui conserve tous les fonds dans des comptes séparés.

Couche inférieure : nommez les propriétaires et les résolveurs

L’ENS fonctionne sur des "nœuds" au lieu de noms lisibles par l’homme : un nom lisible par l’homme est converti en nœud à l’aide de l’algorithme "Namehash".

La couche de base d’ENS est un contrat astucieusement simple (moins de 50 lignes de code) défini par ERC137 qui permet uniquement aux propriétaires de nœuds de définir des informations sur leurs noms et de créer des sous-nœuds (l’équivalent ENS des sous-domaines DNS).

Les seules fonctions sur la couche de base sont celles qui permettent à un propriétaire de nœud de définir des informations sur son propre nœud (en particulier le résolveur, la durée de vie ou le transfert de propriété) et de créer des propriétaires de nouveaux sous-nœuds.

L’algorithme Namehash

Namehash est un algorithme récursif qui peut convertir n’importe quel nom en un hachage qui identifie le nom.

"Récursif" signifie que nous résolvons le problème en résolvant un sous-problème qui est un problème plus petit du même type, puis utilisons la solution du sous-problème pour résoudre le problème d’origine.

Namehash hache de manière récursive les composants du nom, produisant une chaîne unique de longueur fixe (ou "nœud") pour tout domaine d’entrée valide. Par exemple, le nœud Namehash de subdomain.example.eth est keccak('<example.eth> ' nœud) + keccak('<subdomain>')'. Le sous-problème que nous devons résoudre est de calculer le nœud pour `+example.eth+, qui est keccak('<.eth>' node) + keccak('<example>')'. Pour commencer, nous devons calculer le nœud pour `+eth+, qui est `keccak(<root node>) + keccak('<eth>')'.

Le nœud racine est ce que nous appelons le "cas de base" de notre récursivité, et nous ne pouvons évidemment pas le définir de manière récursive, sinon l’algorithme ne se terminera jamais ! Le nœud racine est défini comme 0x00000000000000000000000000000000000000000000000000000000000 (32 zéro octets).

En mettant tout cela ensemble, le nœud de subdomain.example.eth est donc keccak(keccak(keccak(0x0...0 + keccak('eth')) + keccak('example')) + keccak('subdomain')).

En généralisant, nous pouvons définir la fonction Namehash comme suit (le cas de base pour le nœud racine, ou nom vide, suivi de l’étape récursive) :

namehash([]) = 0x0000000000000000000000000000000000000000000000000000000000000000
namehash([label, ...]) = keccak256(namehash(...) + keccak256(label))

En Python cela devient :

def namehash(name):
  if name == '':
    return '\0' * 32
  else:
    label, _, remainder = name.partition('.')
    return sha3(namehash(remainder) + sha3(label))

Thus, mastering-ethereum.eth will be processed as follows:

namehash('mastering-ethereum.eth')
⇒ sha3(namehash('eth') + sha3('mastering-ethereum'))
⇒ sha3(sha3(namehash('') + sha3('eth')) + sha3('mastering-ethereum'))
⇒ sha3(sha3(('\0' * 32) + sha3('eth')) + sha3('mastering-ethereum'))

Bien sûr, les sous-domaines peuvent eux-mêmes avoir des sous-domaines : il peut y avoir un sub.subdomain.example.eth après subdomain.example.eth, puis un sub.sub.subdomain.example.eth, et ainsi de suite. Pour éviter un recalcul coûteux, puisque Namehash ne dépend que du nom lui-même, le nœud d’un nom donné peut être précalculé et inséré dans un contrat, éliminant ainsi le besoin de manipulation de chaîne et permettant une recherche immédiate des enregistrements ENS quel que soit le nombre de composants dans le nom brut.

Comment choisir un nom valide

Les noms consistent en une série d’étiquettes séparées par des points. Bien que les lettres majuscules et minuscules soient autorisées, toutes les étiquettes doivent suivre un processus de normalisation UTS #46 qui plie les étiquettes avant de les hacher, de sorte que les noms avec une casse différente mais une orthographe identique se retrouveront avec le même Namehash.

Vous pouvez utiliser des étiquettes et des domaines de n’importe quelle longueur, mais pour des raisons de compatibilité avec l’ancien DNS, les règles suivantes sont recommandées :

  • Les libellés ne doivent pas comporter plus de 64 caractères chacun.

  • Les noms ENS complets ne doivent pas dépasser 255 caractères.

  • Les étiquettes (labels) ne doivent pas commencer ou se terminer par des traits d’union, ni commencer par des chiffres.

Propriété du nœud racine

L’un des résultats de ce système hiérarchique est qu’il s’appuie sur les propriétaires du nœud racine, qui sont capables de créer des domaines de premier niveau (TLD).

Alors que l’objectif final est d’adopter un processus décisionnel décentralisé pour les nouveaux TLD, au moment de la rédaction, le nœud racine est contrôlé par un multisig 4 sur 7, détenu par des personnes de différents pays (construit comme un reflet des 7 détenteurs de clés du système DNS). En conséquence, une majorité d’au moins 4 des 7 détenteurs de clés est requise pour effectuer tout changement.

Actuellement, le but et l’objectif de ces détenteurs de clés est de travailler en consensus avec la communauté pour :

  • Migrer et mettre à niveau la propriété temporaire du TLD .eth vers un contrat plus permanent une fois le système évalué.

  • Autoriser l’ajout de nouveaux TLD, si la communauté convient qu’ils sont nécessaires.

  • Migrer la propriété du multisig racine vers un contrat plus décentralisé, lorsqu’un tel système est convenu, testé et mis en œuvre.

  • Servir de moyen de dernier recours pour traiter les bogues ou les vulnérabilités dans les registres de niveau supérieur.

Résolveurs

Le contrat ENS de base ne peut pas ajouter de métadonnées aux noms ; c’est le travail des soi-disant « contrats de résolveur ». Ce sont des contrats créés par l’utilisateur qui peuvent répondre à des questions sur le nom, telles que l’adresse Swarm associée à l’application, l’adresse qui reçoit les paiements à l’application (en ether ou en jetons) ou le hachage de l’application (pour vérifier son intégrité).

Couche intermédiaire : les nœuds .eth

Au moment de la rédaction, le seul domaine de premier niveau pouvant être enregistré de manière unique dans un contrat intelligent est .eth.

Note

Des travaux sont en cours pour permettre aux propriétaires de domaines DNS traditionnels de revendiquer la propriété ENS. Bien qu’en théorie cela puisse fonctionner pour .com, le seul domaine pour lequel cela a été implémenté jusqu’à présent est .xyz, et uniquement sur le testnet de Ropsten.

Les domaines .eth sont distribués via un système d’enchères. Il n’y a pas de liste réservée ni de priorité, et la seule façon d’acquérir un nom est d’utiliser le système. Le système d’enchères est un morceau de code complexe (plus de 500 lignes) ; la plupart des premiers efforts de développement (et des bugs !) dans ENS concernaient cette partie du système. Cependant, il est également remplaçable et évolutif, sans risque pour les fonds - nous en reparlerons plus tard.

Enchères Vickrey

Les noms sont distribués via une enchère Vickrey modifiée. Dans une enchère Vickrey traditionnelle, chaque enchérisseur soumet une offre scellée, et tous sont révélés simultanément, auquel cas le plus offrant remporte l’enchère mais ne paie que la deuxième offre la plus élevée. Par conséquent, les enchérisseurs sont incités à ne pas enchérir moins que la valeur réelle du nom pour eux, car enchérir sur leur valeur réelle augmente les chances qu’ils gagnent mais n’affecte pas le prix qu’ils paieront finalement.

Sur une chaîne de blocs, certains changements sont nécessaires :

  • Pour s’assurer que les enchérisseurs ne soumettent pas d’enchères qu’ils n’ont pas l’intention de payer, ils doivent verrouiller au préalable une valeur égale ou supérieure à leur enchère, afin de garantir la validité de l’enchère.

  • Étant donné que vous ne pouvez pas cacher de secrets sur une chaîne de blocs, les enchérisseurs doivent exécuter au moins deux transactions (un processus de validation-révélation) afin de masquer la valeur et le nom d’origine sur lesquels ils ont enchéri.

  • Étant donné que vous ne pouvez pas révéler toutes les offres simultanément dans un système décentralisé, les enchérisseurs doivent révéler eux-mêmes leurs propres offres ; s’ils ne le font pas, ils perdent leurs fonds bloqués. Sans ce forfait, on pourrait faire de nombreuses offres et choisir de n’en révéler qu’une ou deux, transformant une enchère à offre scellée en une enchère de prix croissant traditionnelle.

Par conséquent, l’enchère est un processus en quatre étapes :

  1. Lancez l’enchère. Ceci est nécessaire pour diffuser l’intention d’enregistrer un nom. Cela crée toutes les dates limites d’enchères. Les noms sont hachés, de sorte que seuls ceux qui ont le nom dans leur dictionnaire sauront quelle enchère a été ouverte. Cela permet une certaine confidentialité, ce qui est utile si vous créez un nouveau projet et que vous ne souhaitez pas partager de détails à son sujet. Vous pouvez ouvrir plusieurs enchères fictives en même temps, donc si quelqu’un vous suit, il ne peut pas simplement enchérir sur toutes les enchères que vous ouvrez.

  2. Faites une offre scellée. Vous devez le faire avant la date limite d’enchère, en liant une quantité donnée d’ether au hachage d’un message secret (contenant, entre autres, le hachage du nom, le montant réel de l’enchère et un sel). Vous pouvez verrouiller plus d’ether que vous n’enchérissez réellement afin de masquer votre véritable évaluation.

  3. Révélez l’enchère. Pendant la période de révélation, vous devez effectuer une transaction qui révèle l’enchère, qui calculera ensuite l’enchère la plus élevée et la deuxième enchère la plus élevée et renverra l’ether aux enchérisseurs non retenus. Chaque fois que l’enchère est révélée, le gagnant actuel est recalculé ; par conséquent, le dernier à être défini avant l’expiration du délai de révélation devient le grand gagnant.

  4. Nettoyer après. Si vous êtes le gagnant, vous pouvez finaliser l’enchère afin de récupérer la différence entre votre enchère et la deuxième enchère la plus élevée. Si vous avez oublié de révéler, vous pouvez faire une révélation tardive et récupérer une partie de votre enchère.

Couche supérieure : les actes

La couche supérieure d’ENS est encore un autre contrat super simple avec un seul but : détenir les fonds.

Lorsque vous gagnez un nom, les fonds ne sont en fait envoyés nulle part, mais sont simplement bloqués pendant la période pendant laquelle vous souhaitez conserver le nom (au moins un an). Cela fonctionne comme un rachat garanti : si le propriétaire ne veut plus du nom, il peut le revendre au système et récupérer son ether (ainsi, le coût de détention du nom est le coût d’opportunité de faire quelque chose avec un rendement supérieur à zéro ).

Bien sûr, avoir un seul contrat détenant des millions de dollars en ether s’est avéré très risqué, donc à la place, ENS crée un contrat d’acte pour chaque nouveau nom. Le contrat d’acte est très simple (environ 50 lignes de code), et il ne permet que les fonds d’être transférés vers un seul compte (le propriétaire de l’acte) et d’être appelés par une seule entité (le contrat du registraire). Cette approche réduit considérablement la surface d’attaque où les bogues peuvent mettre les fonds en danger.

Enregistrer un nom

L’enregistrement d’un nom dans ENS est un processus en quatre étapes, comme nous l’avons vu dans Enchères Vickrey. Nous plaçons d’abord une enchère pour n’importe quel nom disponible, puis nous révélons notre enchère après 48 heures pour sécuriser le nom. Calendrier ENS pour l’enregistrement est un schéma montrant le calendrier d’enregistrement.

Enregistrons notre premier nom !

Nous utiliserons l’une des nombreuses interfaces conviviales disponibles pour rechercher les noms disponibles, placer une enchère sur le nom ethereumbook.eth, révéler l’enchère et sécuriser le nom.

Il existe un certain nombre d’interfaces Web vers ENS qui nous permettent d’interagir avec l’ENS DApp. Pour cet exemple, nous utiliserons l’interface MyCrypto, en conjonction avec MetaMask comme portefeuille.

ens flow
Figure 45. Calendrier ENS pour l’enregistrement

Tout d’abord, nous devons nous assurer que le nom que nous voulons est disponible. En écrivant ce livre, nous voulions vraiment enregistrer le nom mastering.eth, mais hélas, Recherche de noms ENS sur MyCrypto.com a révélé qu’il était déjà pris ! Comme les inscriptions à l’ENS ne durent qu’un an, il pourrait devenir possible d’obtenir ce nom à l’avenir. En attendant, recherchons ethereumbook.eth (Recherche de noms ENS sur MyCrypto.com).

Génial ! Le nom est disponible. Pour l’enregistrer, nous devons avancer avec Démarrer une offre d’enchère pour un nom ENS. Débloquons MetaMask et commençons notre enregistrement pour ethereumbook.eth.

Démarrer une offre d’enchère pour un nom ENS
Figure 47. Démarrer une offre d’enchère pour un nom ENS

Faisons notre offre. Pour ce faire, nous devons suivre les étapes de Placer une offre pour un nom ENS.

Placer une offre d’enchère pour un nom ENS
Figure 48. Placer une offre pour un nom ENS
Warning

Comme mentionné dans Enchères Vickrey, vous devez révéler votre enchère dans les 48 heures suivant la fin de l’enchère, sinon vous perdez les fonds de votre enchère. Avons-nous oublié de le faire et avons-nous perdu 0,01 ETH nous-mêmes ? Vous pariez que nous l’avons fait.

Prenez une capture d’écran, enregistrez votre phrase secrète (comme sauvegarde pour votre enchère) et ajoutez un rappel dans votre calendrier pour la date et l’heure de révélation, afin de ne pas oublier et de ne pas perdre vos fonds.

Enfin, nous confirmons la transaction en cliquant sur le gros bouton vert de soumission affiché dans [ens-metamask-bid].

Transaction MetaMask contenant votre enchère
Figure 49. Transaction MetaMask contenant votre enchère

Si tout se passe bien, après avoir soumis une transaction de cette manière, vous pouvez revenir et révéler l’offre dans les 48 heures, et le nom que vous avez demandé sera enregistré sur votre adresse Ethereum.

Gérer votre nom ENS

Une fois que vous avez enregistré un nom ENS, vous pouvez le gérer à l’aide d’une autre interface conviviale : ENS Manager.

Une fois là-bas, saisissez le nom que vous souhaitez gérer dans la zone de recherche (voir L’interface web ENS Manager). Vous devez avoir votre portefeuille Ethereum (par exemple, MetaMask) déverrouillé, afin que le DApp ENS Manager puisse gérer l’enregistrement en votre nom.

L’interface web de l’ENS Manager
Figure 50. L’interface web ENS Manager

À partir de cette interface, nous pouvons créer des sous-domaines, définir un contrat de résolution (nous en reparlerons plus tard) et connecter chaque nom à la ressource appropriée, telle que l’adresse Swarm d’une interface DApp.

Création d’un sous-domaine ENS

Commençons par créer un sous-domaine pour notre exemple de DApp d’enchères (voir [ens-manager-add-subdomain]). Nous nommerons le sous-domaine auction, donc le nom complet sera auction.ethereumbook.eth.

Ajout du sous-domaine auction.ethereumbook.eth
Figure 51. Ajout du sous-domaine auction.ethereumbook.eth

Une fois que nous avons créé le sous-domaine, nous pouvons saisir auction.ethereumbook.eth dans la zone de recherche et le gérer, tout comme nous avons géré le domaine ethereumbook.eth précédemment.

Résolveurs ENS

Dans ENS, la résolution d’un nom est un processus en deux étapes :

  1. Le registre ENS est appelé avec le nom à résoudre après l’avoir haché. Si l’enregistrement existe, le registre renvoie l’adresse de son résolveur.

  2. Le résolveur est appelé, en utilisant la méthode appropriée à la ressource demandée. Le résolveur renvoie le résultat souhaité.

Ce processus en deux étapes présente plusieurs avantages. Séparer la fonctionnalité des résolveurs du système de nommage lui-même nous donne beaucoup plus de flexibilité. Les propriétaires de noms peuvent utiliser des résolveurs personnalisés pour résoudre n’importe quel type ou ressource, étendant ainsi les fonctionnalités d’ENS. Par exemple, si à l’avenir vous vouliez lier une ressource de géolocalisation (longitude/lattitude) à un nom ENS, vous pourriez créer un nouveau résolveur qui répond à une requête de géolocalisation. Qui sait quelles applications pourraient être utiles à l’avenir ? Avec les résolveurs personnalisés, la seule limite est votre imagination.

Pour plus de commodité, il existe un résolveur public par défaut qui peut résoudre une variété de ressources, y compris l’adresse (pour les portefeuilles ou les contrats) et le contenu (un hachage Swarm pour les DApps ou le code source du contrat).

Puisque nous voulons lier notre DApp d’enchères à un hachage Swarm, nous pouvons utiliser le résolveur public, qui prend en charge la résolution de contenu, comme indiqué dans Définition du résolveur public par défaut pour auction.ethereumbook.eth ; nous n’avons pas besoin de coder ou de déployer un résolveur personnalisé.

Définir le résolveur public par défaut pour auction.ethereumbook.eth
Figure 52. Définition du résolveur public par défaut pour auction.ethereumbook.eth

Résolution d’un nom en un hachage Swarm (contenu)

Une fois que le résolveur pour auction.ethereumbook.eth est défini pour être le résolveur public, nous pouvons le configurer pour qu’il renvoie le hachage Swarm comme contenu de notre nom (voir Définition du 'contenu' à retourner pour auction.ethereumbook.eth).

Définir le retour 'contenu' pour auction.ethereumbook.eth
Figure 53. Définition du 'contenu' à retourner pour auction.ethereumbook.eth

Après avoir attendu un peu de temps pour que notre transaction soit confirmée, nous devrions être en mesure de résoudre correctement le nom. Avant de définir un nom, notre DApp d’enchères pouvait être trouvée sur une passerelle Swarm par son hachage :

  • https://swarm-gateways.net/bzz:/ab164cf37dc10647e43a233486cdeffa8334b026e32a480dd9cbd020c12d4581

ou en recherchant dans un navigateur DApp ou une passerelle Swarm pour l’URL Swarm :

  • bzz://ab164cf37dc10647e43a233486cdeffa8334b026e32a480dd9cbd020c12d4581

Maintenant que nous l’avons attaché à un nom, c’est beaucoup plus simple :

  • http://swarm-gateways.net/bzz:/auction.ethereumbook.eth/

Nous pouvons également le trouver en recherchant "auction.ethereumbook.eth" dans n’importe quel portefeuille compatible ENS ou navigateur DApp (par exemple, Mist).

De l’application à la DApp

Au cours des dernières sections, nous avons progressivement construit une application décentralisée. Nous avons commencé avec une paire de contrats intelligents pour organiser une vente aux enchères d’actes ERC721. Ces contrats ont été conçus pour ne pas avoir de compte gouvernant ou privilégié, de sorte que leur fonctionnement est véritablement décentralisé. Nous avons ajouté une interface, implémentée en JavaScript, qui offre une interface pratique et conviviale à notre DApp. La DApp d’enchères utilise le système de stockage décentralisé Swarm pour stocker les ressources d’application telles que les images. La DApp utilise également le protocole de communication décentralisé Whisper pour offrir une salle de chat cryptée pour chaque enchère, sans aucun serveur central.

Nous avons téléchargé l’intégralité de l’interface sur Swarm, afin que notre DApp ne s’appuie sur aucun serveur Web pour servir les fichiers. Enfin, nous avons attribué un nom à notre DApp en utilisant ENS, en le connectant au hachage Swarm de l’interface, afin que les utilisateurs puissent y accéder avec un nom simple et facile à retenir, lisible par l’homme.

A chacune de ces étapes, nous avons augmenté la décentralisation de notre application. Le résultat final est une DApp qui n’a pas de point central d’autorité, pas de point central de défaillance, et exprime la vision "web3".

Architecture DApp d’enchères montre l’architecture complète de la DApp d’enchères.

Architecture DApp d’enchères
Figure 54. Architecture DApp d’enchères

Conclusion

Les applications décentralisées sont l’aboutissement de la vision d’Ethereum, telle qu’exprimée par les fondateurs dès les premières conceptions. Alors que de nombreuses applications s’appellent aujourd’hui "DApps", la plupart ne sont pas entièrement décentralisées. Cependant, il est déjà possible de construire des applications presque totalement décentralisées. Au fil du temps, à mesure que la technologie mûrit, de plus en plus de nos applications peuvent être décentralisées, ce qui se traduit par un Web plus résilient, résistant à la censure et libre.

La machine virtuelle Ethereum

Bannière Amazon du livre Maîtriser Ethereum

Au cœur du protocole et du fonctionnement Ethereum se trouve la machine virtuelle Ethereum, ou EVM en abrégé. Comme vous pouvez le deviner d’après son nom, il s’agit d’un moteur de calcul, pas très différent des machines virtuelles du cadre de développement .NET de Microsoft, ou des interprètes d’autres langages de programmation compilés par code intermédiaire tels que Java. Dans ce chapitre, nous examinons en détail l’EVM, y compris son jeu d’instructions, sa structure et son fonctionnement, dans le contexte des mises à jour d’état d’Ethereum.

Qu’est-ce que l’EVM ?

L’EVM est la partie d’Ethereum qui gère le déploiement et l’exécution des contrats intelligents. Les transactions simples de transfert de valeur d’un EOA à un autre n’ont pas besoin de l’impliquer, pratiquement, mais tout le reste impliquera une mise à jour d’état calculée par l’EVM. À un niveau élevé, l’EVM fonctionnant sur la chaîne de blocs Ethereum peut être considéré comme un ordinateur mondial décentralisé contenant des millions d’objets exécutables, chacun avec son propre magasin de données permanent.

L’EVM est une machine d’état quasi–Turing-complète ; "quasi" car tous les processus d’exécution sont limités à un nombre fini d’étapes de calcul par la quantité de gaz disponible pour toute exécution de contrat intelligent donnée. En tant que tel, le problème d’arrêt est "résolu" (toutes les exécutions de programme s’arrêteront) et la situation où l’exécution pourrait (accidentellement ou par malveillance) s’exécuter indéfiniment, amenant ainsi la plate-forme Ethereum à s’arrêter dans son intégralité, est évitée.

L’EVM a une architecture basée sur la pile, stockant toutes les valeurs en mémoire sur une pile. Il fonctionne avec une taille de mot de 256 bits (principalement pour faciliter les opérations de hachage natif et de courbe elliptique) et possède plusieurs composants de données adressables :

  • Un code de programme ROM immuable, chargé avec le code intermédiaire du smart contract à exécuter

  • Une mémoire volatile, avec chaque emplacement explicitement initialisé à zéro

  • Un stockage permanent qui fait partie de l’état Ethereum, également initialisé à zéro

Il existe également un ensemble de variables d’environnement et de données disponibles pendant l’exécution. Nous les détaillerons plus loin dans ce chapitre.

L’architecture et le contexte d’exécution de la machine virtuelle Ethereum (EVM) montre l’architecture EVM et le contexte d’exécution.

L’architecture de la machine virtuelle Ethereum (EVM) et son contexte d’exécution
Figure 55. L’architecture et le contexte d’exécution de la machine virtuelle Ethereum (EVM)

Comparaison avec la technologie existante

Le terme "machine virtuelle" est souvent appliqué à la virtualisation d’un ordinateur réel, typiquement par un "hyperviseur" tel que VirtualBox ou QEMU, ou d’une instance entière du système d’exploitation, comme le KVM de Linux. Ceux-ci doivent fournir une abstraction logicielle, respectivement, du matériel réel, des appels système et des autres fonctionnalités du noyau.

L’EVM fonctionne dans un domaine beaucoup plus limité : c’est juste un moteur de calcul, et en tant que tel fournit une abstraction du calcul et du stockage, similaire à la spécification Java Virtual Machine (JVM), par exemple. D’un point de vue de haut niveau, la JVM est conçue pour fournir un environnement d’exécution indépendant du système d’exploitation ou du matériel hôte sous-jacent, permettant la compatibilité entre une grande variété de systèmes. Les langages de programmation de haut niveau tels que Java ou Scala (qui utilisent la JVM) ou C# (qui utilise .NET) sont compilés dans le jeu d’instructions code intermédiaire de leur machine virtuelle respective. De la même manière, l’EVM exécute son propre jeu d’instructions de code intermédiaire (décrit dans la section suivante), dans lequel sont compilés des langages de programmation de contrats intelligents de niveau supérieur tels que LLL, Serpent, Mutan ou Solidity.

L’EVM n’a donc aucune capacité de planification, car l’ordre d’exécution est organisé en externe - les clients Ethereum exécutent des transactions de bloc vérifiées pour déterminer quels contrats intelligents doivent être exécutés et dans quel ordre. En ce sens, l’ordinateur mondial Ethereum est mono-fil (d’exécution), comme JavaScript. L’EVM n’a pas non plus de gestion "d’interface système" ou de "support matériel" - il n’y a pas de machine physique avec laquelle s’interfacer. L’ordinateur mondial Ethereum est entièrement virtuel.

Le jeu d’instructions EVM (opérations de code intermédiaire)

Le jeu d’instructions EVM offre la plupart des opérations que l’on peut s’attendre, y compris :

  • Opérations arithmétiques et logiques au niveau du bit

  • Enquêtes sur le contexte d’exécution

  • Accès à la pile, à la mémoire et au stockage

  • Contrôler les opérations de flux

  • Journalisation, appels et autres opérateurs

En plus des opérations de code intermédiaire typiques, l’EVM a également accès aux informations de compte (par exemple, l’adresse et le solde) et aux informations de bloc (par exemple, le numéro de bloc et le prix actuel du gaz).

Commençons notre exploration de l’EVM plus en détail en examinant les opcodes disponibles et ce qu’ils font. Comme vous pouvez vous y attendre, tous les opérandes sont extraits de la pile et le résultat (le cas échéant) est souvent remis sur le dessus de la pile.

Note

Une liste complète des opcodes et leur coût de gaz correspondant peut être trouvée dans Opcodes Ethereum EVM et consommation de gaz.

Les opcodes disponibles peuvent être divisés dans les catégories suivantes :

Opérations arithmétiques

Instructions d’opcode arithmétique :

ADD        // Ajouter les deux premiers éléments de la pile
MUL        // Multiplier les deux premiers éléments de la pile
SUB        // Soustraire les deux premiers éléments de la pile
DIV        // Division entière
SDIV       // Division d'entier signé
MOD        // Opération modulo (reste)
SMOD       // Opération modulo signée
ADDMOD     // Addition modulo n'importe quel nombre
MULMOD     // Multiplication modulo n'importe quel nombre
EXP        // Opération exponentielle
SIGNEXTEND // Étend la longueur d'un entier signé complément à deux
SHA3       // Calcule le hachage Keccak-256 d'un bloc de mémoire

Notez que toute l’arithmétique est effectuée modulo 2256 (sauf indication contraire) et que la puissance zéro de zéro, 00, est considérée comme égale à 1.

Opérations de pile

Instructions de gestion de la pile, de la mémoire et du stockage :

POP        // Supprime l'élément du haut de la pile
MLOAD      // Charger un mot machine de la mémoire
MSTORE     // Enregistre un mot machine en mémoire
MSTORE8    // Enregistre un octet en mémoire
SLOAD      // Charger un mot machine depuis le stockage
SSTORE     // Enregistrer un mot machine dans le stockage
MSIZE      // Récupère la taille de la mémoire active en octets
PUSHx      // Place l'élément x octet sur la pile, où x peut être n'importe quel entier de
           // 1 à 32 (mot machine complet) inclus
DUPx       // Dupliquer le x-ième élément de la pile, où x peut être n'importe quel entier de
           // 1 à 16 inclus
SWAPx      // Échangez les 1er et (x+1)-ème éléments de la pile, où x peut être n'importe quel
           // entier de 1 à 16 inclus
Opérations de flux de processus

Instructions pour le flux de contrôle :

STOP     // Arrête l'exécution
JUMP     // Mettre le compteur de programme à n'importe quelle valeur
JUMPI    // Modifier conditionnellement le compteur de programme
PC       // Récupérer la valeur du compteur de programme (avant l'incrément
         // correspondant à cette instruction)
JUMPDEST // Marquer une destination valide pour les sauts
Opérations système

Opcodes pour le système exécutant le programme :

LOGx         // Ajouter un enregistrement de journal avec x sujets, où x est n'importe quel entier
             // de 0 à 4 inclus
CREATE       // Créer un nouveau compte avec le code associé
CALL         // Message-appel dans un autre compte, c'est-à-dire exécuter un autre
             // code du compte
CALLCODE     // Message-appel dans ce compte avec un autre
             // code du compte
RETURN       // Arrête l'exécution et renvoie les données de sortie
DELEGATECALL // Message-appel dans ce compte avec une alternative
             // code du compte, mais en conservant les valeurs actuelles pour
             // sender et value
STATICCALL   // Message-appel statique dans un compte
REVERT       // Arrête l'exécution, annule les changements d'état mais retourne
             // des données et le gaz restant
INVALID      // L'instruction invalide désignée
SELFDESTRUCT // Arrête l'exécution et enregistre le compte pour suppression
Opérations logiques

Opcodes pour les comparaisons et la logique au niveau du bit :

LT     // Comparaison inférieure à
GT     // Comparaison supérieure à
SLT    // Comparaison inférieure signée
SGT    // Comparaison supérieure signée
EQ     // Comparaison d'égalité
ISZERO // Opérateur NOT simple
AND    // Opération AND au niveau du bit
OR     // Opération OR au niveau du bit
XOR    // Opération XOR au niveau du bit
NOT    // Opération NOT au niveau du bit
BYTE   // Récupérer un seul octet d'un mot pleine largeur de 256 bits
Opérations environnementales

Opcodes traitant des informations sur l’environnement d’exécution :

GAZ            // Obtenir la quantité de gaz disponible (après la réduction pour
               // cette instruction)
ADRESSE        // Obtenir l'adresse du compte en cours d'exécution
SOLDE          // Obtenir le solde du compte d'un compte donné
ORIGIN         // Obtenir l'adresse de l'EOA qui a lancé cette
               // éxecution EVM
APPELANT       // Obtenir l'adresse de l'appelant immédiatement responsable
               // pour cette exécution
CALLVALUE      // Obtenir le montant en ether déposé par l'appelant responsable
               // pour cette exécution
CALLDATALOAD   // Récupère les données d'entrée envoyées par l'appelant responsable de
               // cette exécution
CALLDATASIZE   // Récupère la taille des données d'entrée
CALLDATACOPY   // Copie les données d'entrée dans la mémoire
CODESIZE       // Récupère la taille du code en cours d'exécution dans l'environnement actuel
CODECOPY       // Copiez le code en cours d'exécution dans l'environnement actuel vers
               // la mémoire
GASPRICE       // Obtenir le prix du gaz spécifié par la
               // transaction d'origine
EXTCODESIZE    // Récupère la taille du code de n'importe quel compte
EXTCODECOPY    // Copie le code de n'importe quel compte en mémoire
RETURNDATASIZE // Récupère la taille des données de sortie de l'appel précédent
               // dans l'environnement actuel
RETURNDATACOPY // Copie la sortie des données de l'appel précédent dans la mémoire
Opérations sur les blocs

Opcodes pour accéder aux informations sur le bloc actuel :

BLOCKHASH  // Obtenir le hachage de l'un des 256 derniers
           // blocs complétés
COINBASE   // Obtenir l'adresse du bénéficiaire du bloc pour la récompense du bloc
TIMESTAMP  // Récupère l'horodatage du bloc
NUMBER     // Récupère le numéro du bloc
DIFFICULTY // Obtenir la difficulté du bloc
GASLIMIT   // Récupère la limite de gaz du bloc

État Ethereum

Le travail de l’EVM consiste à mettre à jour l’état Ethereum en calculant des transitions d’état valides à la suite de l’exécution du code de contrat intelligent, tel que défini par le protocole Ethereum. Cet aspect conduit à la description d’Ethereum comme une machine à états basée sur les transactions, qui reflète le fait que des acteurs externes (c’est-à-dire les titulaires de compte et les mineurs) initient des transitions d’état en créant, acceptant et ordonnant des transactions. Il est utile à ce stade de considérer ce qui constitue l’état Ethereum.

Au niveau supérieur, nous avons l’état mondial d’Ethereum. L’état mondial est un mappage des adresses Ethereum (valeurs 160 bits) vers des comptes. Au niveau inférieur, chaque adresse Ethereum représente un compte comprenant un solde d’ether (stocké comme le nombre de wei détenu par le compte), un nonce (représentant le nombre de transactions envoyées avec succès depuis ce compte s’il s’agit d’un EOA, ou le nombre de contrats créés par celui-ci s’il s’agit d’un compte contractuel), le stockage du compte (qui est une donnée permanente store, utilisé uniquement par les contrats intelligents) et le code programme du compte (encore une fois, uniquement si le compte est un compte de contrat intelligent). Un EOA n’aura toujours aucun code et un stockage vide.

Lorsqu’une transaction entraîne l’exécution d’un code de contrat intelligent, une EVM est instanciée avec toutes les informations requises par rapport au bloc en cours de création et à la transaction spécifique en cours de traitement. En particulier, la ROM de code de programme de l’EVM est chargée avec le code du compte de contrat appelé, le compteur de programme est mis à zéro, la mémoire est chargée à partir de la mémoire du compte de contrat, la mémoire est mise à zéro, et tout le bloc et les variables d’environnement sont définies. Une variable clé est l’approvisionnement en gaz pour cette exécution, qui est fixé à la quantité de gaz payée par l’expéditeur au début de la transaction (voir Gaz pour plus de détails). Au fur et à mesure de l’exécution du code, l’alimentation en gaz est réduite en fonction du coût en gaz des opérations exécutées. Si, à un moment quelconque, l’alimentation en gaz est réduite à zéro, nous obtenons une exception "Out of Gas" (OOG) ; l’exécution s’arrête immédiatement et la transaction est abandonnée. Aucune modification de l’état d’Ethereum n’est appliquée, à l’exception du nonce de l’expéditeur qui est incrémenté et de son solde d’ether qui diminue pour payer le bénéficiaire du bloc pour les ressources utilisées pour exécuter le code jusqu’au point d’arrêt. À ce stade, vous pouvez penser à l’EVM s’exécutant sur une copie en bac à sable de l’état mondial d’Ethereum, cette version en bac à sable étant complètement supprimée si l’exécution ne peut pas se terminer pour une raison quelconque. Cependant, si l’exécution réussit, l’état réel est mis à jour pour correspondre à la version en bac à sable, y compris toute modification des données de stockage du contrat appelé, tout nouveau contrat créé et tout transfert de solde d’ether initié.

Notez que, comme un contrat intelligent peut lui-même initier efficacement des transactions, l’exécution de code est un processus récursif. Un contrat peut appeler d’autres contrats, chaque appel entraînant l’instanciation d’un autre EVM autour de la nouvelle cible de l’appel. Chaque instanciation a son état mondial de bac à sable initialisé à partir du bac à sable de l’EVM au niveau supérieur. Chaque instanciation reçoit également une quantité spécifiée de gaz pour son alimentation en gaz (n’excédant pas la quantité de gaz restant dans le niveau supérieur, bien sûr), et peut donc elle-même s’arrêter à une exception près en raison du manque de gaz pour terminer son exécution . Encore une fois, dans de tels cas, l’état du bac à sable est ignoré et l’exécution revient à l’EVM au niveau supérieur.

Compilation de Solidity en code intermédiaire EVM

La compilation un fichier source Solidity en code intermédiaire EVM peut être réalisé via plusieurs méthodes. Dans Les bases d’Ethereum nous avons utilisé le compilateur Remix en ligne. Dans ce chapitre, nous utiliserons l’exécutable solc en ligne de commande. Pour une liste d’options, exécutez la commande suivante :

$ solc --help

La génération du flux d’opcode brut d’un fichier source Solidity est facilement réalisée avec l’option de ligne de commande --opcodes. Ce flux d’opcode laisse de côté certaines informations (l’option --asm produit les informations complètes), mais c’est suffisant pour cette discussion. Par exemple, la compilation d’un exemple de fichier Solidity, Example.sol, et l’envoi de la sortie de l’opcode dans un répertoire nommé BytecodeDir s’effectuent avec la commande suivante :

$ solc -o BytecodeDir --opcodes Example.sol

ou alors:

$ solc -o BytecodeDir --asm Example.sol

La commande suivante produira le code intermédiaire binaire pour notre exemple de programme :

$ solc -o BytecodeDir --bin Example.sol

Les fichiers d’opcode de sortie générés dépendront des contrats spécifiques contenus dans le fichier source Solidity. Notre fichier Solidity simple Example.sol n’a qu’un seul contrat, nommé example :

pragma solidity ^0.4.19;

contract example {

  address contractOwner;

  function example() {
    contractOwner = msg.sender;
  }
}

Comme vous pouvez le voir, ce contrat ne contient qu’une seule variable d’état persistante, qui est définie comme l’adresse du dernier compte pour exécuter ce contrat.

Si vous regardez dans le répertoire BytecodeDir, vous verrez le fichier d’opcode example.opcode, qui contient les instructions d’opcode EVM du contrat example. L’ouverture du fichier example.opcode dans un éditeur de texte affichera ce qui suit :

PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xE JUMPI PUSH1 0x0 DUP1
REVERT JUMPDEST CALLER PUSH1 0x0 DUP1 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH20
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MUL NOT AND SWAP1 DUP4 PUSH20
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND MUL OR SWAP1 SSTORE POP PUSH1
0x35 DUP1 PUSH1 0x5B PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1
0x40 MSTORE PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 JUMP
0xb9 SWAP14 0xcb 0x1e 0xdd RETURNDATACOPY 0xec 0xe0 0x1f 0x27 0xc9 PUSH5
0x9C5ABCC14A NUMBER 0x5e INVALID EXTCODESIZE 0xdb 0xcf EXTCODESIZE 0x27
EXTCODESIZE 0xe2 0xb8 SWAP10 0xed 0x

Compiler l’exemple avec l’option --asm produit un fichier nommé example.evm dans notre répertoire BytecodeDir. Celui-ci contient une description de niveau légèrement supérieur des instructions du code intermédiaire EVM, ainsi que quelques annotations utiles :

/* "Example.sol":26:132  contract example {... */
  mstore(0x40, 0x60)
    /* "Example.sol":74:130  function example() {... */
  jumpi(tag_1, iszero(callvalue))
  0x0
  dup1
  revert
tag_1:
    /* "Example.sol":115:125  msg.sender */
  caller
    /* "Example.sol":99:112  contractOwner */
  0x0
  dup1
    /* "Example.sol":99:125  contractOwner = msg.sender */
  0x100
  exp
  dup2
  sload
  dup2
  0xffffffffffffffffffffffffffffffffffffffff
  mul
  not
  and
  swap1
  dup4
  0xffffffffffffffffffffffffffffffffffffffff
  and
  mul
  or
  swap1
  sstore
  pop
    /* "Example.sol":26:132  contract example {... */
  dataSize(sub_0)
  dup1
  dataOffset(sub_0)
  0x0
  codecopy
  0x0
  return
stop

sub_0: assembly {
        /* "Example.sol":26:132  contract example {... */
      mstore(0x40, 0x60)
      0x0
      dup1
      revert

    auxdata: 0xa165627a7a7230582056b99dcb1edd3eece01f27c9649c5abcc14a435efe3b...
}

The --bin-runtime option produces the machine-readable hexadecimal bytecode:

60606040523415600e57600080fd5b336000806101000a81548173
ffffffffffffffffffffffffffffffffffffffff
021916908373
ffffffffffffffffffffffffffffffffffffffff
160217905550603580605b6000396000f3006060604052600080fd00a165627a7a7230582056b...

Vous pouvez enquêter sur ce qui se passe ici en détail en utilisant la liste des opcodes donnée dans Le jeu d’instructions EVM (opérations de code intermédiaire). Cependant, c’est toute une tâche, alors commençons par examiner les quatre premières instructions :

PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE

Ici, nous avons PUSH1 suivi d’un octet brut de valeur 0x60. Cette instruction EVM prend l’octet unique suivant l’opcode dans le code du programme (en tant que valeur littérale) et le pousse sur la pile. Il est possible de pousser des valeurs de taille jusqu’à 32 octets sur la pile, comme dans :

PUSH32 0x436f6e67726174756c6174696f6e732120536f6f6e20746f206d617374657221

Le deuxième opcode PUSH1 de example.opcode stocke 0x40 en haut de la pile (en poussant le 0x60 déjà présent d’un emplacement vers le bas).

Vient ensuite MSTORE, qui est une opération de stockage en mémoire qui enregistre une valeur dans la mémoire de l’EVM. Il prend deux arguments et, comme la plupart des opérations EVM, les obtient de la pile. Pour chaque argument, la pile est “poppée” (popped) c’est-à-dire que la valeur supérieure de la pile est retirée et toutes les autres valeurs de la pile sont décalées d’une position. Le premier argument de MSTORE est l’adresse du mot en mémoire où sera mise la valeur à sauvegarder. Pour ce programme, nous avons 0x40 en haut de la pile, de sorte qu’il est retiré de la pile et utilisé comme adresse mémoire. Le deuxième argument est la valeur à sauvegarder, qui est 0x60 ici. Après l’exécution de l’opération MSTORE, notre pile est à nouveau vide, mais nous avons la valeur 0x60 (96 en décimal) à l’emplacement mémoire 0x40.

L’opcode suivant est CALLVALUE, qui est un opcode environnemental qui pousse vers le haut de la pile la quantité d’ether (mesurée en wei) envoyée avec l’appel de message qui a initié cette exécution.

Nous pourrions continuer à parcourir ce programme de cette manière jusqu’à ce que nous ayons une compréhension complète des changements d’état de bas niveau que ce code effectue, mais cela ne nous aiderait pas à ce stade. Nous y reviendrons plus tard dans le chapitre.

Code de déploiement du contrat

Il existe une différence importante mais subtile entre le code utilisé lors de la création et du déploiement d’un nouveau contrat sur la plateforme Ethereum et le code du contrat lui-même. Afin de créer un nouveau contrat, une transaction spéciale est nécessaire dont le champ to est défini sur l’adresse spéciale 0x0 et son champ data défini sur le code d’initiation du contrat. Lorsqu’une telle transaction de création de contrat est traitée, le code du nouveau compte de contrat n’est pas le code du champ données de la transaction. Au lieu de cela, un EVM est instancié avec le code dans le champ data de la transaction chargée dans sa ROM de code de programme, puis la sortie de l’exécution de ce code de déploiement est prise comme code pour le nouveau compte de contrat. C’est ainsi que de nouveaux contrats peuvent être initialisés par programme en utilisant l’état mondial Ethereum au moment du déploiement, en définissant des valeurs dans le stockage du contrat et même en envoyant de l’ether ou en créant de nouveaux contrats.

Lors de la compilation d’un contrat hors ligne, par exemple, en utilisant solc sur la ligne de commande, vous pouvez soit obtenir le code intermédiaire de déploiement ou le code intermédiaire d’exécution.

Le code intermédiaire de déploiement est utilisé pour chaque aspect de l’initialisation d’un nouveau compte de contrat, y compris le code intermédiaire qui finira par être exécuté lorsque les transactions appelleront ce nouveau contrat (c’est-à-dire le code intermédiaire d’exécution) et le code pour tout initialiser en fonction du constructeur du contrat.

Le code intermédiaire d’exécution, en revanche, est exactement le code intermédiaire qui finit par être exécuté lorsque le nouveau contrat est appelé, et rien de plus ; il n’inclut pas le code intermédiaire nécessaire pour initialiser le contrat lors du déploiement.

Prenons comme exemple le simple contrat Faucet.sol que nous avons créé précédemment :

// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.4.19;

// Notre premier contrat est un robinet !
contract Faucet {

  // Donnez de l'ether à quiconque demande
  function withdraw(uint withdraw_amount) public {

      // Limiter le montant du retrait
      require(withdraw_amount <= 100000000000000000);

      // Envoie le montant à l'adresse qui l'a demandé
      msg.sender.transfer(withdraw_amount);
    }

  // Accepte tout montant entrant
  function () external payable {}

}

Pour obtenir le code intermédiaire de déploiement, nous exécuterions solc --bin Faucet.sol. Si nous voulions plutôt uniquement le code intermédiaire d’exécution, nous exécuterions solc --bin-runtime Faucet.sol.

Si vous comparez la sortie de ces commandes, vous verrez que le code intermédiaire d’exécution est un sous-ensemble du code intermédiaire de déploiement. En d’autres termes, le code intermédiaire d’exécution est entièrement contenu dans le code intermédiaire de déploiement.

Désassemblage du code intermédiaire

Le désassemblage du code intermédiaire EVM est un excellent moyen de comprendre comment le haut niveau de Solidity agit dans l’EVM. Il existe quelques désassembleurs que vous pouvez utiliser pour cela :

  • Porosity est un décompilateur à source libre populaire.

  • Ethersplay est un plug-in EVM pour Binary Ninja, un désassembleur.

  • IDA-Evm est un plugin EVM pour IDA, un autre désassembleur.

Dans cette section, nous utiliserons le plug-in Ethersplay pour Binary Ninja et pour commencer Démontage du code intermédiaire d’exécution du robinet. Après avoir obtenu le code intermédiaire d’exécution de Faucet.sol, nous pouvons l’introduire dans Binary Ninja (après avoir chargé le plug-in Ethersplay) pour voir à quoi ressemblent les instructions EVM.

Le code intermédiaire d’exécution de Faucet.sol a été désassemblé
Figure 56. Démontage du code intermédiaire d’exécution du robinet

Lorsque vous envoyez une transaction à un contrat intelligent compatible ABI (ce que vous pouvez supposer que tous les contrats le sont), la transaction interagit d’abord avec le répartiteur de ce contrat intelligent. Le répartiteur lit le champ data de la transaction et envoie la partie pertinente à la fonction appropriée. Nous pouvons voir un exemple de répartiteur au début de notre code intermédiaire d’exécution Faucet.sol désassemblé. Après l’instruction familière MSTORE, nous voyons les instructions suivantes :

PUSH1 0x4
CALLDATASIZE
LT
PUSH1 0x3f
JUMPI

Comme nous l’avons vu, PUSH1 0x4 place 0x4 en haut de la pile, qui est autrement vide. CALLDATASIZE obtient la taille en octets des données envoyées avec la transaction (appelée calldata) et pousse ce nombre sur la pile. Une fois ces opérations exécutées, la pile ressemble à ceci :

Pile

<length of calldata from tx>

0x4

Cette instruction suivante est LT, abréviation de "plus petit que". L’instruction LT vérifie si l’élément du haut de la pile est inférieur à l’élément suivant de la pile. Dans notre cas, il vérifie si le résultat de CALLDATASIZE est inférieur à 4 octets.

Pourquoi l’EVM vérifie-t-il que les données d’appel de la transaction font au moins 4 octets ? En raison du fonctionnement des identificateurs de fonction. Chaque fonction est identifiée par les 4 premiers octets de son hachage Keccak-256. En plaçant le nom de la fonction et les arguments qu’elle prend dans une fonction de hachage keccak256, nous pouvons déduire son identifiant de fonction. Dans notre cas, nous avons :

keccak256("withdraw(uint256)") = 0x2e1a7d4d...

Ainsi, l’identifiant de fonction pour la fonction withdraw(uint256) est 0x2e1a7d4d, puisqu’il s’agit des 4 premiers octets du hachage résultant. Un identifiant de fonction a toujours une longueur de 4 octets, donc si le champ entier data de la transaction envoyée au contrat est inférieur à 4 octets, alors il n’y a pas de fonction avec laquelle la transaction pourrait éventuellement communiquer, à moins qu’une fonction de secours ne soit définie. Parce que nous avons implémenté une telle fonction de secours dans Faucet.sol, l’EVM saute à cette fonction lorsque la longueur des données d’appel est inférieure à 4 octets.

LT retire les deux premières valeurs de la pile et, si le champ data de la transaction est inférieur à 4 octets, il y pousse un 1. Sinon, il pousse un 0. Dans notre exemple, supposons que le champ data de la transaction envoyée à notre contrat était inférieur à 4 octets.

L’instruction PUSH1 0x3f pousse l’octet 0x3f sur la pile. Après cette instruction, la pile ressemble à ceci :

Pile

0x3f

1

L’instruction suivante est JUMPI, qui signifie "sauter si". Cela fonctionne comme ceci :

jumpi(label, cond) // Aller à "label" si "cond" est vrai

Dans notre cas, label est 0x3f, où réside notre fonction de secours dans notre contrat intelligent. L’argument cond est 1, qui était le résultat de l’instruction LT précédente. Pour mettre toute cette séquence en mots, le contrat passe à la fonction de repli si les données de transaction sont inférieures à 4 octets.

À 0x3f, seule une instruction STOP suit, car bien que nous ayons déclaré une fonction de repli, nous l’avons laissée vide. Comme vous pouvez le voir dans Instruction JUMPI menant à la fonction de repli, si nous n’avions pas implémenté une fonction de secours, le contrat lèverait une exception à la place.

Instruction JUMPI menant à la fonction de repli
Figure 57. Instruction JUMPI menant à la fonction de repli

Examinons le bloc central du répartiteur. En supposant que nous recevions des données d’appel d’une longueur supérieure à 4 octets, l’instruction JUMPI ne passerait pas à la fonction de repli. Au lieu de cela, l’exécution du code procéderait aux instructions suivantes :

PUSH1 0x0
CALLDATALOAD
PUSH29 0x1000000...
SWAP1
DIV
PUSH4 0xffffffff
AND
DUP1
PUSH4 0x2e1a7d4d
EQ
PUSH1 0x41
JUMPI

PUSH1 0x0 pousse 0 sur la pile, qui est maintenant à nouveau vide. CALLDATALOAD accepte comme argument un index dans les données d’appel envoyées au contrat intelligent et lit 32 octets à partir de cet index, comme ceci :

calldataload(p) //charge 32 octets de données d'appel à partir de la position d'octet p

Puisque 0 était l’index qui lui a été transmis par la commande PUSH1 0x0, CALLDATALOAD lit 32 octets de données d’appel à partir de l’octet 0, puis le pousse vers le haut de la pile (après avoir extrait le 0x0 d’origine). Après l’instruction PUSH29 0x1000000…​, la pile est alors :

Pile

0x1000000…​ (longueur de 29 octets)

<32 bytes of calldata starting at byte 0>

SWAP1 commute l’élément supérieur sur la pile avec le i-ème élément après lui. Dans ce cas, il échange 0x1000000…​ avec le calldata. La nouvelle pile est :

Pile

<32 bytes of calldata starting at byte 0>

0x1000000…​ (longueur de 29 octets)

L’instruction suivante est DIV, qui fonctionne comme suit :

div(x, y) // division entière x / y

Dans ce cas, x = 32 octets de calldata commençant à l’octet 0, et y = 0x100000000…​ (29 octets au total). Pouvez-vous penser à la raison pour laquelle le répartiteur fait la division ? Voici un indice : nous avons lu 32 octets de calldata plus tôt, en commençant à l’index 0. Les 4 premiers octets de cette calldata sont l’identifiant de la fonction.

Le 0x100000000…​ que nous avons poussé plus tôt fait 29 octets de long, composé d’un 1 au début, suivi de tous les 0. Diviser nos 32 octets de données d’appel par cette valeur ne nous laissera que les 4 octets les plus élevés de notre charge de données d’appel, à partir de l’index 0. Ces 4 octets - les 4 premiers octets des données d’appel commençant à l’index 0 - sont l’identifiant de la fonction, et cela est la façon dont l’EVM extrait ce champ.

Si cette partie n’est pas claire pour vous, pensez-y comme ceci : en base 10, 1234000 / 1000 = 1234. En base 16, ce n’est pas différent. Au lieu que chaque lieu soit un multiple de 10, c’est un multiple de 16. Tout comme la division par 103 (1000) dans notre petit exemple ne conservait que les chiffres les plus élevés, divisant notre valeur de base 16 de 32 octets par 1629 fait de même.

Le résultat du DIV (l’identifiant de la fonction) est poussé sur la pile, et notre pile est maintenant :

Pile

<function identifier sent in data>

Étant donné que les instructions PUSH4 0xffffffff et AND sont redondantes, nous pouvons les ignorer complètement, car la pile restera la même après leur exécution. L’instruction DUP1 duplique le premier élément de la pile, qui est l’identificateur de la fonction. L’instruction suivante, PUSH4 0x2e1a7d4d, pousse l’identificateur de fonction précalculé de la fonction withdraw (uint256) sur la pile. La pile est maintenant :

Pile

0x2e1a7d4d

<function identifier sent in data>

<function identifier sent in data>

L’instruction suivante, EQ, extrait les deux premiers éléments de la pile et les compare. C’est là que le répartiteur fait son travail principal : il compare si l’identifiant de la fonction envoyé dans le champ msg.data de la transaction correspond à celui de withdraw (uint256). S’ils sont égaux, EQ pousse 1 sur la pile, qui sera finalement utilisé pour passer à la fonction de retrait. Sinon, EQ pousse 0 sur la pile.

En supposant que la transaction envoyée à notre contrat a bien commencé avec l’identifiant de fonction pour withdraw(uint256), notre pile est devenue :

Pile

1

<function identifier sent in data>(maintenant connu pour être 0x2e1a7d4d)

Ensuite, nous avons PUSH1 0x41, qui est l’adresse à laquelle la fonction withdraw(uint256) vit dans le contrat. Après cette instruction, la pile ressemble à ceci :

Pile

0x41

1

identifiant de la fonction envoyé dans msg.data

L’instruction JUMPI est la suivante, et elle accepte à nouveau les deux premiers éléments de la pile comme arguments. Dans ce cas, nous avons jumpi(0x41, 1), qui indique à l’EVM d’exécuter le saut vers l’emplacement de la fonction withdraw(uint256), et l’exécution du code de cette fonction peut continuer.

Complétude de Turing et Gaz

Comme nous l’avons déjà évoqué, en termes simples, un système ou langage de programmation est Turing complet s’il peut exécuter n’importe quel programme. Cette capacité, cependant, s’accompagne d’une mise en garde très importante : certains programmes prennent une éternité à s’exécuter. Un aspect important de ceci est que nous ne pouvons pas dire, juste en regardant un programme, s’il prendra une éternité ou non à s’exécuter. Nous devons en fait suivre l’exécution du programme et attendre qu’il se termine pour le savoir. Bien sûr, si l’exécution prend une éternité, nous devrons attendre une éternité pour le savoir. C’est ce qu’on appelle le problème d’arrêt et ce serait un énorme problème pour Ethereum s’il n’était pas résolu.

En raison du problème d’arrêt, l’ordinateur du monde Ethereum risque de se voir demander d’exécuter un programme qui ne s’arrête jamais. Cela pourrait être par accident ou par malveillance. Nous avons discuté du fait qu’Ethereum agit comme une machine à un seul fil d’exécution, sans aucun planificateur, et donc s’il était bloqué dans une boucle infinie, cela signifierait qu’il deviendrait inutilisable.

Cependant, avec le gaz, il existe une solution : si après qu’une quantité maximale de calculs a été effectuée, l’exécution n’est pas terminée, l’exécution du programme est stoppée par l’EVM. Cela fait de l’EVM une machine quasi–Turing-complète : elle peut exécuter n’importe quel programme que vous lui introduisez, mais seulement si le programme se termine dans un certain nombre de calculs. Cette limite n’est pas fixée dans Ethereum - vous pouvez payer pour l’augmenter jusqu’à un maximum (appelé "limite de gaz de bloc"), et tout le monde peut accepter d’augmenter ce maximum au fil du temps. Néanmoins, à tout moment, il y a une limite en place, et les transactions qui consomment trop de gaz lors de l’exécution sont stoppées.

Dans les sections suivantes, nous examinerons le gaz et examinerons en détail son fonctionnement.

Gaz

Le gaz est l’unité d’Ethereum pour mesurer les ressources de calcul et de stockage nécessaires pour effectuer des actions sur la chaîne de blocs Ethereum. Contrairement à Bitcoin, dont les frais de transaction ne tiennent compte que de la taille d’une transaction en kilo-octets, Ethereum doit tenir compte de chaque étape de calcul effectuée par les transactions et l’exécution du code de contrat intelligent.

Chaque opération effectuée par une transaction ou un contrat coûte une quantité fixe de gaz. Quelques exemples, tirés du Ethereum Yellow Paper :

  • L’ajout de deux nombres coûte 3 gaz

  • Le calcul d’un hachage Keccak-256 coûte 30 gaz + 6 gaz pour chaque 256 bits de données hachés

  • L’envoi d’une transaction coûte 21 000 gaz

Le gaz est un composant crucial d’Ethereum et joue un double rôle : comme tampon entre le prix (volatile) d’Ethereum et la récompense des mineurs pour le travail qu’ils font, et comme défense contre les attaques par déni de service. Pour éviter les boucles infinies accidentelles ou malveillantes ou tout autre gaspillage de calcul dans le réseau, l’initiateur de chaque transaction est tenu de fixer une limite à la quantité de calcul qu’il est prêt à payer. Le système de gaz décourage ainsi les attaquants d’envoyer des transactions de "spam", car ils doivent payer proportionnellement pour les ressources de calcul, de bande passante et de stockage qu’ils consomment.

Comptabilisation du gaz pendant l’exécution

Lorsqu’une EVM est nécessaire pour effectuer une transaction, en premier lieu il reçoit un approvisionnement en gaz égal au montant spécifié par la limite de gaz dans la transaction. Chaque opcode exécuté a un coût en gaz, et donc l’approvisionnement en gaz de l’EVM est réduit au fur et à mesure que l’EVM avance dans le programme. Avant chaque opération, l’EVM vérifie qu’il y a suffisamment de gaz pour payer l’exécution de l’opération. S’il n’y a pas assez de gaz, l’exécution est interrompue et la transaction est annulée.

Si l’EVM atteint la fin de l’exécution avec succès, sans manquer de gaz, le coût du gaz utilisé est payé au mineur sous forme de frais de transaction, converti en ether sur la base du prix du gaz spécifié dans la transaction :

frais de mineur = coût du gaz * prix du gaz

Le gaz restant dans l’approvisionnement en gaz est remboursé à l’expéditeur, à nouveau converti en ether sur la base du prix du gaz spécifié dans la transaction :

gaz restant = limite de gaz - coût du gaz
ether remboursé = gaz restant * prix du gaz

Si la transaction « manque de gaz » pendant l’exécution, l’opération est immédiatement terminée, ce qui déclenche une exception « à court de gaz » (OOG). La transaction est annulée et toutes les modifications apportées à l’état sont annulées.

Bien que la transaction ait échoué, l’expéditeur se verra facturer des frais de transaction, car les mineurs ont déjà effectué le travail de calcul jusqu’à ce point et doivent être indemnisés pour cela.

Considérations sur la comptabilisation du gaz

Les coûts relatifs du gaz des différentes opérations pouvant être effectuées par l’EVM ont ont été soigneusement choisis pour protéger au mieux la chaîne de blocs Ethereum contre les attaques. Vous pouvez voir un tableau détaillé des coûts de gaz pour différents opcodes EVM dans Opcodes EVM et coût du gaz.

Les opérations plus intensives en calcul coûtent plus de gaz. Par exemple, l’exécution de la fonction SHA3 est 10 fois plus coûteuse (30 gaz) que l’opération ADD (3 gaz). Plus important encore, certaines opérations, telles que EXP, nécessitent un paiement supplémentaire en fonction de la taille de l’opérande. L’utilisation de la mémoire EVM et le stockage des données dans le stockage en chaîne d’un contrat entraînent également un coût en gaz.

L’importance de faire correspondre le coût du gaz au coût réel des ressources a été démontrée en 2016 lorsqu’un attaquant a découvert et exploité une inadéquation des coûts. L’attaque a généré des transactions très coûteuses en calcul et a presque paralysé le réseau principal Ethereum. Cette inadéquation a été résolue par un hard fork (nom de code "Tangerine Whistle") qui a modifié les coûts relatifs du gaz.

Coût du gaz versus prix du gaz

Alors que le coût du gaz est une mesure de calcul et de stockage utilisée dans l’EVM, le gaz lui-même a également un prix mesuré en ether. Lors d’une transaction, l’expéditeur spécifie le prix du gaz qu’il est prêt à payer (en ether) pour chaque unité de gaz, permettant au marché de décider de la relation entre le prix de l’ether et le coût des opérations informatiques (mesuré en gaz) :

frais de transaction = total de gaz utilisé * prix du gaz payé (en ether)

Lors de la construction d’un nouveau bloc, les mineurs du réseau Ethereum peuvent choisir parmi les transactions en attente en sélectionnant celles qui proposent de payer un prix du gaz plus élevé. Offrir un prix du gaz plus élevé incitera donc les mineurs à inclure votre transaction et à la faire confirmer plus rapidement.

En pratique, l’expéditeur d’une transaction fixera une limite de gaz supérieure ou égale à la quantité de gaz qu’il s’attend à utiliser. Si la limite de gaz est fixée à une valeur supérieure à la quantité de gaz consommée, l’expéditeur recevra un remboursement du montant excédentaire, car les mineurs ne sont indemnisés que pour le travail qu’ils effectuent réellement.

Il est important d’être clair sur la distinction entre le coût du gaz et le prix du gaz. Récapituler:

  • Le coût du gaz est le nombre d’unités de gaz nécessaires pour effectuer une opération particulière.

  • Le prix du gaz est la quantité d’ether que vous êtes prêt à payer par unité de gaz lorsque vous envoyez votre transaction au réseau Ethereum.

Tip

Alors que le gaz a un prix, il ne peut pas être "possédé" ni "dépensé". Le gaz n’existe qu’à l’intérieur de l’EVM, en tant que décompte de la quantité de travail de calcul effectué. L’expéditeur se voit facturer des frais de transaction en ether, qui sont ensuite convertis en gaz pour la comptabilité EVM, puis de nouveau en ether en tant que frais de transaction payés aux mineurs.

Coûts de gaz négatifs

Ethereum encourage la suppression des variables et des comptes de stockage utilisés en remboursant une partie du gaz utilisé lors de l’exécution du contrat.

Il y a deux opérations dans l’EVM avec des coûts de gaz négatifs :

  • La suppression d’un contrat (SELFDESTRUCT)vaut un remboursement de 24 000 gaz.

  • Changer une adresse de stockage d’une valeur différente de zéro à zéro (SSTORE[x] = 0)vaut un remboursement de 15 000 gaz.

Pour éviter l’exploitation du mécanisme de remboursement, le remboursement maximum pour une transaction est fixé à la moitié de la quantité totale de gaz consommée (arrondie à l’inférieur).

Limite de gaz du bloc

La limite de gaz de bloc est la quantité maximale de gaz pouvant être consommée par toutes les transactions d’un bloc et limite le nombre de transactions pouvant tenir dans un bloc.

Par exemple, supposons que nous ayons 5 transactions dont les limites de gaz ont été fixées à 30 000, 30 000, 40 000, 50 000 et 50 000. Si la limite de gaz du bloc est de 180 000, alors quatre de ces transactions peuvent tenir dans un bloc, tandis que la cinquième devra attendre un futur bloc. Comme indiqué précédemment, les mineurs décident des transactions à inclure dans un bloc. Différents mineurs sont susceptibles de sélectionner différentes combinaisons, principalement parce qu’ils reçoivent des transactions du réseau dans un ordre différent.

Si un mineur essaie d’inclure une transaction qui nécessite plus de gaz que la limite actuelle de gaz du bloc, le bloc sera rejeté par le réseau. La plupart des clients Ethereum vous empêcheront d’émettre une telle transaction en donnant un avertissement du type "la transaction dépasse la limite de gaz du bloc". La limite de gaz de bloc sur le réseau principal Ethereum est de 8 millions de gaz au moment de la rédaction selon https://etherscan.io, ce qui signifie qu’environ 380 transactions de base (chacune consommant 21 000 gaz) pourraient tenir dans un bloc.

Qui décide de la limite de gaz du bloc ?

Les mineurs du réseau décident collectivement de la limite de gaz du bloc. Les personnes qui souhaitent exploiter sur le réseau Ethereum utilisent un programme de minage, tel qu’Ethminer, qui se connecte à un client Geth ou Parity Ethereum. Le protocole Ethereum a un mécanisme intégré où les mineurs peuvent voter sur la limite de gaz afin que la capacité puisse être augmentée ou diminuée dans les blocs suivants. Le mineur d’un bloc peut voter pour ajuster la limite de gaz du bloc d’un facteur de 1/1 024 (0,0976 %) dans les deux sens. Le résultat est une taille de bloc ajustable en fonction des besoins du réseau à ce moment-là. Ce mécanisme est couplé à une stratégie minière par défaut où les mineurs votent sur une limite de gaz d’au moins 4,7 millions de gaz, mais qui cible une valeur de 150 % de la moyenne de la consommation totale récente de gaz par bloc (en utilisant un déplacement exponentiel de 1 024 blocs en moyenne).

Conclusion

Dans ce chapitre, nous avons exploré la machine virtuelle Ethereum, en retraçant l’exécution de divers contrats intelligents et en examinant comment l’EVM exécute le code intermédiaire. Nous avons également examiné le gaz, le mécanisme de comptabilité de l’EVM, et avons vu comment il résout le problème d’arrêt et protège Ethereum des attaques par déni de service. Ensuite, dans Consensus, nous examinerons le mécanisme utilisé par Ethereum pour parvenir à un consensus décentralisé.

Consensus

Bannière Amazon du livre Maîtriser Ethereum

Tout au long de ce livre, nous avons parlé de "règles de consensus" - les règles que tout le monde doit accepter pour que le système fonctionne dans un environnement décentralisé. En informatique, le terme consensus est antérieur aux chaînes de blocs et est lié au problème plus large de la synchronisation de l’état dans les systèmes distribués, de sorte que les différents participants d’un système distribué s’accordent tous (éventuellement) sur un seul état à l’échelle du système. C’est ce qu’on appelle « parvenir à un consensus ».

En ce qui concerne la fonction principale de tenue et de vérification décentralisées des dossiers, il peut devenir problématique de se fier uniquement à la confiance pour s’assurer que les informations dérivées des mises à jour de l’État sont correctes. Ce défi assez général est particulièrement prononcé dans les réseaux décentralisés car il n’y a pas d’entité centrale pour décider de ce qui est vrai. L’absence d’une entité décisionnelle centrale est l’un des principaux attraits des plateformes chaîne de blocs, en raison de la capacité qui en résulte à résister à la censure et au manque de dépendance vis-à-vis de l’autorité pour obtenir l’autorisation d’accéder aux informations. Cependant, ces avantages ont un coût : sans arbitre de confiance, tout désaccord, tromperie ou différence doit être concilié par d’autres moyens. Les algorithmes de consensus sont le mécanisme utilisé pour concilier sécurité et décentralisation.

Dans les chaînes de blocs, le consensus est une propriété essentielle du système. Bref, il y a de l’argent en jeu ! Ainsi, dans le contexte des chaînes de blocs, le consensus consiste à pouvoir arriver à un état commun, tout en maintenant la décentralisation. En d’autres termes, le consensus vise à produire un système de règles strictes sans dirigeants. Il n’y a pas une personne, une organisation ou un groupe "en charge" ; plutôt, le pouvoir et le contrôle sont répartis sur un vaste réseau de participants, dont l’intérêt personnel est servi en suivant les règles et en se comportant honnêtement.

La capacité de parvenir à un consensus sur un réseau distribué, dans des conditions contradictoires, sans centraliser le contrôle est le principe fondamental de toutes les chaînes de blocs publiques ouvertes. Pour relever ce défi et maintenir la propriété précieuse de la décentralisation, la communauté continue d’expérimenter différents modèles de consensus. Ce chapitre explore ces modèles de consensus et leur impact attendu sur les chaînes de blocs de contrats intelligents tels qu’Ethereum.

Note

Alors que les algorithmes de consensus sont une partie importante du fonctionnement des chaînes de bl