Test des contrats intelligents Ethereum sur Go: au revoir, JavaScript

image
Je tiens à remercier mes collÚgues: Sergey Nemesh, Mikhail Popsuyev, Evgeny Babich et Igor Titarenko pour les consultations, les commentaires et les tests. Je tiens également à remercier l'équipe PolySwarm pour avoir développé la version originale du Périgord.

Ceci est une traduction de mon premier article en anglais moyen publié


Les tests ont toujours fait partie intégrante du développement logiciel, bien que ce ne soit pas le plus agréable. En ce qui concerne les contrats intelligents, des tests rigoureux sont requis avec une attention exceptionnelle aux détails, comme les erreurs seront impossibles à corriger aprÚs le déploiement sur le réseau blockchain. Au cours des derniÚres années, la communauté Ethereum a créé de nombreux outils pour développer des contrats intelligents. Certains d'entre eux ne sont pas devenus populaires, par exemple, Vyper - un dialecte Python pour écrire des contrats intelligents. D'autres, comme Solidity, sont devenus une norme reconnue. La documentation la plus complÚte sur les tests de contrats intelligents à ce jour fournit un tas de truffes et de ganaches. Ces deux outils ont une bonne documentation, de nombreux cas ont déjà été décidés sur Stack Overflow et des ressources similaires. Cependant, cette approche présente un inconvénient important: pour écrire des tests, vous devez utiliser Node.js.


PiĂšges JavaScript


MĂȘme si vous n'ĂȘtes pas un fan des langages de programmation typĂ©s statiques et que vous aimez JavaScript, envisagez de faire une faute de frappe et de commencer Ă  comparer le rĂ©sultat d'une fonction qui renvoie une chaĂźne avec une valeur boolĂ©enne en utilisant la mĂ©thode dĂ©prĂ©ciĂ©e d'Ă©gal au lieu de strictEqual.


let proposalExists = await voting.checkProposal(); assert.equal(proposalExists, true, 'Proposal should exist'); 

Si checkProposal renvoie la chaĂźne «oui» ou «non», vous les convertirez toujours en true. La frappe dynamique masque bon nombre de ces piĂšges, et mĂȘme les programmeurs expĂ©rimentĂ©s peuvent commettre de telles erreurs lorsqu'ils travaillent sur un grand projet ou en Ă©quipe avec d'autres dĂ©veloppeurs qui peuvent apporter des modifications au code sans le signaler.


La saisie statique de Go permet d'Ă©viter de telles erreurs. De plus, l'utilisation du langage Go au lieu de Node.js pour les tests est le rĂȘve de tout dĂ©veloppeur Go qui commence Ă  travailler avec des contrats intelligents.


Mon Ă©quipe dĂ©veloppait un systĂšme d'investissement basĂ© sur des contrats intelligents avec une architecture trĂšs complexe. Le systĂšme de contrat intelligent contenait plus de 2 000 lignes de code. Étant donnĂ© que la majeure partie de l'Ă©quipe Ă©tait constituĂ©e de dĂ©veloppeurs Go, les tests sur Go Ă©taient prĂ©fĂ©rables Ă  Node.js.


Le premier environnement pour tester les contrats intelligents sur Go


En 2017, PolySwarm a développé Perigord , un outil similaire à Truffle, utilisant Go au lieu de JavaScript. Malheureusement, ce projet n'est plus supporté, il n'a qu'un seul tutoriel avec des exemples trÚs simples. De plus, il ne prend pas en charge l'intégration avec Ganache (une chaßne de blocs privée pour développer Ethereum avec une interface graphique trÚs pratique). Nous avons amélioré Perigord en éliminant les bogues et en introduisant deux nouvelles fonctions: générer des portefeuilles à partir du code mnémonique et les utiliser pour tester et se connecter à la blockchain Ganache. Vous pouvez lire le code source ici .


Le didacticiel Périgord d'origine ne contient que l'exemple le plus simple d'invoquer un contrat pour modifier une valeur unique. Cependant, dans le monde réel, vous devrez également appeler un contrat à partir de différents portefeuilles, envoyer et recevoir de l'éther, etc. Maintenant, vous pouvez faire tout cela en utilisant le Périgord avancé et la bonne vieille Ganache. Vous trouverez ci-dessous des conseils détaillés sur le développement et le test de contrats intelligents à l'aide de Périgord et Ganache.


Utilisation du Périgord avancé: un guide complet


Pour utiliser Perigord, vous devez installer Go 1.7+, solc, abigen et Ganache. Veuillez consulter la documentation de votre systĂšme d'exploitation.


Installez Périgord comme suit:


 $ go get gitlab.com/go-truffle/enhanced-perigord $ go build 

AprĂšs cela, vous pouvez utiliser la commande perigord:


 $ perigord A golang development environment for Ethereum Usage: perigord [command] Available Commands: add Add a new contract or test to the project build (alias for compile) compile Compile contract source files deploy (alias for migrate) generate (alias for compile) help Help about any command init Initialize new Ethereum project with example contracts and tests migrate Run migrations to deploy contracts test Run go and solidity tests Flags: -h, --help help for perigord Use "perigord [command] --help" for more information about a command. 

Nous allons maintenant créer un contrat intelligent Market simple pour démontrer les options de test disponibles.


Pour démarrer un projet, entrez les informations suivantes dans le terminal:


 $ perigord init market 

Le projet apparaßtra dans le dossier src / dans GOPATH. Déplacez le projet vers un autre dossier et mettez à jour les chemins d'importation si vous souhaitez modifier son emplacement. Voyons ce qu'il y a dans le dossier market /.


 $ tree . ├── contracts │ └── Foo.sol ├── generate.go ├── main.go ├── migrations │ └── 1_Migrations.go ├── perigord.yaml ├── stub │ ├── README.md │ └── main.go ├── stub_test.go └── tests └── Foo.go 

TrÚs similaire au projet créé dans Truffle, n'est-ce pas? Mais c'est parti! Voyons ce qu'il y a dans le fichier de configuration perigord.yaml.


 networks: dev: url: /tmp/geth_private_testnet/geth.ipc keystore: /tmp/geth_private_testnet/keystore passphrase: blah mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat num_accounts: 10 

Pour les tests, vous pouvez utiliser à la fois le réseau geth privé et les fichiers de portefeuille, et vous connecter à Ganache. Ces options s'excluent mutuellement. Nous prenons les mnémoniques par défaut, générons 10 comptes et nous connectons à Ganache. Remplacez le code dans perigord.yaml par:


 networks: dev: url: HTTP://127.0.0.1:7545 mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat num_accounts: 10 

HTTP http://127.0.0.1:7545 - l'adresse standard du serveur Ganache RPC. Veuillez noter que vous pouvez créer n'importe quel nombre de comptes pour les tests, mais seuls les comptes générés dans la Ganache (GUI) contiendront des fonds.


Nous allons créer un contrat appelé Market.sol. Il peut tenir un registre des paires d'adresses, dont l'une envoie des fonds au compte du contrat, et l'autre a le droit de recevoir des fonds lorsque le propriétaire du contrat autorise une telle transaction. Par exemple, deux participants ne se font pas confiance, mais font confiance au propriétaire du contrat, qui décide si une certaine condition est remplie. L'exemple implémente plusieurs fonctions de base à des fins de démonstration.


Ajoutez un contact au projet:


 $ perigord add contract Market 

Le suffixe .sol sera ajouté automatiquement. Vous pouvez également ajouter d'autres contrats ou supprimer l'exemple de contrat Foo.sol. Pendant que vous travaillez chez GOPATH, vous pouvez utiliser des contrats d'importation pour créer des structures complexes. Nous aurons trois fichiers Solidity: le contrat Market principal, les contrats auxiliaires Ownable et Migrations, et la bibliothÚque SafeMath. Vous pouvez trouver le code source ici .


Maintenant, le projet a la structure suivante:


 . ├── contracts │ ├── Market.sol │ ├── Ownable.sol │ └── SafeMath.sol ├── generate.go ├── main.go ├── migrations │ └── 1_Migrations.go ├── perigord.yaml ├── stub │ ├── README.md │ └── main.go ├── stub_test.go └── tests └── Foo.go 

Générez le bytecode EVM, les liaisons ABI et Go:


 $ perigord build 

Ajoutez les migrations de tous les contrats que vous déploierez. Parce que nous déployons uniquement Market.sol, nous n'avons besoin que d'une nouvelle migration:


 $ perigord add migration Market 

Notre contrat ne contient pas de constructeur qui accepte les paramĂštres. Si vous devez transmettre des paramĂštres au constructeur, ajoutez-les Ă  la fonction Deploy {NewContract} dans le fichier de migration:


 address, transaction, contract, err := bindings.Deploy{NewContract}(auth, network.Client(), “FOO”, “BAR”) 

Supprimez l'exemple de fichier Foo.go et ajoutez un fichier de test pour notre contrat:


 $ perigord add test Market 

Pour utiliser des portefeuilles déterministes, nous devons lire les mnémoniques du fichier de configuration:


 func getMnemonic() string { viper.SetConfigFile("perigord.yaml") if err := viper.ReadInConfig(); err != nil { log.Fatal() } mnemonic := viper.GetStringMapString("networks.dev")["mnemonic"] return mnemonic } 

La fonction d'assistance suivante est utilisée pour obtenir l'adresse réseau:


 func getNetworkAddress() string { viper.SetConfigFile("perigord.yaml") if err := viper.ReadInConfig(); err != nil { log.Fatal() } networkAddr := viper.GetStringMapString("networks.dev")["url"] return networkAddr } 

Une autre fonction d'assistance dont nous aurons besoin est sendETH, nous l'utiliserons pour transférer Ether depuis l'un des portefeuilles générés (indiqué par l'index) vers n'importe quelle adresse Ethereum:


 func sendETH(s *MarketSuite, c *ethclient.Client, sender int, receiver common.Address, value *big.Int) { senderAcc := s.network.Accounts()[sender].Address nonce, err := c.PendingNonceAt(context.Background(), senderAcc) if err != nil { log.Fatal(err) } gasLimit := uint64(6721975) // in units gasPrice := big.NewInt(3700000000) wallet, err := hdwallet.NewFromMnemonic(getMnemonic()) toAddress := receiver var data []byte tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data) chainID, err := c.NetworkID(context.Background()) if err != nil { log.Fatal(err) } privateKey, err := wallet.PrivateKey(s.network.Accounts()[sender]) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey) if err != nil { log.Fatal(err) } ts := types.Transactions{signedTx} rawTx := hex.EncodeToString(ts.GetRlp(0)) var trx *types.Transaction rawTxBytes, err := hex.DecodeString(rawTx) err = rlp.DecodeBytes(rawTxBytes, &trx) err = c.SendTransaction(context.Background(), trx) if err != nil { log.Fatal(err) } } 

Les deux fonctions suivantes sont utilisées pour modifier un appel de contrat:


 func ensureAuth(auth bind.TransactOpts) *bind.TransactOpts { return &bind.TransactOpts{ auth.From, auth.Nonce, auth.Signer, auth.Value, auth.GasPrice, auth.GasLimit, auth.Context} } func changeAuth(s MarketSuite, account int) bind.TransactOpts { return *s.network.NewTransactor(s.network.Accounts()[account]) } 

Procédure de test


Pour un appel, nous créons un contratSessionActual pour un contrat spécifique. Parce que le contrat a un propriétaire, nous pouvons obtenir son adresse et vérifier s'il correspond au compte Ganache zéro par défaut. Nous procéderons comme suit (nous omettons la gestion des erreurs pour économiser de l'espace):


 contractSession := contract.Session("Market") c.Assert(contractSession, NotNil) contractSessionActual, ok := contractSession.(*bindings.MarketSession) c.Assert(ok, Equals, true) c.Assert(contractSessionActual, NotNil) owner, _ := contractSessionActual.Owner() account0 := s.network.Accounts()[0] c.Assert(owner.Hex(), Equals, account0.Address.Hex()) //Owner account is account 0 

La prochaine fonctionnalité utile consiste à changer le portefeuille à l'origine du contrat:


 ownerInd := 0 sender := 5 receiver := 6 senderAcc := s.network.Accounts()[sender].Address receiverAcc := s.network.Accounts()[receiver].Address //Call contract on behalf of its owner auth := changeAuth(*s, ownerInd) _, err = contractSessionActual.Contract.SetSenderReceiverPair(ensureAuth(auth), senderAcc, receiverAcc) 

Parce que l'une des principales fonctions utilisées dans les tests est la modification du contrat d'appel, effectuons un paiement au nom de l'expéditeur:


 auth = changeAuth(*s, sender) //Change auth fo senderAcc to make a deposit on behalf of the sender client, _ := ethclient.Dial(getNetworkAddress()) //Let's check the current balance balance, _ := client.BalanceAt(context.Background(), contract.AddressOf("Market"), nil) c.Assert(balance.Int64(), Equals, big.NewInt(0).Int64()) //Balance should be 0 //Let's transfer 3 ETH to the contract on behalf of the sender value := big.NewInt(3000000000000000000) // in wei (3 eth) contractReceiver := contract.AddressOf("Market") sendETH(s, client, sender, contractReceiver, value) balance2, _ := client.BalanceAt(context.Background(), contract.AddressOf("Market"), nil) c.Assert(balance2.Int64(), Equals, value.Int64()) //Balance should be 3 ETH 

Le code de test complet est ici .


Ouvrez maintenant stub_test.go et assurez-vous que toutes les importations pointent vers votre projet actuel. Dans notre cas, c'est:


 import ( _ "market/migrations" _ "market/tests" "testing" . "gopkg.in/check.v1" ) 

Exécutez les tests:


 $ perigord test 

Si tout est fait correctement, aprÚs la fin du test, il y aura un résultat similaire:


 Running migration 2 Running migration 3 OK: 1 passed PASS ok market 0.657s 

Si vous rencontrez des problÚmes, téléchargez les fichiers source et répétez les étapes décrites dans ce guide.


En conclusion


Perigord est un outil de test fiable Ă©crit dans votre langue prĂ©fĂ©rĂ©e. Il crĂ©e la mĂȘme structure de projet que Truffle et a les mĂȘmes Ă©quipes, vous n'aurez donc pas besoin de rĂ©apprendre. Le typage statique et une signature de fonction sans ambiguĂŻtĂ© vous permettent de dĂ©velopper et d'effectuer rapidement le dĂ©bogage, ainsi que de vous protĂ©ger de maniĂšre significative contre les fautes de frappe dans les arguments. En PĂ©rigord, vous pouvez facilement migrer un projet existant vers Truffle (tout ce que vous devez faire est de copier et coller les fichiers de contrat dans le dossier appropriĂ© et ajouter des tests), et Ă©galement dĂ©marrer un tout nouveau projet avec des tests Ă©crits en Go.


J'espÚre que le travail commencé par l'équipe PolySwarm et poursuivi par Inn4Science sera utile à la communauté Go et libérera des heures de test et de débogage à l'aide d'outils moins pratiques.

Source: https://habr.com/ru/post/fr445254/


All Articles