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 [references]) 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 [gas]. 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 [intro_chapter] 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 [EIP55]). 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 1. 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 2. 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 3. 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 4. 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 [solidity_faucet_example], 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 [intro_chapter] 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 5. 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 6. 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 [elliptic_curve].

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 [pubkey], 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 1. 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 7. 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 [evm_chapter].

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.