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.
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
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
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;
}
}
El constructor:
- Toma un parámetro de entrada.
- Incluye una cláusula
require
que podría abortar el despliegue. - 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
Verifica que el archivo .env
se configuró correctamente al obtener la dirección de tu cuenta:
accounts = await ethers.getSigners()
accounts[0].address
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))
Esto incluirá los valores de bytecode
y deployedBytecode
:
compile.bytecode
compile.deployedBytecode
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
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)
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
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()
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
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
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
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")
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
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
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)