Tezos - Gestion des évolutions d’un smart contract

le 23/08/2021 par Frank Hillard
Tags: Évènements

Cet article a pour but de discuter des méthodes pour modifier un service décentralisé sur la Blockchain Tezos. Il est recommandé d’avoir des notions sur le fonctionnement de la blockchain pour bien comprendre les sujets évoqués dans cet article.

Introduction

Dans le cadre du delivery d’un produit, il arrive très régulièrement que le client souhaite mettre en place un service et ultérieurement faire des mises à jour. Soit parce que le besoin n’avait pas été bien cadré, ou bien que de nouvelles fonctionnalités apporteraient de la plus value.

Il est facile de modifier du code dans un système centralisé, mais ce n’est pas le cas avec la Blockchain. Une Dapp (decentralised application) se base sur un “smart contract” déployé sur une blockchain. En théorie, c’est-à-dire dans l’esprit “blockchain”, un “smart contract” est immuable et son code ne peut pas être modifié. Cette notion d’immuabilité du “smart contract” est nécessaire sur une Blockchain publique mais n’est pas forcément respectée dans un Blockchain privée.

En pratique, pour répondre aux besoins du client, il faut tout de même trouver des architectures, structures de données, et design patterns qui permettent d’améliorer une Dapp, et ce en minimisant le coût de transition (d’une version V1 à une version V2). On parle de versioning de contrats intelligents.

Lors de la mise en place d’un système décentralisé (basé sur un ou des “smart contracts”), il est conseillé de prédire les futures évolutions et de prévoir comment sera géré ce versioning.

  • Voyons quels sont les coûts liés au versioning de “smart contracts”.
  • Voyons de quels types d’améliorations nous pouvons parler
  • Voyons les différents patterns qui facilitent les évolutions d’un service décentralisé (basé sur un ou plusieurs “smart contracts”)

Nous illustrerons nos propos par des exemples sur la blockchain Tezos.

Rappel blockchain

Tout d’abord, faisons un petit rappel “blockchain” sur les “smart contracts” et le storage afin de bien comprendre le problème. Voici un petit lien pour ceux qui ne connaissent pas du tout Tezos.

La Blockchain est un système transactionnel combiné avec un réseau Peer-to-Peer où les nœuds discutent entre eux pour établir un consensus sur des données partagées par tous les participants du réseau. Ces données sont rangées dans des transactions qui sont groupées dans des blocs qui sont, à leur tour, chaînés entre eux pour former une chaîne de blocs. Cette chaîne de blocs est immuable.

Les applications décentralisées (Dapp) sont des services numériques déployés sur une blockchain ; en d’autres termes, une Dapp inclut un frontend et un backend; ce dernier implémente un code (“smart contract”) injecté (dans une transaction de la blockchain) qui peut être exécuté ultérieurement.

Une zone mémoire persistante appelée storage est associée à un “smart contract”. La structure des données du storage et la manipulation de ces données sont décrites dans le “smart contract”. Une fois qu’un “smart contract” est déployé sur la blockchain, son code et la définition de son storage ne peuvent plus être changés; cependant les données à l’intérieur du storage sont modifiables.

Le coûts de la migration

Lorsqu’un “smart contract” est  déployé, un nouveau storage lui est associé. Dans le cas d’une évolution d’une Dapp, une nouvelle fonctionnalité ne peut pas être ajoutée au code existant (cf transaction immuable) du “smart contract”. Mais il est possible de déployer un nouveau “smart contract” incluant cette nouvelle fonctionnalité. Par contre les données stockées dans le storage du premier “smart contract” ne seront pas automatiquement accessibles par le second “smart contract”.

Afin de fournir une continuité du service , il est impératif de récupérer les données du storage V1 et de les utiliser dans le nouveau “smart contract”. Malheureusement, dans la Blockchain, la manipulation du storage a un coût, payé en gaz lors de l'exécution de la transaction (le gaz est une unité utilisée pour comptabiliser le coût d'exécution d’une transaction) :

  • allocation : chaque allocation d’espace mémoire dans le storage (sur Tezos, un octet coûte 1 mutez). Le “gas model” (coût en gas de l’invocation d’un “smart contract”) évolue avec les différentes améliorations du protocole.
  • transactions : le transfert d’information via une transaction d’un contrat vers un autre contrat

Etant donné que le storage peut contenir de gros volumes de données, la duplication des données (copier off-chain toutes les données du storage v1 et les intégrer dans le nouveau storage v2) devient très coûteuse. Il faut trouver un moyen de réduire ce coût !

Une technique typique de versioning consiste à déployer une deuxième version d’un “smart contract” en mettant en œuvre une stratégie pour réutiliser les données du storage du premier “smart contract” en minimisant le coût de récupération des données.

Une optimisation évidente consiste à transférer les données par paquets au lieu de les récupérer unitairement. Naïvement, on pourrait extraire toutes les informations d’un storage et les écrire dans le nouveau storage par paquets. Ceci permet de réduire le nombre de transactions, mais ne réduit pas le coût relatif à l’allocation mémoire.

L’extraction et la préparation des données peut se faire off-chain, le coût est un peu diminué si on compacte les informations dans moins de transactions, mais le coût de l’allocation n’est pas diminué.

Nous verrons plus tard des patterns permettant de faire évoluer le contrat en gardant le même storage et donc d’éviter la ré-allocation mémoire.

Les évolutions de smart contracts

Afin de réduire les coûts de réutilisation des données existantes, il est recommandé de prévoir en amont les évolutions envisageables d’un “smart contract”.

Une évolution envisageable est d'ajouter de nouvelles propriétés à un objet, ou d’ajouter un nouveau type d’objet dans notre storage. En d’autres termes, prévoir que la définition de la structure de données (storage) pourrait être amenée à changer.

Une autre évolution envisageable est de changer l’utilisation des données, ce qui implique de changer le code qui utilise les données stockées dans le storage. Mais, comme précédemment mentionné, le code du “smart contract” est immuable !! Il faut envisager de déployer de nouveaux contrats ou trouver une manière pour modifier le comportement d’un contrat déployé.

Une autre évolution possible consiste à intégrer notre “smart contract” à un ensemble complexe de contrats interagissants ensemble pour converger globalement vers un comportement stable. Par exemple, un pattern de Factory (que nous verrons plus tard dans cet article) met en place un éco-système de “smart contracts”.

Par exemple, le stable coin (Maker protocol) est un ensemble de contrats permettant l’utilisation d’une crypto-monnaie (DAI) indexée sur une autre monnaie (Dollar). Ce système prend en compte une seconde sous crypto-monnaie MKR, un système de vente aux enchères pour réguler la stabilité entre DAI et USD.

Pour récapituler, nous avons 3 approches possibles permettant de réutiliser le storage lors de l’évolution d’un “smart contract” existant :

  • structurelle : liée à la modification de la structure des données (storage)
  • comportementale : liée à la modification des règles métier (business rules)
  • Eco-système complexe de contrats : liée à l’intégration d’un “smart contract” dans un système plus complexe de “smart contracts”.

Les solutions

Certaines architectures de “smart contract” permettent les évolutions d’un service décentralisé. Ces considérations étant architecturales, il est nécessaire de prendre en compte ces patterns de versioning dès le début (dès le lancement du service / déploiement du premier “smart contract”).

Le versioning suppose d'utiliser un nouveau service à la place de l’ancien, mais qui est responsable de la bascule ? Un super utilisateur ? L’idée d’un super-utilisateur va à l’encontre de la notion égalitaire d’un service décentralisé (normalement, tous les utilisateurs ont les mêmes droits ou presque). C’est pourquoi certains patterns sont considérés comme des “anti-patterns”. Certains patterns sont donc plus appropriés sur une blockchain privée (ne nécessitant pas l’immuabilité des contrats) et d’autres à la fois sur des blockchains privées ou publiques.

Stockage à structure dynamique

Dans le cas d’une évolution structurelle, il est possible de prévoir un storage un peu générique et extensible. Par exemple, le storage pourrait stocker les données dans un dictionnaire (map) au lieu d’avoir des champs dédiés dans la structure du storage. Une nouvelle clé peut être ajoutée et ainsi étendre la structure de données. Cependant, ceci alourdit le code; au lieu d’accéder directement à un champ, il faut donner le nom de la clé, rechercher dans la map et traiter le cas où la valeur n’est pas disponible pour une clé donnée.

Bref, il est possible de coder le storage de façon souple permettant ainsi de faire évoluer la structure des données; c’est-à-dire un stockage dynamique.

Cependant, il est rare qu’une modification de la structure du storage n'implique pas de changer le comportement du contrat. Si on ajoute un nouveau champ c’est pour l’utiliser !

L’idée est de séparer les données et la logique.

Séparation - Data / Business rules

Dans le cas d’une évolution comportementale, il est envisageable de séparer les données et la logique du “smart contract” :

  • un “smart contract” “data” qui stocke l’information sous la forme d’un pattern extensible et générique, par exemple dans un dictionnaire (set, get);
  • un “smart contract” “featurev1” qui récupère les données du contrat “data” et applique la logique voulue sur ces données.

Dans ce cas, il faut déployer un nouveau contrat “featurev2” avec le nouveau comportement et il faut s’assurer que  le contrat “data” puisse communiquer avec ce nouveau contrat. Appelons ce pattern : Data Proxy pattern.

Un problème de polymorphisme apparaît dans ce genre de situation. Nous en parlerons dans une section dédiée ("Attention au polymorphisme").

L'interaction entre plusieurs contrats nécessite l’envoi de transactions (operations) entre ces contrats; c’est-à-dire un contrat extrait une donnée d’un storage d’un autre contrat et l’utilise dans son code. C’est un processus asynchrone, le contrat appelant attend que son entrypoint de retour soit appelé.

L’implémentation de ce processus asynchrone est très simple (en Ethereum avec le langage Solidity), mais sur Tezos l’implémentation est plus lourde et nécessite la création d’une “opération” (invocation à un contrat) et l’écriture d’une callback (entrypoint spécifique pour traiter la donnée lorsqu’elle est reçue). En effet, ce processus asynchrone est codé dans 2 entrypoints séparés (contrairement à Solidity).

Séparation - Lambda

Dans le cas d’une évolution comportementale, la bonne pratique, en LIGO, consiste à utiliser un pattern “Lambda”.

L’idée est de déporter une partie du code du “smart contract” dans une fonction dite “lambda” dont le corps de la fonction est défini dans le storage. Le corps de la fonction lambda peut être modifié par l’invocation d’un entrypoint du “smart contract”. Le schéma ci-dessous illustre l'exécution d’une transaction qui utilise une lambda.

Il est donc possible de modéliser le comportement de notre “smart contract” à l’aide de fonctions lambda ; et ainsi se laisser la possibilité de modifier le comportement attendu en fournissant une nouvelle implémentation de la fonction lambda.

L’article “Tezos - LIGO pattern - Lambda” fait un zoom sur l’utilisation du lambda pattern en LIGO (avec un exemple d’implémentation).

Ce pattern implique la possibilité de changer le comportement du “smart contract” à la volée, et donc nécessite une gouvernance qui dicte quelles sont les nouvelles règles, quand changent-elles et “qui a le droit d’faire ça”. Cette gouvernance pourrait être implémentée sous la forme d’un pattern “MultiSig” (voir ci-dessous)

MultiSig pattern

Pour rappel, le pattern MultiSig permet d'exécuter une action si suffisamment d'utilisateurs ont donné leur accord. Ce pattern est utilisé lorsque la gouvernance est partagée entre les  utilisateurs.

Ce pattern est utilisé dans le cas de consortium où la décision ne repose pas sur une seule personne mais sur plusieurs acteurs (un comité décisionnaire).

Eco-système de smart contracts

On a vu que la séparation des données et de la logique d’un “smart contract” entraîne la création de plusieurs “smart contracts”. Même s’il n’y a que deux contrats qui interagissent entre eux via des transactions, l’idée d’architecture de smart contracts émerge.

Dans le cas d’une évolution comportementale, on peut améliorer un “smart contract” existant en le combinant avec d’autres patterns. Nous avons vu que le pattern Data Proxy est adapté à Ethereum mais que le pattern Lambda est plus approprié pour Tezos. Il est également conseillé de combiner les différents patterns en fonction du besoin.

Hybride

Par exemple, pour le pattern Lambda qui nécessite une gouvernance, la combinaison hybride avec un pattern MultiSig permet d’éviter une décision unilatérale lors du changement de la logique d’un “smart contract”.

Il est conseillé de bien prévoir les évolutions envisageables (structurelle et/ou comportementale) afin d’appliquer le meilleur pattern. Certains patterns s’appuyant sur une communication intensive entre les “smart contracts” peuvent provoquer un coût important en gaz.

Sur-couche

Il est également possible d’envisager des évolutions d’un “smart contract” qui ont attrait à une architecture de contrats plus complexe permettant d’ajouter des fonctionnalités supplémentaires autour d’un service existant.

Par exemple, dans le cas d’un “smart contract” ZOO déjà déployé permettant de référencer les animaux d’un zoo (sous la forme d’un token non fongible avec un standard FA2). Le client souhaite pouvoir échanger des animaux avec d’autres zoos. Le contrat déployé ZOO gère les activités au sein d’un zoo mais ne peut pas gérer plusieurs zoos. Il est alors envisageable de mettre en place un pattern de Factory permettant de :

  • générer un contrat pour chaque zoo, et ainsi apporter les mêmes fonctionnalités que le contrat déployé
  • permettre l’échange d’animaux entre zoos ; c’est-à-dire des fonctionnalités inter-zoos.

Le schéma ci-dessous illustre l’architecture de contrats proposée par l’exemple du zoo. Le contrat “Factory” joue un rôle de tiers de confiance pour enregistrer de nouveaux zoos (créer de nouvelles instances de zoos), et pour permettre un échange atomique d’animaux entre zoos. Le pattern Factory permet de créer à la volée un nouveau contrat pour un zoo qui souhaiterait rejoindre le réseau.

Bien d’autres surcouches peuvent être imaginées et implémentées !

Attention au polymorphisme

Sur la blockchain Tezos, la communication entre contrats via des transactions est nécessaire pour partager des informations entre contrats. Pour envoyer une transaction, un contrat doit connaître son destinataire (son adresse, et ses entrypoints si le destinataire est un contrat). Si le contrat “destinataire” n’existe pas encore, comment fait-on ?

En LIGO, l’interface d’un contrat est récupérée avec la fonction “get_contract_opt” qui est généralement utilisée pour transmettre une transaction à un contrat. Ainsi un contrat “A” déjà déployé ne pourra communiquer qu’avec un contrat dont il connaît l’interface (les entrypoints et leurs paramètres).

Il existe également la fonction “get_entrypoint_opt” qui permet de vérifier l'existence d’un entrypoint précis d’un contrat. Par exemple, elle permet qu’un contrat déployé (“Data”) puisse prévoir une communication avec un autre contrat “B” pas encore déployé. Ce dernier contrat “B” pas encore déployé devra posséder un entrypoint avec le nom et les paramètres attendus.

Ce schéma illustre la situation décrite précédemment (liée aux entrypoints du contrat). Les lignes en rouge doivent utiliser “get_entrypoint_opt” , celles en noires peuvent utiliser “get_contract_opt” car la totalité de l’interface du contrat est connue.

L’utilisation de get_entrypoint_opt est nécessaire si on souhaite faire du versioning, et donc nécessaire avec les pattern Data Proxy, Lambda et Factory.

Pour résumer

Les évolutions de “smart contracts” ou versioning peuvent être des évolutions structurelles, comportementales et architecturales. Des patterns architecturaux et des modélisations de données adaptés peuvent faciliter l’ajout de nouvelles fonctionnalités, tout en évitant de perdre les données. Ces considérations architecturales doivent être prises en compte dès la conception du service décentralisé.

Ainsi, prévoir les améliorations d’un “smart contract” implique de mettre en place :

  • une séparation des données et de la logique métier
  • une modélisation souple des données:  en utilisant une “map” et des “méta-attributs”
  • une modélisation paramétrable : avec l’implémentation de corps de fonctions “lambda” dans le storage
  • une architecture extensible : en utilisant un pattern Factory et en prévoyant le support du polymorphisme