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(), “FOO”, “BAR”) 

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