Prueba de contratos inteligentes de Ethereum en marcha: adi贸s, JavaScript

imagen
Quiero agradecer a mis colegas: Sergey Nemesh, Mikhail Popsuyev, Evgeny Babich e Igor Titarenko por sus consultas, comentarios y pruebas. Tambi茅n quiero agradecer al equipo de PolySwarm por desarrollar la versi贸n original de Perigord.

Esta es una traducci贸n de mi primer art铆culo publicado en ingl茅s medio


Las pruebas siempre han sido una parte integral del desarrollo de software, aunque no la m谩s agradable. Cuando se trata de contratos inteligentes, se requieren pruebas rigurosas con atenci贸n excepcional al detalle, como Los errores ser谩n imposibles de corregir despu茅s de la implementaci贸n en la red blockchain. En los 煤ltimos a帽os, la comunidad Ethereum ha creado muchas herramientas para desarrollar contratos inteligentes. Algunos de ellos no se hicieron populares, por ejemplo, Vyper, un dialecto de Python para escribir contratos inteligentes. Otros, como Solidity, se han convertido en un est谩ndar reconocido. La documentaci贸n m谩s extensa sobre la prueba de contratos inteligentes hasta la fecha proporciona un mont贸n de Truffle & Ganache. Ambas herramientas tienen buena documentaci贸n, muchos casos ya se han decidido sobre Stack Overflow y recursos similares. Sin embargo, este enfoque tiene un inconveniente importante: para escribir pruebas, debe usar Node.js.


Trampas de JavaScript


Incluso si no eres un fan谩tico de los lenguajes de programaci贸n con tipos est谩ticos y te encanta JavaScript, considera hacer un error tipogr谩fico y comenzar a comparar el resultado de una funci贸n que devuelve una cadena con un valor booleano usando el m茅todo de desuso igual en lugar de estrictoEqual.


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

Si checkProposal devuelve la cadena "yes" o "no", siempre los convertir谩 en true. La escritura din谩mica oculta muchas de estas trampas, e incluso los programadores experimentados pueden cometer tales errores cuando trabajan en un proyecto grande o en un equipo con otros desarrolladores que pueden realizar cambios en el c贸digo y no informarlo.


La escritura est谩tica de Go ayuda a prevenir tales errores. Adem谩s, el uso del lenguaje Go en lugar de Node.js para las pruebas es el sue帽o de cualquier desarrollador de Go que comience a trabajar con contratos inteligentes.


Mi equipo estaba desarrollando un sistema de inversi贸n basado en contratos inteligentes con una arquitectura muy compleja. El sistema de contrato inteligente conten铆a m谩s de 2,000 l铆neas de c贸digo. Como la mayor parte del equipo eran desarrolladores de Go, las pruebas en Go eran preferibles a Node.js.


El primer entorno para probar contratos inteligentes en Go


En 2017, PolySwarm desarroll贸 Perigord , una herramienta similar a Truffle, usando Go en lugar de JavaScript. Desafortunadamente, este proyecto ya no es compatible, solo tiene un tutorial con ejemplos muy simples. Adem谩s, no admite la integraci贸n con Ganache (una cadena de bloques privada para desarrollar Ethereum con una GUI muy conveniente). Mejoramos Perigord eliminando errores e introduciendo dos nuevas funciones: generar billeteras a partir del c贸digo mnem贸nico y usarlas para probar y conectarse a la cadena de bloques de Ganache. Puedes leer el c贸digo fuente aqu铆 .


El tutorial original de Perigord contiene solo el ejemplo m谩s simple de invocar un contrato para cambiar un solo valor. Sin embargo, en el mundo real, tambi茅n deber谩 llamar a un contrato desde diferentes billeteras, enviar y recibir Ether, etc. Ahora puedes hacer todo esto usando el Perigord avanzado y el viejo Ganache. A continuaci贸n encontrar谩 una gu铆a detallada sobre el desarrollo y prueba de contratos inteligentes con Perigord & Ganache.


Uso de Perigord avanzado: una gu铆a completa


Para usar Perigord necesita instalar Go 1.7+, solc, abigen y Ganache. Consulte la documentaci贸n de su sistema operativo.


Instale Perigord de la siguiente manera:


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

Despu茅s de eso, puedes usar el 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. 

Ahora crearemos un contrato simple de mercado inteligente para demostrar las opciones de prueba disponibles.


Para comenzar un proyecto, ingrese lo siguiente en la terminal:


 $ perigord init market 

El proyecto aparecer谩 en la carpeta src / en GOPATH. Mueva el proyecto a otra carpeta y actualice las rutas de importaci贸n si desea cambiar su ubicaci贸n. Veamos qu茅 hay en el mercado / carpeta.


 $ 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 

Muy similar al proyecto creado en Truffle, 驴no? Pero todo est谩 en marcha! Veamos qu茅 hay en el archivo de configuraci贸n 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 las pruebas, puede usar tanto la red geth privada como los archivos de billetera y conectarse a Ganache. Estas opciones son mutuamente excluyentes. Tomamos la mnemotecnia predeterminada, generamos 10 cuentas y nos conectamos a Ganache. Reemplace el c贸digo en perigord.yaml con:


 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 : la direcci贸n est谩ndar del servidor Ganache RPC. Tenga en cuenta que puede crear cualquier n煤mero de cuentas para pruebas, pero solo las cuentas generadas en Ganache (GUI) contendr谩n fondos.


Crearemos un contrato llamado Market.sol. Puede mantener un registro de pares de direcciones, una de las cuales env铆a fondos a la cuenta del contrato, y la otra tiene derecho a recibir fondos cuando el propietario del contrato autoriza dicha transacci贸n. Por ejemplo, dos participantes no conf铆an el uno en el otro, sino que conf铆an en el propietario del contrato, que decide si se cumple una determinada condici贸n. El ejemplo implementa varias funciones b谩sicas para fines de demostraci贸n.


Agregar un contacto al proyecto:


 $ perigord add contract Market 

El postfix .sol se agregar谩 autom谩ticamente. Tambi茅n puede agregar otros contratos o eliminar el contrato de muestra Foo.sol. Mientras trabaja en GOPATH, puede usar contratos de importaci贸n para crear estructuras complejas. Tendremos tres archivos de Solidity: el contrato principal de Market, los contratos auxiliares Ownable and Migrations y la biblioteca SafeMath. Puedes encontrar el c贸digo fuente aqu铆 .


Ahora el proyecto tiene la siguiente estructura:


 . 鈹溾攢鈹 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 

Genere bytecode EVM, enlaces ABI y Go:


 $ perigord build 

Agregue las migraciones de todos los contratos que implementar谩. Porque solo implementamos Market.sol, solo necesitamos una nueva migraci贸n:


 $ perigord add migration Market 

Nuestro contrato no contiene un constructor que acepte par谩metros. Si necesita pasar par谩metros al constructor, agr茅guelos a la funci贸n Deploy {NewContract} en el archivo de migraci贸n:


 address, transaction, contract, err := bindings.Deploy{NewContract}(auth, network.Client(), 鈥淔OO鈥, 鈥淏AR鈥) 

Elimine el archivo de muestra Foo.go y agregue un archivo de prueba para nuestro contrato:


 $ perigord add test Market 

Para usar billeteras deterministas, necesitamos leer los mnem贸nicos del archivo de configuraci贸n:


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

La siguiente funci贸n auxiliar se utiliza para obtener la direcci贸n de red:


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

Otra funci贸n auxiliar que necesitaremos es sendETH, la usaremos para transferir Ether desde una de las billeteras generadas (indicadas por el 铆ndice) a cualquier direcci贸n de 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) } } 

Las siguientes dos funciones se utilizan para modificar una llamada 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]) } 

Procedimiento de prueba


Para una llamada, creamos un contractSessionActual para un contrato espec铆fico. Porque el contrato tiene un propietario, podemos obtener su direcci贸n y verificar si coincide con la cuenta predeterminada de Ganache cero. Haremos esto de la siguiente manera (omitiremos el manejo de errores para ahorrar espacio):


 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 siguiente caracter铆stica 煤til es cambiar la billetera que causa el 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 Una de las principales funciones utilizadas en las pruebas es cambiar el contrato de llamada, hagamos un pago en nombre del remitente:


 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 

El c贸digo de prueba completo est谩 aqu铆 .


Ahora abra stub_test.go y aseg煤rese de que todas las importaciones apunten a su proyecto actual. En nuestro caso, es:


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

Ejecute las pruebas:


 $ perigord test 

Si todo se hace correctamente, despu茅s del final de la prueba habr谩 un resultado similar:


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

Si tiene alg煤n problema, descargue los archivos de origen y repita los pasos descritos en esta gu铆a.


En conclusi贸n


Perigord es una herramienta de prueba confiable escrita en su idioma favorito. 脡l crea la misma estructura de proyecto que Truffle, y tiene los mismos equipos, por lo que no tendr谩 que volver a aprender. La escritura est谩tica y una firma de funci贸n inequ铆voca le permiten desarrollar y realizar depuraci贸n r谩pidamente, as铆 como proteger significativamente contra errores tipogr谩ficos en los argumentos. En Perigord, puede migrar f谩cilmente un proyecto existente a Truffle (todo lo que necesita hacer es copiar y pegar los archivos del contrato en la carpeta correspondiente y agregar pruebas), y tambi茅n comenzar un proyecto completamente nuevo con pruebas escritas en Go.


Espero que el trabajo iniciado por el equipo de PolySwarm y continuado por Inn4Science sea 煤til para la comunidad Go y libere horas de pruebas y depuraci贸n utilizando herramientas menos convenientes.

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


All Articles