Ethereum : trucs et astuces concernant la machine virtuelle

le 12/09/2017 par Philippe Prados
Tags: Software Engineering

Les différentes technologies de Blockchain (Bitcoin, Ethereum, etc.) consistent à produire un consensus, entre de nombreuses parties, sur un état stable. Pour Bitcoin, il s’agit de se mettre d’accord sur l’état d’un livre de compte. Ethereum ajoute la notion de consensus sur l’état de contrats dits intelligents (smart contracts).Alors qu’un contrat est usuellement contrôlé par une entité centralisée, Ethereum permet lui l’exécution de contrats numériques sous le contrôle du réseau distribué. Des fonds (en monnaie électronique Ether) peuvent être sous la responsabilité du contrat, et le code décide ensuite de leur distribution. Typiquement, le contrat peut servir de notaire, en gardant des fonds jusqu'à ce qu’une condition nécessaire soit validée (délai, preuve de livraison, etc.).

Pour arriver à un consensus sur l’état de la blockchain, des serveurs (que l’on appelle des mineurs) valident chaque nouvel état, constitué d’un nouveau bloc, avant son ajout à la suite des précédents. Cette validation passe par la résolution d'un problème mathématique complexe consistant à trouver une valeur particulière à associer au bloc, pour répondre à certaines caractéristiques. Le premier serveur à valider un bloc est habilité à l’ajouter à la chaîne de bloc. Il est rémunéré pour cela.

Les développeurs de contrats utilisent généralement le langage de développement Solidity : le code Solidity est compilé dans un byte-code destiné à la machine virtuelle Ethereum (EVM).

Il existe de nombreuses implémentations de la machine virtuelle Ethereum. Certaines simulent les instructions localement dans un navigateur, avec un simple code JavaScript. D’autres convertissent le code des contrats en code assembleur, afin de réduire le temps d’exécution du programme.

Nous vous proposons d’étudier quelques spécificités de l’EVM et du langage Solidity, afin de pouvoir les utiliser judicieusement. L’objectif de cet article est de parcourir les spécificités de cet environnement d’exécution. Des choix ont été faits par les concepteurs d’Ethereum qui sont inhabituels par rapport aux approches utilisées par d’autres langages de programmation. Par exemple, contrairement à Java, la machine virtuelle n’a pas connaissance de notions complexes comme l’héritage, le polymorphisme, les constructeurs, la gestion mémoire, etc. Nous pensons que connaître ces spécificités permet de proposer des smart contrats plus efficaces. Dans un prochain article, nous mettrons à profit ces particularités pour contourner une limite d’Ethereum : l’immuabilité des contrats.

1 Ethereum VM (EVM)

L’Ethereum Virtual Machine propose des calculs sur des données de 256 bits (32 bytes) à l’aide d’une pile d’exécution. Les valeurs sont déposées sur une pile et les instructions consomment les données au-dessus de la pile pour y déposer le résultat.

Par exemple, la suite d’instructions suivante effectue une addition et place le résultat sur le sommet de la pile.

PUSH1 0x42
PUSH1 0x24
ADD

Il y a également des zones mémoires spéciales. La RAM, dédiée à l’exécution de la transaction, commence à l’adresse zéro et augmente au fur et à mesure des besoins, par pas de 32 bytes. Comme toutes les instructions de la machine virtuelle, cela a un coût (1 gaz par 32 bytes écrit).

Les données persistantes dans le contrat (c’est à dire les attributs) sont mémorisées dans une table d’association utilisant 32 bytes pour la clé et 32 bytes pour la valeur. Les modifications sont sauvegardées dans le prochain bloc de la blockchain, après validation par la communauté.

Un attribut coûte 20.000 gaz lors de sa création (passage à une valeur différente de zéro), puis 5000 gaz lors de chaque modification. Il rapporte 15.000 gaz lors de sa remise à zéro.

Pour invoquer une méthode d’un contrat, il faut envoyer une transaction au réseau Ethereum. Charge à lui de trouver le mineur qui sera en capacité d’exécuter la méthode. Cela se traduit techniquement par l’envoi d’une transaction au réseau, avec la description de l’invocation (contrat, méthode, paramètres).

Dans une transaction, on trouve :

  • msg.sender : l'émetteur du message (current call)
  • msg.gas : Le gaz maximum restant pour la suite du traitement
  • msg.data : Les paramètres de l’invocation du contrat
  • msg.sig : La signature de la méthode invoquée, sur 4 bytes. Il s’agit en fait d’un calcul de hash sur le nom de la méthode, intégrant la liste des paramètres. Cela correspond aux quatre premiers octets de msg.data
  • msg.value : éventuellement, une quantité de wei (sous unité d’éther) pour transférer des fonds au contrat

Pour construire un contrat depuis l’extérieur de la blockchain, il faut invoquer le contrat zéro, avec en paramètre, le code et la description du nouveau contrat. Le code du contrat zéro se charge d’ajouter le nouveau contrat dans la chaîne de bloc, d’exécuter le constructeur et de retourner l’adresse du contrat.

Pour construire un contrat depuis un autre contrat, il est possible d’utiliser l’instruction CREATE de l’EVM. Cela est moins coûteux que depuis l’extérieur (100 gaz contre 53.000, cf les spécification de l’EVM).

Des instructions permettent à un contrat d’écrire des logs. Les logs sont des tableaux de bytes, associés à chaque contrat. Cela permet aux API externes de récupérer des informations lorsqu’une transaction est validée par la communauté. C’est le canal privilégié par Solidity pour les communications asynchrones entre les contrats et l’extérieur d’Ethereum. Ecrire dans un log permet de communiquer le résultat d’un appel asynchrone.

Parmi les instructions avancées de la machine virtuelle Ethereum, il y a trois instructions spéciales :

  • call pour invoquer un autre contrat et le modifier
  • callcode pour utiliser le code d’un autre contrat, sur l’état du contrat appelant, en indiquant le contrat actuel comme à l’origine de l’invocation.
  • delegatecall pour utiliser le code d’un autre contrat, sur l’état du contrat appelant, en gardant la valeur du msg.sender (l’identité de l’invocation de la méthode d’origine).

callcode n'est pas très utile à cause du changement de msg.sender. Pour ne pas casser la compatibilité, ils ont ajoutés une nouvelle instruction : delegatecall.

2 Solidity

Au-dessus de la machine virtuelle, le langage Solidity propose de nombreux concepts de plus haut niveau, pouvant être compilés en instructions de la machine virtuelle. Souvent, le code rédigé en Solidity est très éloigné du code compilé pour l’EVM. Il est parfois important de comprendre la relation entre les deux, pour exploiter au mieux les avantages des deux modèles de programmation.

Petit rappel sur les concepts portés par Solidity :

  • Génère du code EVM
  • propose une sorte d’héritage avec la notion de constructeur
  • propose un modèle particulier de polymorphisme

• Comme les instructions de la EVM fonctionnent en 256 bits, cela n'économise pas le coût de sauvegarde des informations. Pour optimiser cela, Solidity se charge d’effectuer des opérations de masque binaire, pour pouvoir manipuler quelques bytes à la fois sur chaque zone mémoire de 32 bytes. Par exemple, la méthode f() suivante :

contract Test {
  byte b;
  function f() {
    b=0xAA;
  }
}

est compilée en cette longue suite d’instructions. Tout cela pour ne modifier QUE le premier octet des 32 bytes.

// b = b AND NOT(0xFF) OR 0xAA
PUSH 0 // Offset de l’attribut
DUP1
SLOAD // Lecture de l’attribut de position 0
PUSH FF // Masque binaire pour un octet
NOT // Inversion du masque (0xFFFFF....FF00)
AND // Masque entre la donnée de l’attribut et le masque
PUSH AA // Valeur à écrire
OR // Or entre 0xAA et l’attribut moins le premier octet
SWAP1
SSTORE // Ecriture de l’attribut

Cela fait beaucoup d’instructions, mais reste bien moins coûteux en éther que de mémoriser chaque octet dans un espace mémoire de 32 bytes.

• Comme il n’y a qu’un seul point d’entrée pour un contrat EVM, Solidity a décidé de consacrer les quatre premiers octets des paramètres de la transaction à l’identification de la méthode à invoquer. La valeur des octets correspondent à un bytes4(sha3("f(uint)") sur le nom de la méthode, complété par le type des différents paramètres.

Le code du contrat commence par un aiguillage semblable à un switch, basé sur la signature de chaque méthode. Si aucune méthode ne correspond, alors la méthode par défaut est utilisée. C’est une méthode particulière, n’ayant pas de nom ni de paramètre. Cette approche permet de gérer le polymorphisme.

switch (msg.sig) {
  case e1c7392a : // function init()
  …
  case 2db12ac4 : // function changeToV2()
  …
  case 82692679 : // function doSomething()
  …
  default : // function ()
  …
}

• La machine virtuelle n’a pas de notion de constructeur. Pour construire une instance, il faut envoyer une transaction vers le contrat zéro. Les datas sont alors considérées comme du code à exécuter.

Solidity génère automatiquement le code du constructeur, qui se charge de fournir le code du contrat à générer sous forme de data.

• Solidity propose une instruction throw, pour signaler une erreur dans un contrat. Comme il n’existe pas d’équivalent dans la machine virtuelle, le code généré invoque un saut vers une adresse mémoire invalide. Cela implique une erreur lors de l’exécution, ce qui est le but recherché. Ne vous étonnez pas alors, de recevoir une erreur de type “invalid jump destination”.

• Solidity propose la notion d’event, qui correspond aux logs de la machine virtuelle. Ils permettent d’informer un code à l’écoute de la chaîne lorsqu'une méthode d’un contrat est invoquée. Des filtres permettent d’attendre l’acquittement d’un traitement particulier dans la blockchain. Concrètement, Solidity utilise toujours la première valeur des logs de l’EVM pour y placer le hash de la signature de l’event. Les autres valeurs correspondent aux paramètres de l’event. Le hash de la signature de l’évènement est utilisé comme nom de topic, pour l’indexation des événements.

• Un dernier point à savoir. Pour maîtriser la mémoire nécessaire à l’invocation des méthodes, l’attribut de clé 0x40 est utilisé par Solidity. La valeur stockée indique la plus haute adresse mémoire en mémoire RAM utilisée par les méthodes du contrat.

Dans un prochain article, nous utiliserons toutes ces particularités pour répondre à la question suivante : comment modifier un contrat immuable ?