Testando contratos inteligentes da Ethereum on Go: adeus, JavaScript

imagem
Quero agradecer aos meus colegas: Sergey Nemesh, Mikhail Popsuyev, Evgeny Babich e Igor Titarenko pelas consultas, feedback e testes. Também quero agradecer à equipe do PolySwarm por desenvolver a versão original do Perigord.

Esta Ă© uma tradução do meu primeiro artigo publicado em inglĂȘs mĂ©dio


Os testes sempre fizeram parte integrante do desenvolvimento de software, embora nĂŁo sejam os mais agradĂĄveis. Quando se trata de contratos inteligentes, sĂŁo necessĂĄrios testes rigorosos com uma atenção excepcional aos detalhes, como os erros serĂŁo impossĂ­veis de corrigir apĂłs a implantação na rede blockchain. Nos Ășltimos anos, a comunidade Ethereum criou muitas ferramentas para o desenvolvimento de contratos inteligentes. Alguns deles nĂŁo se tornaram populares, por exemplo, o Vyper - um dialeto em Python para escrever contratos inteligentes. Outros, como Solidity, tornaram-se um padrĂŁo reconhecido. A documentação mais extensa sobre o teste de contratos inteligentes atĂ© o momento fornece um monte de Truffle & Ganache. Ambas as ferramentas possuem boa documentação, muitos casos jĂĄ foram decididos no Stack Overflow e recursos semelhantes. No entanto, essa abordagem tem uma desvantagem importante: para escrever testes, vocĂȘ precisa usar o Node.js.


Armadilhas de JavaScript


Mesmo que vocĂȘ nĂŁo seja fĂŁ de linguagens de programação estĂĄticas e adora JavaScript, considere fazer um erro de digitação e começar a comparar o resultado de uma função que retorna uma string com um valor booleano usando o mĂ©todo equal obsoleto em vez de strictEqual.


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

Se checkProposal retornar a string "yes" ou "no", vocĂȘ sempre as converterĂĄ em true. A digitação dinĂąmica oculta muitas dessas armadilhas, e mesmo programadores experientes podem cometer esses erros ao trabalhar em um projeto grande ou em uma equipe com outros desenvolvedores que podem fazer alteraçÔes no cĂłdigo e nĂŁo reportĂĄ-lo.


A digitação eståtica do Go ajuda a evitar esses erros. Além disso, o uso da linguagem Go em vez do Node.js para teste é o sonho de qualquer desenvolvedor Go que comece a trabalhar com contratos inteligentes.


Minha equipe estava desenvolvendo um sistema de investimento baseado em contratos inteligentes com uma arquitetura muito complexa. O sistema de contrato inteligente continha mais de 2.000 linhas de cĂłdigo. Como a maior parte da equipe era de desenvolvedores Go, os testes no Go eram preferĂ­veis aos Node.js.


O primeiro ambiente para testar contratos inteligentes no Go


Em 2017, a PolySwarm desenvolveu o Perigord , uma ferramenta semelhante ao Truffle, usando Go em vez de JavaScript. Infelizmente, este projeto nĂŁo Ă© mais suportado, ele possui apenas um tutorial com exemplos muito simples. AlĂ©m disso, ele nĂŁo suporta a integração com o Ganache (uma blockchain privada para o desenvolvimento do Ethereum com uma GUI muito conveniente). Melhoramos o Perigord eliminando bugs e introduzindo duas novas funçÔes: gerar carteiras a partir do cĂłdigo mnemĂŽnico e usĂĄ-las para testar e conectar-se Ă  blockchain Ganache. VocĂȘ pode ler o cĂłdigo fonte aqui .


O tutorial original do Perigord contĂ©m apenas o exemplo mais simples de chamar um contrato para alterar um Ășnico valor. No entanto, no mundo real, vocĂȘ tambĂ©m precisarĂĄ chamar um contrato de diferentes carteiras, enviar e receber Ether, etc. Agora vocĂȘ pode fazer tudo isso usando o Perigord avançado e o bom e velho Ganache. Abaixo, vocĂȘ encontrarĂĄ orientaçÔes detalhadas sobre o desenvolvimento e teste de contratos inteligentes usando a Perigord & Ganache.


Usando o Perigord avançado: um guia completo


Para usar o Perigord, vocĂȘ precisa instalar o Go 1.7+, solc, abigen e Ganache. Por favor, consulte a documentação do seu sistema operacional.


Instale o Perigord da seguinte maneira:


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

Depois disso, vocĂȘ pode usar o comando 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. 

Agora, criaremos um contrato inteligente de mercado simples para demonstrar as opçÔes de teste disponíveis.


Para iniciar um projeto, digite o seguinte no terminal:


 $ perigord init market 

O projeto aparecerå na pasta src / em GOPATH. Mova o projeto para outra pasta e atualize os caminhos de importação se desejar alterar sua localização. Vamos ver o que estå no mercado / pasta.


 $ 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 

Muito parecido com o projeto criado no Truffle, não é? Mas estå tudo pronto! Vamos ver o que estå no arquivo de configuração 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 

Para o teste, vocĂȘ pode usar os arquivos de rede e carteira geth privados e conectar-se ao Ganache. Essas opçÔes sĂŁo mutuamente exclusivas. Tomamos as mnemĂŽnicas padrĂŁo, geramos 10 contas e nos conectamos ao Ganache. Substitua o cĂłdigo em perigord.yaml por:


 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 - o endereço padrĂŁo do servidor Ganache RPC. Observe que vocĂȘ pode criar qualquer nĂșmero de contas para teste, mas apenas as contas geradas na Ganache (GUI) conterĂŁo fundos.


Criaremos um contrato chamado Market.sol. Ele pode manter um registro de pares de endereços, um dos quais envia fundos para a conta do contrato e o outro tem o direito de receber fundos quando o proprietårio do contrato autorizar essa transação. Por exemplo, dois participantes não confiam um no outro, mas confiam no proprietårio do contrato, que decide se uma determinada condição é atendida. O exemplo implementa vårias funçÔes båsicas para fins de demonstração.


Adicione um contato ao projeto:


 $ perigord add contract Market 

O postfix .sol serĂĄ adicionado automaticamente. VocĂȘ tambĂ©m pode adicionar outros contratos ou excluir o contrato de amostra Foo.sol. Enquanto vocĂȘ trabalha na GOPATH, pode usar contratos de importação para criar estruturas complexas. Teremos trĂȘs arquivos do Solidity: o contrato principal do Market, os contratos auxiliares Ownable e Migrations e a biblioteca SafeMath. VocĂȘ pode encontrar o cĂłdigo fonte aqui .


Agora o projeto tem a seguinte estrutura:


 . ├── 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 

Gere ligaçÔes de bytecode, ABI e Go do EVM:


 $ perigord build 

Adicione as migraçÔes de todos os contratos que vocĂȘ implantarĂĄ. Porque implantamos apenas o Market.sol, precisamos de apenas uma nova migração:


 $ perigord add migration Market 

Nosso contrato nĂŁo contĂ©m um construtor que aceite parĂąmetros. Se vocĂȘ precisar passar parĂąmetros para o construtor, adicione-os Ă  função Deploy {NewContract} no arquivo de migração:


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

Exclua o arquivo de exemplo Foo.go e adicione um arquivo de teste para o nosso contrato:


 $ perigord add test Market 

Para usar carteiras determinísticas, precisamos ler os mnemÎnicos do arquivo de configuração:


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

A seguinte função auxiliar é usada para obter o endereço de rede:


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

Outra função auxiliar que precisaremos é sendETH, a usaremos para transferir o Ether de uma das carteiras geradas (indicadas pelo índice) para qualquer endereço 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) } } 

As duas funçÔes a seguir são usadas para modificar uma chamada de contrato:


 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]) } 

Procedimento de teste


Para uma chamada, criamos um contractSessionActual para um contrato específico. Porque Se o contrato tiver um proprietårio, podemos obter seu endereço e verificar se ele corresponde à conta Ganache zero padrão. Faremos isso da seguinte maneira (omitiremos o tratamento de erros para economizar espaço):


 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 

O prĂłximo recurso Ăștil Ă© alterar a carteira que causa o contrato:


 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) 

Porque uma das principais funçÔes usadas no teste é alterar o contrato de chamada, vamos fazer um pagamento em nome do remetente:


 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 

O cĂłdigo de teste completo estĂĄ aqui .


Agora abra stub_test.go e verifique se todas as importaçÔes apontam para o seu projeto atual. No nosso caso, é:


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

Execute os testes:


 $ perigord test 

Se tudo for feito corretamente, depois do final do teste, haverĂĄ um resultado semelhante:


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

Se vocĂȘ tiver algum problema, baixe os arquivos de origem e repita as etapas descritas neste guia.


Em conclusĂŁo


O Perigord Ă© uma ferramenta de teste confiĂĄvel, escrita no seu idioma favorito. Ele cria a mesma estrutura de projeto que o Truffle e tem as mesmas equipes, portanto vocĂȘ nĂŁo precisa reaprender. A digitação estĂĄtica e uma assinatura de função inequĂ­voca permitem que vocĂȘ desenvolva e execute a depuração rapidamente, alĂ©m de proteger significativamente contra erros de digitação nos argumentos. No Perigord, vocĂȘ pode migrar facilmente um projeto existente para o Truffle (tudo o que vocĂȘ precisa fazer Ă© copiar e colar os arquivos do contrato na pasta apropriada e adicionar testes), alĂ©m de iniciar um projeto completamente novo com os testes escritos em Go.


Espero que o trabalho iniciado pela equipe PolySwarm e continuado pela Inn4Science seja Ăștil para a comunidade Go e livre de horas de teste e depuração com ferramentas menos convenientes.

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


All Articles