Menu

english flag

Ethereum

Ethereum

Contrairement à des blockchains comme Bitcoin, qui permet essentiellement d’effectuer des transactions de cryptomonnaie Bitcoin, Ethereum possède en plus un truc assez extraordinaire, c’est l’exécution de code décentralisée.

Oui, décentralisée. Ça veut dire qu’il est possible d’écrire un programme, du code quoi, et de le faire exécuter non pas sur un serveur, mais sur des milliers de serveurs ou nœuds. Et les résultats de notre programme sont également enregistrés de manière décentralisée. Je ne sais pas vous, mais moi je trouve ça incroyable, et ça m’a vraiment donné envie de creuser un peu le sujet.

Donc Ethereum, c’est une blockchain parmi tant d’autres. Ce n’est pas des blockchains qui manquent aujourd’hui, mais à ce jour, Ethereum est la plus connue et la plus utilisée, du moins du côté des blockchains qui permettent, justement, d’exécuter du code. Elle a ses défauts que d’autres blockchains corrigent (mais souvent au détriment d’autres aspects), ce n’est pas vraiment le sujet.

Nous allons voir comment Ethereum fonctionne, en abordant les notions de comptes EOA, de contrats, d’états et de transactions.

Ethereum 101

Nous avons vu dans l’article Blockchain 101 le fonctionnement général des blockchains. Ethereum fonctionne globalement de cette manière, le mécanisme de consensus étant la preuve d’enjeu, ou Proof of Stake. La cryptomonnaie propre à Ethereum est l’Ether (ou ETH). Tout comme Bitcoin et toutes les autres blockchains, il est possible d’envoyer des Ethers à d’autres utilisateurs via des transactions. Chaque utilisateur a son adresse.

Ce qu’Ethereum apporte, c’est qu’en plus des utilisateurs classiques qui effectuent des transactions, il est possible de créer des petits programmes, des smart contracts, qui existent également sur la blockchain. Ils ont tous une adresse, tout comme les utilisateurs, mais ils ont aussi du code, enregistré sur la blockchain.

Pour distinguer ces deux types de comptes, on appelle les utilisateurs classiques des EOA (Externally Owned Accounts), qu’on oppose aux comptes de contrats (contracts accounts), qu’on appellera simplement contrats.

EOA vs Contrats

Les comptes créés par des humains, les EOA, sont donc des comptes avec une adresse, une clé publique et une clé privée. Ils peuvent initier des transactions en les signant, envoyer des Ethers, et en recevoir. Ces transactions peuvent être envoyées à d’autres EOA, ce qui permet d’envoyer des Ethers, mais également vers des contrats.

Les contrats ont également une adresse, mais n’ont pas de clé privée. Ils ne peuvent alors pas initier de transaction. Ils ne peuvent que réagir à des transactions initiées par des EOA, ou à des messages envoyés par d’autres contrats. En effet, une fois appelé par un EOA, un contrat peut tout à fait envoyer des messages à d’autres contrats. La notion de message est abordée à la fin de cet article.

EOA vs Contract

Organisation des données

Avant de plonger sur le pourquoi du comment un compte de type contrat peut exécuter du code au sein de l’écosystème Ethereum, nous allons zoomer sur les différentes données gérées et utilisées par Ethereum. En effet, dans cet écosystème, un état global des adresses doit être maintenu à jour (avec les soldes des comptes, par exemple), la liste des transactions doit être stockée et vérifiable, les messages émis dans les différentes transactions doivent être accessibles, et le stockage permanent de chaque smart contract doit, par définition, être également enregistré quelque part.

Toutes ces données ne sont pas stockées dans les blocs de la blockchain. Aussi étonnant que cela puisse paraitre (en tout cas pour moi au premier abord), ces informations sont enregistrées dans des bases de données, en dehors des blocs, sous forme d’arbres qui suivent un format spécifique : ce sont des Merkle Patricia Tries, qui permettent de stocker une liste de clés/valeurs de manière optimisée.

Il n’y a pas de typo, c’est bien Trie, et non pas Tree, en référence au mot anglais Retrieve. Nous verrons probablement les Merkle Patricia Tries en détails dans un article dédié.

Ces données sont donc enregistrées dans les arbres suivants :

  • State trie, ou world state, qui contient lui-même des liens vers des storage tries
  • Transactions tries
  • Receipt tries

Ainsi, dans les blocks, seul le hash de la racine de chacun de ces arbres est stocké.

Ethereum Blocks

Pour simplifier les prochains schémas, il arrivera qu’on note des transactions dans des blocs. Mais comme indiqué ici, le détail des transactions n’est techniquement pas inclus dans les blocs.

C’est à chaque client de savoir stocker le contenu des arbres et de gérer les requêtes à partir du hash du nœud racine (tous les clients n’utilisent pas les mêmes bases de données d’ailleurs).

Cette organisation permet aux équipements légers (mobiles, IoT) de se synchroniser facilement et rapidement avec la blockchain sans pour autant télécharger d’immenses volumes de données, et d’avoir ainsi connaissance des hash des nœuds racines des différents arbres, et ce pour chaque bloc.

Avec seulement les hashs des nœuds racines, un équipement léger peut demander à des nœuds complets (full nodes), c’est à dire des nœuds qui ont enregistré la blockchain ainsi que toutes les bases de données, de lui envoyer des données spécifiques. Grâce aux hashs des nœuds racine, le client léger pourra vérifier la validité de ces données (une transaction, le solde d’un compte, etc.).

Notons que même un full node Ethereum ne requiert qu’environ 1To d’espace disque. C’est accessible à vraiment tout le monde, et c’est ce qui fait qu’il y a autant de personnes qui participent au réseau décentralisé. Par ailleurs, il existe également les archive nodes. Contrairement aux full nodes qui ne se synchronisent qu’avec les 128 derniers blocs, les nœuds d’archive possèdent toute la blockchain. Si vous voulez plus d’informations, n’hésitez pas à lire cet article.

Voyons ensemble à quoi correspondent ces différents arbres de données.

World State

Commençons par le State Trie, ou le World State. Nous pouvons préciser que, tandis que nous comparions une blockchain à une base de données décentralisée, Ethereum est plus complexe et complet que ça. On pourrait plutôt décrire Ethereum comme une machine à état décentralisée.

C’est donc l’état général de Ethereum qui est appelé World State. Dans cet état, il y a toutes les adresses actives des utilisateurs (c’est à dire les adresses étant présentes dans au moins une transaction), et à chaque adresse est associé un état de compte (account state).

World State

Account State

L’état de chaque compte est donc enregistré dans le world state contenant les 4 champs suivants :

  • balance : Le solde d’Ether du compte
  • nonce : Un numéro qui s’incrémente à chaque transaction pour un EOA, et à chaque création de contrat pour un contrat
  • codeHash : Un hash qui permet de retrouver le code du smart contract (le hash d’une chaine de caractère vide pour un EOA)
  • storageRoot : Le hash du nœud racine de l’arbre Merkle Patricia de l’account storage, ou storage trie. Il permet de récupérer l’état du contrat, comme la valeur des variables enregistrées de manière permanente par le contrat. Ce champ est vide pour un compte EOA.

Account State

A chaque fois qu’un bloc de la blockchain est validé, l’ensemble des transactions vont apporter des modifications au world state, pour donner un nouvel état.

Dans l’exemple du schéma suivant, un bloc effectue deux transactions :

  1. L’adresse A envoie 2 coins à l’adresse C. Les soldes (balance) de A et de C vont évoluer, ainsi que le nonce de A (qui s’incrémente à chaque transaction)
  2. L’adresse A envoie 4 coins à l’adresse D. Le solde de A va évoluer, et l’adresse D n’existant pas encore dans le world state va être ajoutée, avec un solde valant 4, et un nonce valant 0.

Les champs en rouge sont donc ceux qui sont modifiés suite à l’exécution des transactions du bloc, menant à un nouvel état N+1.

World State Update

Transactions

Nous avons maintenant une vision plus claire des types de comptes qui existent, et comment ils sont enregistrés au sein d’Ethereum. Nous avons expliqué que les blocs contiennent des transactions qui modifient l’état des comptes impliqués, et par conséquent l’état général, ou world state. Ces transactions sont en réalité enregistrées dans une base de données, le Transactions Trie, de manière ordonnée.

Dans une transaction, on trouve plusieurs éléments :

  • Nonce : Le nonce est propre à chaque compte (stocké pour chaque adresse dans le world state, si vous avez bien suivi), et est incrémenté pour chaque nouvelle transaction
  • gasPrice et gasLimit: Ils permettent à l’utilisateur de définir les frais de transaction
  • to: L’adresse destinataire de la transaction
  • value: Le nombre de Eth envoyés (optionnel)
  • v,r,s: La signature de l’utilisateur
  • data: Permet d’envoyer des données à un autre compte, ou permet de définir le contrat lors de sa création

Si vous êtes observateur, vous constaterez qu’une transaction doit être signée. Or le seul type de compte qui possède une clé privée est l’EOA. Les contrats ne possèdent pas de clé privée. Ils ne peuvent donc pas initier de transaction.

Il existe en réalité deux types de transactions chez Ethereum, celles qui permettent d’envoyer un message à un autre compte, et celles qui permettent de créer un contrat.

L’envoi d’un message

Dans une transaction, un compte A envoie un message à un compte B. L’adresse de destination to est celle du compte B, et les champs value et data peuvent être utilisés.

Envoi d’Ether

Pour envoyer des Ether à l’adresse de destination, la somme souhaitée sera indiquée dans value. Quand on compte envoie de l’argent à un autre compte, c’est uniquement ce champ value qui est renseigné. Le compte de destination peut être un EOA ou un contrat.

Si la destination est un contrat, il faut que le contrat ait été conçu pour recevoir des Ethers de la sorte.

Envoi de données

Le champ data est quant à lui majoritairement utilisé pour exécuter le code d’un smart contract, quand la transaction lui est destinée. C’est aussi possible d’envoyer des données à un EOA, et le destinataire la traitera comme bon lui semble.

Lors de l’appel d’une fonction d’un contrat, le champ data doit être formaté de la manière suivante :

data: <Sélecteur de la fonction> <arguments>

Le sélecteur de la fonction est calculé en hashant la signature de la fonction, et en ne retenant que les 4 premiers octets.

Par exemple, imaginons la fonction suivante :

function getItemValue(string calldata _itemName, uint256 _itemId) public returns(uint256 value) {
  // Code de la fonction
}

La signature de la fonction est :

getItemValue(string,uint256)

Et le sélecteur :

bytes4(keccak256("getItemValue(string,uint256)"));
// Output:
0xc2e58fec

Donc le contenu de data ressemblera à

data: 0xc2e58fec<arguments>

Nous verrons comment les arguments sont organisés dans un prochain article, mais voici un exemple pour l’appel getItemValue("pixis", 8) :

0xc2e58fec                                                       # Sélecteur de fonction
0000000000000000000000000000000000000000000000000000000000000040 # Pointeur vers la chaine
0000000000000000000000000000000000000000000000000000000000000008 # 8
0000000000000000000000000000000000000000000000000000000000000005 # Longueur de la chaine
7069786973000000000000000000000000000000000000000000000000000000 # Chaine "pixis"

Ce type de message peut donc être envoyé depuis une transaction d’un compte EOA vers un smart contract.

Sachez qu’il est également possible qu’un contrat appelle une fonction d’un autre contrat en envoyant le même format de message. Tout se passera dans la même transaction, puisqu’un contrat ne peut pas signer de nouvelle transaction. Ce type d’appel entre contrat est un message call, c’est une instruction spécifique de la machine virtuelle de Ethereum. Seul le message est envoyé, le contrat de destination sera exécuté, et le résultat de cet appel sera retourné au contrat appelant. Nous verrons ces appels plus en détails dans de prochains articles.

La création d’un contrat

Le deuxième type de transaction permet à un compte EOA de créer un nouveau contrat. Pour cela, la transaction a pour destinataire l’adresse nulle 0x00000..., et le champ data est utilisé.

Ce champ data est divisé en deux parties :

  • Le code d’initialisation (initialization bytecode) qui permet de déployer le contrat. On y trouvera notamment le code du constructeur du contrat avec ses arguments (s’il y a un constructeur) ou encore les modifications du storage si des variables sont déclarées. Ce code termine en retournant l’adresse en mémoire du runtime bytecode ainsi que sa taille.
  • Le code de runtime (runtime bytecode) est le code du contrat, incluant le code de toutes les fonctions.

Une fois que cette transaction est traitée, un nouveau compte, celui du contrat, est créé. Son adresse est dérivée de l’adresse du créateur du contrat et du nonce de ce compte. Ainsi, à chaque nouvelle création de contrat, une adresse différente sera générée.

Comme nous l’avons vu précédemment, une nouvelle entrée dans le world state sera créée pour cette adresse. Le nonce sera 0, le solde du contrat dépendra du champ value de la transaction qui l’a créé (0 par défaut), mais le plus important sont les champs :

  • codeHash : Il permet de retrouver où se trouve le runtime bytecode du compte, c’est à dire toute la logique du smart contract
  • storageRoot : Un contrat étant toujours associé à un espace de stockage permanent, le account storage, cette valeur permet de retrouver cet espace de stockage afin de lire et modifier toutes les variables utilisées dans le smart contract.

Receipts

Le dernier arbre dont nous n’avons pas parlé est le Receipts Trie. Il permet de stocker les informations qui ne sont pas nécessaires au bon fonctionnement des smart contracts, mais qui peuvent être utilisées par des applications tierces, comme des front-ends, ou des clients.

Il y a un seul Receipts Trie par bloc. C’est un résumé des transactions qui se sont exécutées dans le bloc.

On y trouve par exemple le statut de la transaction (si elle a échoué ou non), ou encore le montant de gas utilisé.

De plus, lorsqu’un smart contract est exécuté, il peut émettre des événements.

contract MyContract {
  // Initialisation d'un événement "Transfer"
  Event Transfer(address to, uint value, uint tokenId);

  function transferTokens(address _to, uint _value, uint _tokenId) external {
    // Code de la fonction

    // Emission de l'événement "Transfer"
    emit Transfer(_to, _value, _tokenId);
  }
}

Dans cet exemple, l’événement Transfer est émis à la fin de la fonction transferToken. Cet événement sera ajouté au Receipts Trie du bloc.

Conclusion

Ces différents éléments nous permettent de mieux comprendre comment fonctionne Ethereum, ce qui définit un smart contract, comment un utilisateur peut en créer et comment il peut interagir avec. Cet article, couplé avec l’introduction aux blockchains, permettent de poser les bases pour expliquer le fonctionnement de la machine virtuelle de Ethereum, la EVM (Ethereum Virtual Machine). Mais ça, c’est dans le prochain article !


hackndo logo
Auteur : Pixis
Créateur du blog, suivez-moi sur twitter ou discord