WEB3DEV Español

Cover image for Elementos Internos de Despliegue de Contratos Inteligentes
Paulo Gio
Paulo Gio

Posted on

Elementos Internos de Despliegue de Contratos Inteligentes

Las transacciones de despliegue de contratos son únicas en varios aspectos. En este artículo, diseccionamos una transacción de este tipo, examinando de cerca el bytecode que da vida a un nuevo contrato.

¿Con quién estamos hablando?

Las transacciones son un intercambio entre dos partes.

Al realizar transacciones con la criptomoneda nativa, las direcciones de "desde" y "hacia" identifican a los propietarios iniciales y finales de la moneda. Al ejecutar un contrato inteligente, el remitente de la transacción le pide al contrato inteligente que ejecute una función. Así, el contrato se convierte en el destinatario de la transacción.

De manera similar, en el despliegue de contratos, el remitente de la transacción es quien crea el nuevo contrato en la cadena. Pero, ¿quién es el destinatario? ¡Es la propia blockchain! La cadena de bloques designa una dirección especial para que cualquiera pueda solicitar este servicio. Esta dirección especial es la dirección nula.

¡Aquí está el código! ¡Desplieguelo!

Teniendo una contraparte de despliegue, ¿podríamos simplemente enviar el código del contrato inteligente a la dirección NULL, verdad? No exactamente. La carga útil de datos de la transacción de despliegue es un poco más complicada, y por una buena razón.

La razón es el constructor, un fragmento de código encargado de la validación del despliegue y la inicialización del contrato. Un constructor puede...

  • … abortar el despliegue si falla la validación.
  • … proporcionar inicialización única para variables de estado.

Además, el constructor sólo se ejecuta durante el despliegue. Por lo tanto, no tiene sentido guardarlo en la cadena.

Ahora estamos listos para definir la carga útil de datos de la transacción de despliegue, que claramente debe integrar la lógica del constructor.

De hecho, la carga útil de despliegue es una versión ligeramente modificada del código del constructor. Incluye toda la lógica del constructor, pero cuando se ejecuta con éxito, devuelve la parte del contrato inteligente que se escribirá en la cadena.

Hablo de una parte del contrato inteligente, ya que como se mencionó anteriormente, el constructor no se escribe en la cadena. La figura muestra la cadena de bloques recibiendo una transacción de despliegue y ejecutando el constructor. Finalmente, esto devuelve exitosamente el código del contrato o lo revierte, abortando el despliegue.

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*FYQJcsxVpWnbGYkNFgLlfg.png

Despliegue de Contrato Inteligente

Desplegando un Contrato

Ahora es el momento de investigar la carga útil de despliegue de la transacción. He preparado un proyecto de Hardhat con un contrato simple.

Clonar e Instalar Dependencias

Para seguir el proceso, clona e instala las dependencias del proyecto:

git clone [email protected]:kaxxa123/BlockchainThings.git 

cd ./BlockchainThings/ContractBytecode

npm install
Enter fullscreen mode Exit fullscreen mode

Configurar y Compilar

A continuación, necesitamos personalizar ligeramente el proyecto:

  • Este código se ejecutará en cualquier cadena compatible con EVM. Aquí, Hardhat se configuró para ejecutarse en la red de prueba Avalanche Fuji. Se eligió Fuji debido a su grifo (faucet) fácil de usar que nos proporciona 2 AVAX sin complicaciones. Así que comienza por solicitar algunos AVAX de la red de prueba.
  • En la carpeta ContractBytecode, cambia el nombre del archivo:

    De: ./BlockchainThings/ContractBytecode/.env_template

    A: ./BlockchainThings/ContractBytecode/.env

  • Edita el archivo .env para establecer la clave privada de la cuenta a la que se enviaron los AVAX. Una vez listo, el contenido debería lucir algo como esto:

    PRIVATE_KEY_1=”0x1234567890abcd…..”

Ahora estamos listos para compilar el proyecto:

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

El Código

A continuación, revisa el código del contrato con el que estaremos trabajando.

pragma solidity 0.8.18;

contract Demo {
   address public owner;
   uint public counter;

   constructor(uint start) payable {
       require (start > 100, "Demasiado pequeño");
       owner = msg.sender;
       counter = start;
   }

   function increase() external {
       ++counter;
   }
}
Enter fullscreen mode Exit fullscreen mode

El constructor:

  1. Toma un parámetro de entrada.
  2. Incluye una cláusula require que podría abortar el despliegue.
  3. Inicializa dos variables de estado.

Para simplificar nuestro ejemplo, el constructor está marcado como payable (pagadero). De lo contrario, el compilador inyectaría una segunda cláusula require asegurando que no se incluya ninguna cantidad de criptomoneda con la transacción de despliegue. Esto haría que el bytecode fuera más difícil de seguir.

Despliegue

Antes de adentrarnos en el bytecode, veamos qué datos se requieren al desplegarlo utilizando sendTransaction.

Anteriormente, compilamos el contrato inteligente, busca la salida resultante en ./artifacts/contracts/Demo.sol/Demo.json

Nos interesa especialmente:

bytecode - el código completo del contrato, incluido el constructor. deployedBytecode - la parte del código del contrato que excluye al constructor.

A continuación, ejecutamos una consola de node.js conectada a Avalanche Fuji:

npx hardhat console --network fuji
Enter fullscreen mode Exit fullscreen mode

Verifica que el archivo .env se configuró correctamente al obtener la dirección de tu cuenta:

accounts = await ethers.getSigners() 
accounts[0].address
Enter fullscreen mode Exit fullscreen mode

Carga el archivo de salida de compilación:

fs = require("fs") 
fs.readFile('./artifacts/contracts/Demo.sol/Demo.json', 'utf8', 
(err, data) => compile = JSON.parse(data))
Enter fullscreen mode Exit fullscreen mode

Esto incluirá los valores de bytecode y deployedBytecode:

compile.bytecode 
compile.deployedBytecode
Enter fullscreen mode Exit fullscreen mode

Estos valores están formateados de la siguiente manera:

compile.bytecode = Código de Inicialización (también conocido como constructor) | Parte del Contrato en la Cadena
compile.deployedBytecode = Parte del Contrato en la Cadena

Si el constructor no requiere parámetros de entrada, podríamos enviar una transacción con compile.bytecode como carga útil. Dado que tenemos un parámetro, esto debe agregarse al bytecode. Dejaré que ethers.js haga esto.

paramIn = 200
DemoFactory = await ethers.getContractFactory("Demo")
complete = await DemoFactory.getDeployTransaction(paramIn)
complete.data
Enter fullscreen mode Exit fullscreen mode

El valor en complete.data tiene todo lo que necesitamos y está formateado de la siguiente manera:

complete.data = Código de Inicialización | Parte del Contrato en la Cadena | Parámetros del Constructor

Confirmamos que los valores que acabamos de discutir están realmente formateados como se describió...

// Eliminar el prefijo "0x" de las cadenas
bytecode = compile.bytecode.slice(2)
deployedBytecode = compile.deployedBytecode.slice(2)
bytecodeEx = complete.data.slice(2)

// Confirmar que el bytecode termina con deployedBytecode
assert(bytecode.length - bytecode.indexOf(deployedBytecode) ==  
    deployedBytecode.length)

// Confirmar que bytecodeEx comienza con bytecode
assert(bytecodeEx.indexOf(bytecode) == 0)

// Confirmar que el parámetro de entrada del constructor coincide con nuestra entrada
//'00000000000000000000000000000000000000000000000000000000000000c8'
param = bytecodeEx.slice(bytecode.length)
assert(parseInt(param, 16) == paramIn)
Enter fullscreen mode Exit fullscreen mode

Bien, ahora estamos listos para desplegar el contrato utilizando sendTransaction con la carga útil complete.data:

trn = await accounts[0].sendTransaction({to: null, data: complete.data}) 
receipt = await trn.provider.getTransactionReceipt(trn.hash)

receipt.contractAddress
Enter fullscreen mode Exit fullscreen mode

Y verificamos el despliegue ejecutando sus funciones:

abi = DemoFactory.interface.fragments 
addr = receipt.contractAddress 
demo = new ethers.Contract(addr, abi, accounts[0])

await demo.owner() 
await demo.counter() 
await demo.increase() 
await demo.counter()
Enter fullscreen mode Exit fullscreen mode

El Bytecode

La mejor manera de ver cómo el código de inicialización incluye la lógica del constructor es pasando por el bytecode. El bytecode no es fácil de leer, pero si nos preparamos con algunos valores clave de referencia, se vuelve más sencillo. Aquí tienes una tabla de valores que veremos mientras recorremos el código.

Descripción Cálculo Valor Hexadecimal
longitud del bytecode (bytecodeEx.length/2).toString(16) 21f
longitud del bytecode excluyendo parámetros del constructor (bytecode.length/2).toString(16) 1ff
parámetros del constructor ((bytecodeEx.length - bytecode.length)/2).toString(16) 20
parámetro del constructor (200).toString(16) c8
valor de la condición en (100).toString(16) 64
instrucción require (start > 100, ...)
longitud del código del contrato (deployedBytecode.length/2).toString(16) 15b
desplazamiento del código del contrato ((bytecode.length - deployedBytecode.length)/2).toString(16) a4
variable de estado "owner" 0
variable de estado "counter" 1

A continuación, recorreremos los bytes individuales, convertiremos cada opcode utilizando una tabla como esta y para cada opcode determinaremos el estado de la pila. No muestro los valores almacenados en memoria, pero el código es lo suficientemente simple como para no requerir esto.

El orden del bytecode también se ajustó para que pueda leerse de manera secuencial. Básicamente, mi volcado muestra el índice del bytecode 7c justo después del salto en el índice 21 y luego vuelve al índice 22 cuando el código regresa.

idx bytecode opcodes stack descripción
00 60 80 PUSH1 80 [80] Guardar 80 en la ubicación de memoria 40
02 60 40 PUSH1 40 [40, 80] Guardar 80 en la ubicación de memoria 40
04 52 MSTORE []
05 60 40 PUSH1 40 [40]
07 51 MLOAD [80] Cargar ubicación de memoria 40
08 61 01ff PUSH2 01ff [1ff, 80] 1ff - longitud del bytecode excepto los parámetros del ctr
0b 38 CODESIZE [21f, 1ff, 80] empujar la longitud del bytecode 21f
0c 03 SUB [ 20, 80] Restar para obtener la longitud de los parámetros del ctr
0d 80 DUP1 [20, 20, 80]
0e 61 01ff PUSH2 01ff [1ff, 20, 20, 80]
11 83 DUP4 [80, 1ff, 20, 20, 80]
12 39 CODECOPY [20, 80] Copiar parámetro del ctr de tamaño 20
12 39 CODECOPY [20, 80] a la ubicación de memoria 80

Este código acaba de copiar el parámetro de entrada del constructor en la memoria.

Valores de la pila:

20 — tamaño del parámetro del ctr

80 — ubicación en memoria del parámetro del ctr
Enter fullscreen mode Exit fullscreen mode
idx bytecode opcodes stack descripción
13 81 DUP2 [80, 20, 80]
14 01 ADD [a0, 80] Obtener puntero de memoria siguiendo al parámetro del ctr
15 60 40 PUSH1 40 [40, a0, 80]
17 81 DUP2 [a0, 40, a0, 80]
18 90 SWAP1 [40, a0, a0, 80] Intercambiar valores en la pila
19 52 MSTORE [a0, 80] Guardar el puntero de memoria a0 en la ubicación de memoria 40

| 1a | 61 0022 | PUSH2 0022 | [22, a0, 80] | Empujar el valor 22 a la pila |
| 1d | 91 | SWAP2 | [80, a0, 22] | Intercambiar valores en la pila |
| 1e | 61 007c | PUSH2 007c | [7c, 80, a0, 22]| Empujar el valor 7c a la pila |
| 21 | 56 | JUMP | [80, a0, 22] | Saltar a la ubicación 7c |

Valores de la pila:

a0 — ubicación en memoria siguiente después del parámetro del

ctr 22 — ubicación para "volver" para continuar desde donde el código se detuvo

80 — ubicación en memoria del parámetro del ctr
Enter fullscreen mode Exit fullscreen mode
idx bytecode opcodes stack descripción
7c 5b JUMPDEST [80, a0, 22]
7d 60 00 PUSH1 00 [00, 80, a0, 22] Empujar el valor 00 a la pila
7f 60 20 PUSH1 20 [20, 00, 80, a0, 22] Empujar el valor 20 a la pila
81 82 DUP3 [80, 20, 00, 80, a0, 22] Duplicar el tercer elemento en la pila
82 84 DUP5 [a0, 80, 20, 00, 80, a0, 22] Duplicar el quinto elemento en la pila
83 03 SUB [ 20, 20, 00, 80, a0, 22] Restar para obtener el tamaño del parámetro del ctr
84 12 SLT [00, 00, 80, a0, 22] ¿(cima < cima-1)?
85 15 ISZERO [01, 00, 80, a0, 22] ¿(cima == 00)?
86 61 008e PUSH2 008e [8e, 01, 00, 80, a0, 22] Empujar el valor 8e a la pila
89 57 JUMPI [00, 80, a0, 22] Si (cima-1!=0) Saltar a 8e
8a 60 00 PUSH1 00 Empujar el valor 00 a la pila
8c 80 DUP1 Duplicar el primer elemento en la pila
8d fd REVERT Revertir la ejecución

Este código verificó el tamaño esperado de los parámetros ctr.

Valores de pila:

80 — ubicación de memoria del parámetro ctr
a0 — ubicación de memoria después del parámetro ctr
Enter fullscreen mode Exit fullscreen mode

22 - ubicación de "retroceso"

idx bytecode opcodes stack descripción
8e 5b JUMPDEST [00, 80, a0, 22]
8f 50 POP [80, a0, 22] Eliminar el elemento superior de la pila
90 51 MLOAD [c8, a0, 22] Cargar parámetro del ctr desde ubicación en memoria 80
91 91 SWAP2 [22, a0, c8] Intercambiar valores en la pila
92 90 SWAP1 [a0, 22, c8] Intercambiar valores en la pila
93 50 POP [22, c8] Eliminar el elemento superior de la pila
94 56 JUMP [c8] Saltar de regreso a la ubicación 22

Valores de la pila:

c8 — valor del parámetro de entrada del constructor

idx bytecode opcodes stack descripción
22 5b JUMPDEST [c8]
23 60 64 PUSH1 64 [64, c8] Empujar el valor 0x64 (100) a la pila
25 81 DUP2 [c8, 64, c8] Duplicar el segundo elemento en la pila
26 11 GT [01, c8] ¿(cima > 64)?
27 61 0062 PUSH2 0062 [62, 01, c8] Empujar el valor 0x0062 (98) a la pila
2a 57 JUMPI [c8] Si (cima-1 != 0) Saltar a la ubicación 62

Este código verificó la condición requerida en:

require (start > 100, "Demasiado pequeño")
Enter fullscreen mode Exit fullscreen mode

Si la verificación falla, el código no saltará y la secuencia de códigos que sigue se revierte.

Valores de pila:

c8 — valor del parámetro de entrada ctr
Enter fullscreen mode Exit fullscreen mode
idx bytecode opcodes descripción
2b 60 40 PUSH1 40 Empujar el valor 0x40 (64) a la pila
2d 51 MLOAD Cargar valor de memoria
2e 62 461bcd PUSH3 461bcd Empujar valor 0x461bcd a la pila
32 60 e5 PUSH1 e5 Empujar el valor 0xe5 (229) a la pila
34 1b SHL Desplazar a la izquierda
35 81 DUP2 Duplicar el segundo elemento en la pila
36 52 MSTORE Guardar en memoria
37 60 20 PUSH1 20 Empujar el valor 0x20 (32) a la pila
39 60 04 PUSH1 04 Empujar el valor 0x04 (4) a la pila
3b 82 DUP3 Duplicar el tercer elemento en la pila
3c 01 ADD Sumar
3d 52 MSTORE Guardar en memoria
3e 60 09 PUSH1 09 Empujar el valor 0x09 (9) a la pila
40 60 24 PUSH1 24 Empujar el valor 0x24 (36) a la pila
42 82 DUP3 Duplicar el tercer elemento en la pila
43 01 ADD Sumar
44 52 MSTORE Guardar en memoria
45 68 151bdb PUSH9 151bdb Empujar valor 0x151bdb a la pila
c81cdb
585b1b
4f 60 ba PUSH1 ba Empujar el valor 0xba (186) a la pila
51 1b SHL Desplazar a la izquierda
52 60 44 PUSH1 44 Empujar el valor 0x44 (68) a la pila
54 82 DUP3 Duplicar el tercer elemento en la pila
55 01 ADD Sumar
56 52 MSTORE Guardar en memoria
57 60 64 PUSH1 64 Empujar el valor 0x64 (100) a la pila
59 01 ADD Sumar
5a 60 40 PUSH1 40 Empujar el valor 0x40 (64) a la pila
5c 51 MLOAD Cargar valor de memoria
5d 80 DUP1 Duplicar el primer elemento en la pila
5e 91 SWAP2 Intercambiar valores en la pila
5f 03 SUB Restar
60 90 SWAP1 Intercambiar valores en la pila
61 fd REVERT Revertir si la condición de require falló

Cuando se cumple la condición require, el código continúa desde aquí…

idx bytecode opcodes pila descripción
62 5b JUMPDEST [c8] Etiqueta de salto (Destino de Salto)
63 60 00 PUSH1 00 [00, c8] Empujar 0x00 (0) a la pila
65 80 DUP1 [00, 00, c8] Duplicar el primer elemento
66 54 SLOAD [00, 00, c8] Cargar valor del estado en la clave 00
(es decir, la dirección del propietario)
67 60 01 PUSH1 01 [01, 00, 00, c8] Empujar 0x01 (1) a la pila
69 60 01 PUSH1 01 [01, 01, 00, 00, c8] Empujar 0x01 (1) a la pila
6b 60 a0 PUSH1 a0 [a0, 01, 01, 00, 00, c8] Empujar 0xa0 (160) a la pila
6d 1b SHL [10000000000..., 01, 00, 00, c8] Desplazar a la izquierda (a0 = 20 * 8 = longitud de dirección)
6e 03 SUB [fffffffffff..., 00, 00, c8] Restar
(Creación de una máscara de 20 bytes)
6f 19 NOT [fff...00000000, 00, 00, c8] Invertir el valor superior
(!(top))
70 16 AND [00, 00, c8] Operación lógica AND (y) entre el valor superior y el anterior
71 33 CALLER [addr, 00, 00, c8] Obtener la dirección del llamante
72 17 OR [addr, 00, c8] Operación lógica OR (o) entre el valor superior y el anterior
73 90 SWAP1 [00, addr, c8] Intercambiar elementos en la pila
74 55 SSTORE [c8] Almacenar la dirección en la ranura 0
owner = msg.sender
75 60 01 PUSH1 01 [01, c8] Empujar 0x01 (1) a la pila
77 55 SSTORE [] Almacenar el parámetro ctr
counter = start
78 61 0095 PUSH2 0095 [95] Empujar 0x0095 (149) a la pila
7b 56 JUMP [] Saltar a la ubicación 0x0095 (149)

Declaraciones de almacenamiento:

owner = msg.sender
counter = start
Enter fullscreen mode Exit fullscreen mode
idx bytecode opcodes pila descripción
95 5b JUMPDEST [] Etiqueta de salto (Destino de Salto)
96 61 015b PUSH2 015b [15b] Empujar 0x015b (347) a la pila
99 80 DUP1 [15b, 15b] Duplicar el primer elemento
9a 61 00a4 PUSH2 00a4 [ a4, 15b, 15b] Empujar 0x00a4 (164) a la pila
9d 60 00 PUSH1 00 [00, a4, 15b, 15b] Empujar 0x00 (0) a la pila
9f 39 CODECOPY [15b] Copiar longitud de código 15b a la memoria desde la secuencia en la posición a4
a0 60 00 PUSH1 00 [00, 15b] Empujar 0x00 (0) a la pila
a2 f3 RETURN [] Devolver el código desde la memoria en la posición 0 con longitud 15b
a3 fe INVALID INVALID marca el final del código de inicialización. El siguiente es el código del contrato.

Este código maneja el caso de una ejecución exitosa del constructor que devuelve el código del contrato inteligente para escribirlo en la cadena.

Artículo original publicado por Alexander Zammit. Traducción de Paulinho Giovannini.

Discussion (0)