
La blockchain et les contrats intelligents sont toujours un sujet brûlant chez les développeurs et les techniciens. Il y a beaucoup de recherches et de discussions sur leur avenir et où tout va et où cela mènera. Chez Waves Platform, nous avons notre propre opinion sur ce que devraient être les contrats intelligents, et dans cet article, je vous dirai comment nous les avons fait, quels problèmes nous avons rencontrés et pourquoi ils ne sont pas comme les contrats intelligents d'autres projets de blockchain (tout d'abord Ethereum).
Cet article est également un guide pour ceux qui veulent comprendre le fonctionnement des contrats intelligents dans le réseau Waves, essayer d'écrire votre propre contrat et vous familiariser avec les outils dont les développeurs disposent déjà.
Comment en sommes-nous arrivés à une telle vie?
On nous a souvent demandé quand avons-nous obtenu des contrats intelligents, car les développeurs ont apprécié la facilité de travailler avec le réseau, la vitesse du réseau (grâce à Waves NG ) et le faible niveau de commissions. Cependant, les contrats intelligents offrent beaucoup plus de place à l'imagination.
Les contrats intelligents sont devenus très populaires ces dernières années en raison de la propagation de la blockchain. Ceux qui ont rencontré la technologie blockchain dans leur travail, lorsqu'ils mentionnent les contrats intelligents, pensent généralement à Ethereum et Solidity. Mais il existe de nombreuses plates-formes blockchain avec des contrats intelligents, et la plupart d'entre elles ont simplement répété ce qu'Ethereum a fait (machine virtuelle + leur propre langage de contrat). Une liste intéressante avec différentes langues et approches se trouve dans ce référentiel .
Qu'est-ce qu'un contrat intelligent?
Au sens large, un contrat intelligent est un protocole conçu pour soutenir, vérifier et faire respecter les termes d'une transaction ou l'exécution de contrats entre des parties. L'idée a été proposée pour la première fois en 1996 par Nick Szabo, mais les contrats intelligents ne sont devenus populaires que ces dernières années.
D'un point de vue technique (qui nous intéresse davantage), un contrat intelligent est un algorithme (code) qui est exécuté non pas sur un seul serveur ou ordinateur, mais sur de nombreux (ou tous) nœuds du réseau de la blockchain, c'est-à-dire décentralisé.
Comment ça marche?
Le premier prototype d'un contrat intelligent sur la blockchain est correctement considéré comme un script Bitcoin - incomplet par Turing, un langage basé sur la pile sur le réseau Bitcoin. Il n'y a pas de concept de compte dans Bitcoin, mais des entrées et des sorties. Dans Bitcoin, lors d'une transaction (création d'une sortie), il est nécessaire de se référer à la transaction de réception (entrée). Si vous êtes intéressé par les détails techniques de l'appareil Bitcoin, je vous recommande de lire cette série d'articles . Puisqu'il n'y a pas de compte dans Bitcoin, Bitcoin Script détermine dans quel cas l'une ou l'autre sortie peut être dépensée.
Ethereum offre beaucoup plus de fonctionnalités, comme Fournit Solidity, un langage complet de Turing qui s'exécute dans une machine virtuelle à l'intérieur de chaque nœud. Une grande puissance s'accompagne d'une grande responsabilité et d'un large éventail de possibilités - un nombre assez important de restrictions, dont nous parlerons plus tard.
Contrats intelligents Waves
Comme je l'ai écrit ci-dessus, on nous a souvent posé des questions sur les contrats intelligents, mais nous ne voulions pas faire «comme à l'antenne» ou «comme dans n'importe quel nom de chaîne», et il y a plusieurs raisons à cela . Par conséquent, nous avons analysé les cas existants pour les contrats et comment nous pouvons aider à résoudre de vrais problèmes avec leur aide.
Après avoir analysé les scénarios d'utilisation, nous avons découvert qu'il existe 2 grandes catégories de tâches qui sont généralement résolues à l'aide de contrats intelligents:
- Tâches simples et directes comme multisig, swaps atomiques ou séquestre.
- dApps, applications décentralisées à part entière avec logique utilisateur. Pour être plus précis, il s'agit d'un backend pour les applications décentralisées. Les exemples les plus frappants sont Cryptokitties ou Bancor.
Il existe également un troisième type de contrat, le plus populaire - les jetons. Sur le réseau Ethereum, par exemple, la grande majorité des contrats de travail sont des jetons standard ERC20. Dans Waves, pour créer des jetons, il n'est pas nécessaire de créer des contrats intelligents, car ils font partie de la blockchain elle-même, et pour émettre un jeton (avec la possibilité de l'échanger immédiatement sur un échange décentralisé (DEX)), il suffit d'envoyer une transaction du type d'émission (transaction d'émission).
Pour les deux types de tâches ci-dessus (pour des raisons de simplicité, nous appellerons des cas simples et complexes) les exigences concernant la langue, les contrats et les concepts sont très différentes. Oui, nous pouvons dire qu'avoir un langage complet de Turing peut résoudre à la fois des problèmes simples et complexes, mais il y a une condition importante: le langage devrait aider à éviter les erreurs. Cette exigence est également importante pour les langues ordinaires et pour les langues des contrats intelligents, elle est particulièrement importante, car les opérations sont financièrement liées, et les contrats sont souvent immuables, et il n'y a aucun moyen de corriger une erreur rapidement et facilement.
Compte tenu des types de tâches décrites ci-dessus, nous avons décidé de progresser progressivement et de donner un outil pour résoudre des problèmes simples comme première étape, et de donner un langage qui puisse facilement implémenter n'importe quelle logique utilisateur comme étape suivante. En conséquence, le système s'est avéré être beaucoup plus puissant que nous ne l'imaginions au début du voyage.
Rendons les comptes intelligents
Progressivement, nous sommes arrivés au concept de comptes intelligents, qui sont conçus pour résoudre des tâches principalement simples. Leur idée est très similaire à Bitcoin Script: des règles supplémentaires peuvent être ajoutées au compte qui déterminent la validité de la transaction sortante. Les principales exigences pour les comptes intelligents étaient:
- Sécurité maximale. Presque chaque mois, vous pouvez trouver des nouvelles qu'une autre vulnérabilité a été trouvée dans les contrats modèles Ethereum. Nous voulions éviter cela.
- Pas besoin de gaz, donc la commission est fixe. Pour ce faire, le script doit être exécuté dans un délai prévisible et avoir des limites de taille assez strictes.
Avant de passer aux détails techniques de la mise en œuvre et de la rédaction des contrats, nous décrivons certaines caractéristiques de la blockchain Waves qui seront importantes pour une meilleure compréhension:
- La blockchain Waves compte actuellement 13 types de transactions différents.

- Dans la blockchain Waves, pas les entrées et sorties (comme dans Bitcoin), mais les comptes (comme, par exemple, dans Nxt). Une transaction est effectuée pour le compte d'un compte spécifique.
- Par défaut, l'exactitude d'une transaction est déterminée par l'état actuel de la blockchain et la validité de la signature au nom de laquelle la transaction est envoyée. La représentation JSON de la transaction semble assez simple:

Étant donné que nous avons déjà différents types de transactions dans la blockchain, nous avons décidé de ne pas créer une entité distincte en tant que compte intelligent, mais d'ajouter une nouvelle transaction qui transforme un compte régulier en un compte intelligent. Tout compte peut devenir un compte intelligent avec des règles de validation de transaction modifiées, pour cela, le compte doit simplement envoyer une transaction du type SetScriptTransaction
, qui contient le contrat compilé.
Dans le cas d'un compte intelligent, le contrat est une règle de validation pour chaque _ transaction sortante.
Et le gaz?
L'une des principales tâches que nous nous sommes fixées est de nous débarrasser du gaz pour des opérations simples. Cela ne signifie pas qu'il n'y aura pas de commission. Il est nécessaire pour que les mineurs aient intérêt à exécuter des scripts. Nous avons abordé la question du côté pratique et décidé de procéder à des tests de performance et de calculer la vitesse des différentes opérations. Pour cela, des benchmarks utilisant JMH ont été développés. Les résultats peuvent être vus ici . Les limitations qui en résultent sont:
- Le script doit s'exécuter plus rapidement que 20 opérations de vérification de signature, ce qui signifie que les vérifications pour un compte intelligent ne seront pas plus de 20 fois plus lentes que pour un compte normal. La taille du script ne doit pas dépasser 8 Ko.
- Pour rendre rentable pour les mineurs l'exécution de contrats intelligents, nous avons fixé une commission supplémentaire minimale pour les comptes intelligents d'un montant de 0,004 ONDES. La commission minimale dans le réseau Waves pour une transaction est de 0,001 WAVES, dans le cas d'un compte intelligent - 0,005 WAVES.
Langage pour les contrats intelligents
L'une des tâches les plus difficiles a été la création de leur propre langage de contrats intelligents. Prendre n'importe quel langage existant et l'adapter (découper) pour nos tâches semble tirer à partir d'un canon sur des moineaux: en plus de cela, dépendre de la base de code de quelqu'un d'autre dans un projet de blockchain est extrêmement risqué .
Essayons d'imaginer quel devrait être le langage idéal pour les contrats intelligents. À mon avis, tout langage de programmation devrait forcer à écrire du code "correct" et sûr, c'est-à-dire idéalement, il ne devrait y avoir qu'une seule bonne façon. Oui, si vous le souhaitez, vous pouvez écrire du code complètement illisible et non pris en charge dans n'importe quelle langue, mais cela devrait être plus difficile que de l'écrire correctement (bonjour PHP et JavaScript). Dans le même temps, le langage devrait être pratique pour le développement. Étant donné que le langage s'exécute sur tous les nœuds du réseau, il est nécessaire qu'il soit aussi efficace que possible - une exécution paresseuse peut économiser beaucoup de ressources. J'aimerais aussi avoir un système de typage puissant dans la langue, de préférence algébrique, car il permet de décrire le contrat le plus clairement possible et de se rapprocher du rêve de "Code is law". Si nous formalisons un peu plus nos exigences, nous obtenons les paramètres de langue suivants:
- Soyez strictement et statiquement tapé. Un typage fort élimine automatiquement de nombreuses erreurs de programmation potentielles.
- Ayez un système de frappe puissant pour vous empêcher de vous tirer une balle dans le pied.
- Soyez paresseux pour ne pas perdre de précieux cycles de traitement.
- Avoir des fonctions spécifiques dans la bibliothèque standard pour travailler avec la blockchain, par exemple, les hachages. Dans le même temps, la bibliothèque de langues standard ne doit pas être surchargée, car il doit toujours y avoir une bonne façon.
- Ne pas avoir d'exceptions lors de l'exécution.
Dans notre langage RIDE, nous avons essayé de prendre en compte ces fonctionnalités importantes, et comme nous développons beaucoup sur Scala et comme la programmation fonctionnelle, le langage est à certains égards similaire à Scala et F #.
Les plus gros problèmes dans la mise en œuvre dans la pratique se sont posés avec la dernière exigence, car si vous n'avez pas d'exceptions dans la langue, alors, par exemple, l'opération d'ajout devra renvoyer une option , qui devra être vérifiée pour le débordement, ce qui sera certainement gênant pour les développeurs. Les exceptions étaient un compromis, mais sans possibilité de les intercepter - s'il y avait une exception, la transaction n'était pas valide. Un autre problème était de transférer dans la langue tous les modèles de données que nous avons dans la blockchain. J'ai déjà décrit que dans Waves, il existe 13 types de transactions différents qui doivent être pris en charge dans la langue et avoir accès à tous leurs domaines.
Des informations complètes sur les opérations disponibles et les types de données dans RIDE se trouvent sur la page de description du langage . Parmi les caractéristiques intéressantes de la langue, nous pouvons également souligner le fait que la langue est basée sur l'expression, c'est-à-dire que tout est expression, ainsi que la présence d'une correspondance de modèle, ce qui vous permet de décrire facilement les conditions de différents types de transactions:
match tx { case t:TransferTransaction => t.recepient case t:MassTransferTransaction => t.transfers case _ => throw }
Toute personne intéressée à apprendre comment fonctionne le code RIDE vaut la peine de consulter le livre blanc , qui décrit toutes les étapes de l'utilisation d'un contrat: analyse, compilation, désérialisation, calcul de la complexité et de l'exécution du script. Les deux premières étapes - l'analyse et la compilation sont effectuées hors chaîne, seul le contrat compilé en base64 entre dans la blockchain. La désérialisation, le calcul de la complexité et l'exécution se font en chaîne et plusieurs fois à différentes étapes:
- Lorsque vous recevez une transaction et l'ajoutez à UTX, sinon il y aura une situation où la transaction sera acceptée par le nœud de la chaîne de blocs, par exemple via l'API REST, mais n'entrera jamais dans le bloc.
- Lorsqu'un bloc est formé, le nœud d'exploration de données valide les transactions et le script est requis.
- Dès réception par les nœuds non miniers d'un bloc et validation des transactions qui y sont incluses.
Chaque optimisation dans l'utilisation des contrats devient précieuse, car elle est effectuée plusieurs fois sur de nombreux nœuds de réseau. Désormais, les nœuds Waves s'exécutent silencieusement sur des machines virtuelles pour 15 $ chez DigitalOcean, malgré l'augmentation des charges de travail après la publication des comptes intelligents.
Quel est le résultat?
Voyons maintenant ce que nous avons obtenu à Waves. Nous rédigerons notre premier contrat, que ce soit un contrat multisig 2-of-3 standard. Pour rédiger un contrat, vous pouvez utiliser l' IDE en ligne (réglage de la langue - un sujet pour un article séparé). Créez un nouveau contrat vide (Nouveau → Contrat vide).
Tout d'abord, nous annoncerons les clés publiques d'Alice, Bob et Cooper, qui contrôleront le compte. Vous aurez besoin de 2 de leurs 3 signatures:
let alicePubKey = base58'B1Yz7fH1bJ2gVDjyJnuyKNTdMFARkKEpV' let bobPubKey = base58'7hghYeWtiekfebgAcuCg9ai2NXbRreNzc' let cooperPubKey = base58'BVqYXrapgJP9atQccdBPAgJPwHDKkh6A8'
La documentation décrit la fonction sigVerify
, qui vous permet de vérifier la signature de la transaction:

Les arguments de la fonction sont le corps de la transaction, la signature vérifiée et la clé publique. Un objet tx
est disponible dans le contrat dans la portée globale, dans lequel les informations de transaction sont stockées. Cet objet a un champ tx.bodyBytes
qui contient les octets de la transaction envoyée. Il existe également un tableau de tx.proofs
, qui stocke des signatures, qui peuvent aller jusqu'à 8. Il convient de noter qu'en fait, vous pouvez envoyer non seulement des signatures à tx.proofs
, mais toute autre information pouvant être utilisée par le contrat.
Nous pouvons nous assurer que toutes les signatures sont présentées et qu'elles sont dans le bon ordre en utilisant 3 lignes simples:
let aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey )) then 1 else 0 let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey )) then 1 else 0 let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey )) then 1 else 0
Eh bien, la dernière étape sera de vérifier qu'au moins 2 signatures sont soumises.
aliceSigned + bobSigned + cooperSigned >= 2
L'ensemble du contrat de multi-signature 2 sur 3 ressemble à ceci:
# let alicePubKey = base58'B1Yz7fH1bJ2gVDjyJnuyKNTdMFARkKEpV' let bobPubKey = base58'7hghYeWtiekfebgAcuCg9ai2NXbRreNzc' let cooperPubKey = base58'BVqYXrapgJP9atQccdBPAgJPwHDKkh6A8' # let aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey )) then 1 else 0 let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey )) then 1 else 0 let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey )) then 1 else 0 # , 2 aliceSigned + bobSigned + cooperSigned >= 2
Remarque: il n'y a pas de mots-clés comme return
dans le code, car la dernière ligne exécutée est considérée comme le résultat du script, et c'est pourquoi il doit toujours retourner true
ou false
En comparaison, le contrat multi-signature Ethereum commun semble beaucoup plus compliqué . Même des variantes relativement simples ressemblent à ceci:
pragma solidity ^0.4.22; contract SimpleMultiSig { uint public nonce; // (only) mutable state uint public threshold; // immutable state mapping (address => bool) isOwner; // immutable state address[] public ownersArr; // immutable state // Note that owners_ must be strictly increasing, in order to prevent duplicates constructor(uint threshold_, address[] owners_) public { require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ >= 0); address lastAdd = address(0); for (uint i = 0; i < owners_.length; i++) { require(owners_[i] > lastAdd); isOwner[owners_[i]] = true; lastAdd = owners_[i]; } ownersArr = owners_; threshold = threshold_; } // Note that address recovered from signatures must be strictly increasing, in order to prevent duplicates function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) public { require(sigR.length == threshold); require(sigR.length == sigS.length && sigR.length == sigV.length); // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191 bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce); address lastAdd = address(0); // cannot have address(0) as an owner for (uint i = 0; i < threshold; i++) { address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]); require(recovered > lastAdd && isOwner[recovered]); lastAdd = recovered; } // If we make it here all signatures are accounted for. // The address.call() syntax is no longer recommended, see: // https://github.com/ethereum/solidity/issues/2884 nonce = nonce + 1; bool success = false; assembly { success := call(gas, destination, value, add(data, 0x20), mload(data), 0, 0) } require(success); } function () payable public {} }
L'IDE a une console intégrée qui vous permet de compiler immédiatement un contrat, de le déployer, de créer des transactions et de voir le résultat de l'exécution. Et si vous voulez travailler sérieusement avec les contrats, je vous recommande de regarder les bibliothèques pour différents langages et le plugin pour Visual Studio Code .
Si vos mains vous démangent, à la fin de l'article, il y a les liens les plus importants avec lesquels commencer la plongée.
Le système est plus puissant que la langue
La blockchain Waves a des types de données spéciaux pour stocker des données - Transactions de données . Ils fonctionnent comme un stockage de valeur-clé associé à un compte, c'est-à-dire, dans un sens, c'est l'état du compte.

La date de transaction peut contenir des chaînes, des nombres, des valeurs booléennes et des tableaux d'octets jusqu'à 32 Ko par clé. Un exemple d'utilisation des transactions de données, qui vous permet d'envoyer une transaction uniquement si le stockage de valeurs-clés du compte contient déjà le numéro 42 sur la key
:
let keyName = "key" match (tx) { case tx:DataTransaction => let x = extract(getInteger(tx.sender, keyName)) x == 42 case _ => false }
Grâce à Data Transaction, les comptes intelligents deviennent un outil extrêmement puissant qui vous permet de travailler avec des oracles, de gérer l'état et de décrire facilement les comportements.

Cet article décrit comment vous pouvez implémenter des NFT (jetons non fongibles) à l'aide de transactions de données et d'un contrat intelligent qui contrôle l'état. Par conséquent, le style du compte contiendra des entrées du formulaire:
+------------+-----------------------------------------------+ | Token Name | Owner Publc Key | +------------+-----------------------------------------------+ | "Token #1" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | | "Token #2" | "3tNLxyJnyxLzDkMkqiZmUjRqXe1UuwFeSyQ14GRYnGL" | | "Token #3" | "3wH7rENpbS78uohErXHq77yKzQwRyKBYhzCR9nKU17q" | | "Token #4" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | | "Token #5" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | +------------+-----------------------------------------------+
Le contrat NFT lui-même semble extrêmement simple:
match tx { case dt: DataTransaction => let oldOwner = extract(getString(dt.sender, dt.data[0].key)) let newOwner = getBinary(dt.data, 0) size(dt.data) == 1 && sigVerify(dt.bodyBytes, dt.proofs[0], fromBase58String(oldOwner)) case _ => false }
Et ensuite?
Le développement ultérieur des contrats intelligents Waves est Ride4DApps , qui permettra d'appeler des contrats sur d'autres comptes, et un langage (ou système) complet de Turing qui vous permettra de résoudre tous les types de tâches, de déclencher d'autres tâches, etc.
Une autre direction intéressante pour le développement de contrats intelligents dans l'écosystème Waves est Smart Assets, qui fonctionne sur un principe similaire - les contrats de Turing incomplets qui se rapportent au jeton. Le contrat contrôle les conditions dans lesquelles les transactions de jetons peuvent être effectuées. Par exemple, avec leur aide, il sera possible de geler les jetons à une certaine hauteur de blockchain ou d'interdire le trading de jetons p2p. Vous pouvez en savoir plus sur les actifs intelligents dans le blog .
Eh bien, à la fin, je vais à nouveau vous donner une liste de ce qui sera nécessaire pour commencer à travailler avec des contrats intelligents sur le réseau Waves.
- La documentation
- IDE avec console
- Livre blanc pour les plus curieux