As transações de implantação de contrato são únicas em vários aspectos. Neste artigo, dissecamos tais transações, examinando de perto o bytecode que dá origem a um novo contrato.
Com quem estamos conversando?
As transações são uma troca entre duas partes.
Ao transacionar a criptomoeda nativa, os endereços de/para identificam os proprietários iniciais e finais da moeda. Ao executar um contrato inteligente, o remetente da transação solicita que um contrato inteligente execute uma função. Assim, o contrato torna-se o destinatário da transação.
Da mesma forma, na implantação do contrato, o remetente da transação é quem cria o novo contrato na cadeia. Mas quem é o destinatário? É a própria blockchain! A blockchain designa um endereço especial para qualquer pessoa solicitar tal serviço. Este endereço especial é o endereço null (nulo).
Aqui está o código, implante-o
Tendo uma contraparte de implantação, poderíamos simplesmente enviar o código do contrato inteligente para o endereço null, certo? Não exatamente! A carga útil dos dados da transação de implantação é um pouco mais complicada e por um bom motivo.
O motivo é o construtor, um trecho de código encarregado da validação da implantação e da inicialização do contrato. Um construtor pode…
- …abortar a implantação se a validação falhar.
- …fornecer inicialização única para variáveis de estado.
Além disso, o construtor só é executado na implantação. Portanto, não há sentido em salvá-lo na cadeia.
Agora estamos prontos para definir a carga de dados da transação de implantação, que claramente deve integrar a lógica do construtor.
De fato, a carga de implantação é uma versão ligeiramente modificada do código do construtor. Ela inclui toda a lógica do construtor, mas quando executada com sucesso, retorna a parte do contrato inteligente a ser escrita na cadeia.
Falo de uma parte do contrato inteligente, pois como já foi dito, o construtor não é escrito na cadeia. A figura mostra a blockchain recebendo uma transação de implantação e executando o construtor. Por fim, isso retorna com sucesso o código do contrato ou reverte a transação, cancelando a implantação.
Implantação de contrato inteligente
Implantando um contrato
Agora é hora de investigar a carga de implantação da transação. Eu preparei um projeto do Hardhat com um contrato simples.
Clonar e instalar dependências
Para prosseguir, clone e instale as dependências do projeto:
git clone [email protected]:kaxxa123/BlockchainThings.git
cd ./BlockchainThings/ContractBytecode
npm install
Configurar e compilar
Em seguida, precisamos personalizar um pouco o projeto:
- Este código será executado em qualquer cadeia compatível com a EVM. Aqui o Hardhat foi configurado para rodar na rede de testes Fuji, da Avalanche. A Fuji foi escolhida por causa de sua torneira fácil de usar, que nos fornece 2 AVAX sem nenhum problema. Portanto, comece solicitando alguns AVAX de teste.
Na pasta ContractBytecode, renomeie o arquivo:
De: ./BlockchainThings/ContractBytecode/.env_template
Para: ./BlockchainThings/ContractBytecode/.envEdite o .env para definir a chave privada da conta para a qual o AVAX foi enviado. Uma vez pronto, o conteúdo deve ter a seguinte aparência:
PRIVATE_KEY_1=”0x1234567890abcd…”
Agora estamos prontos para compilar o projeto:
npx hardhat compile
O código
Em seguida, verifique o código do contrato com o qual estaremos trabalhando.
pragma solidity 0.8.18;
contract Demo {
address public owner;
uint public counter;
constructor(uint start) payable {
require (start > 100, "Too small");
owner = msg.sender;
counter = start;
}
function increase() external {
++counter;
}
}
O construtor:
- Leva um parâmetro de entrada.
- Inclui uma cláusula require que pode abortar a implantação.
- Inicializa duas variáveis de estado.
Para simplificar nosso exemplo, o construtor é marcado como payable (pagável). Caso contrário, o compilador injetaria uma segunda cláusula de exigência, garantindo que nenhuma quantia de criptomoedas seja incluída na transação de implantação. Isso tornaria o bytecode mais difícil de ser compreendido.
Implantar
Antes de nos aprofundarmos no bytecode, vamos ver quais dados são necessários ao implantá-lo usando sendTransaction.
Anteriormente, compilamos o contrato inteligente e procuramos a saída resultante em ./artifacts/contracts/Demo.sol/Demo.json.
Estamos especialmente interessados em:
bytecode - o código de contrato completo, incluindo o construtor.
deployedBytecode - a parte do código do contrato que exclui o construtor.
Em seguida, iniciamos um console do node.js conectado à Avalanche Fuji:
npx hardhat console --network fuji
Verifique se o arquivo .env foi configurado corretamente recuperando o endereço da sua conta:
accounts = await ethers.getSigners()
accounts[0].address
Carregue o arquivo de saída da compilação:
fs = require("fs")
fs.readFile('./artifacts/contracts/Demo.sol/Demo.json', 'utf8',
(err, data) => compile = JSON.parse(data))
Isso incluirá os valores bytecode e deployBytecode:
compile.bytecode
compile.deployedBytecode
Esses valores são formatados da seguinte maneira:
compile.bytecode =
Initialization Code (aka constructor) | Contract On-Chain Portion
compile.deployedBytecode =
Contract On-Chain Portion
Se o construtor não exigisse nenhum parâmetro de entrada, poderíamos enviar uma transação com compile.bytecode como carga útil. Como temos um parâmetro, ele deve ser anexado ao bytecode. Vou deixar o ethers.js fazer isso.
paramIn = 200
DemoFactory = await ethers.getContractFactory("Demo")
complete = await DemoFactory.getDeployTransaction(paramIn)
complete.data
O valor em complete.data tem tudo o que precisamos e está formatado da seguinte forma:
complete.data =
Initialization Code | Contract On-Chain Portion | Constructor Parameters
Vamos confirmar que os valores que acabamos de discutir estão realmente formatados conforme descrito…
//Remova o "0x" inicial das strings
bytecode = compile.bytecode.slice(2)
deployedBytecode = compile.deployedBytecode.slice(2)
bytecodeEx = complete.data.slice(2)
//Confirme se o bytecode termina com o deployBytecode
assert(bytecode.length - bytecode.indexOf(deployedBytecode) ==
deployedBytecode.length)
//Confirme se bytecodeEx começa com bytecode
assert(bytecodeEx.indexOf(bytecode) == 0)
//Confirme se o parâmetro de entrada do construtor corresponde à nossa entrada
//'00000000000000000000000000000000000000000000000000000000000000c8'
param = bytecodeEx.slice(bytecode.length)
assert(parseInt(param, 16) == paramIn)
Ok, agora estamos prontos para implantar o contrato usando sendTransaction com a carga útil complete.data:
trn = await accounts[0].sendTransaction({to: null, data: complete.data})
receipt = await trn.provider.getTransactionReceipt(trn.hash)
receipt.contractAddress
E verificamos a implantação, executando suas funções:
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()
O bytecode
A melhor maneira de ver como o código de inicialização inclui a lógica do construtor é percorrendo o bytecode (é um formato de código intermediário entre o código fonte, o texto que o programador consegue manipular, e o código de máquina, que o computador consegue executar). O bytecode não é fácil de ler, mas se nos prepararmos com alguns valores de referência chave, fica mais fácil. Aqui está uma tabela de valores que veremos aparecendo ao percorrer o código.
Description Computation Hex Value
bytecode length (bytecodeEx.length/2).toString(16) 21f
bytecode length excluding (bytecode.length/2).toString(16) 1ff
constructor parameters
constructor parameter ((bytecodeEx.length - 20
length bytecode.length)/2).toString(16)
contract code length (deployedBytecode.length/2) 15b
.toString(16)
contract code offset ((bytecode.length -
within bytecode stream deployedBytecode.length)/2).toString(16) a4
constructor parameter (200).toString(16) c8
condition value in (100).toString(16) 64
require (start > 100, ...)
owner state variable 0
storage key
counter state variable 1
storage key
Em seguida, percorreremos os bytes individuais, converteremos cada código de operação (opcode) usando uma tabela como esta e, para cada código de operação, calcularemos o estado da pilha (stack). Não mostro os valores mantidos na memória, mas o código é simples o suficiente para não exigir isso.
A ordem do bytecode também foi ajustada para que este possa ser lido sequencialmente. Basicamente, meu registro mostra o índice de bytecode 7c logo após o salto no índice 21 e volta para o índice 22 quando o código volta.
idx bytecode opcodes stack description
00 60 80 PUSH1 80 [80]
02 60 40 PUSH1 40 [40, 80] Save 80 to mem location 40
04 52 MSTORE []
05 60 40 PUSH1 40 [40]
07 51 MLOAD [80] Load memory location 40
08 61 01ff PUSH2 01ff [1ff, 80] 1ff - bytecode length
except ctr parameters
0b 38 CODESIZE [21f, 1ff, 80] push bytecode length 21f
0c 03 SUB [ 20, 80] Subtracting gives the ctr
parameters length
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] Copy ctr param of size 20
@ 1ff to memory location 80
Este código apenas copiou o parâmetro de entrada do construtor para a memória.
Valores da pilha:
20 - tamanho do parâmetro ctr.
80 — localização da memória do parâmetro ctr.
idx bytecode opcodes stack description
13 81 DUP2 [80, 20, 80]
14 01 ADD [a0, 80] Get memory pointer
following ctr parameter
15 60 40 PUSH1 40 [40, a0, 80]
17 81 DUP2 [a0, 40, a0, 80]
18 90 SWAP1 [40, a0, a0, 80]
19 52 MSTORE [a0, 80] Store memory pointer a0
to memory location 40
1a 61 0022 PUSH2 0022 [22, a0, 80]
1d 91 SWAP2 [80, a0, 22]
1e 61 007c PUSH2 007c [7c, 80, a0, 22]
21 56 JUMP [80, a0, 22] Jump to 7c
Valores da pilha:
a0 — próximo local de memória após o parâmetro ctr.
22 — local de “jump back” (retornar a uma posição anterior), para continuar de onde o código parou.
80 — local de memória do parâmetro ctr.
idx bytecode opcodes stack description
7c 5b JUMPDEST [80, a0, 22]
7d 60 00 PUSH1 00 [00, 80, a0, 22]
7f 60 20 PUSH1 20 [20, 00, 80, a0,
22]
81 82 DUP3 [80, 20, 00, 80,
a0, 22]
82 84 DUP5 [a0, 80, 20, 00,
80, a0, 22]
83 03 SUB [20, 20, 00, 80, Get ctr parameter size
a0, 22]
84 12 SLT [00, 00, 80, a0, Is (top < top-1)?
22]
85 15 ISZERO [01, 00, 80, a0, Is (top == 00)?
22]
86 61 008e PUSH2 008e [8e, 01, 00, 80,
a0, 22]
89 57 JUMPI [00, 80, a0, 22] If (top-1!=0) Jump to 8e
8a 60 00 PUSH1 00
8c 80 DUP1
8d fd REVERT
Este código verificou o tamanho esperado dos parâmetros ctr.
Valores da pilha:
80 — localização da memória do parâmetro ctr.
a0 — localização da memória após o parâmetro ctr.
22 — localização do “jump back”.
idx bytecode opcodes stack description
8e 5b JUMPDEST [00, 80, a0, 22]
8f 50 POP [80, a0, 22]
90 51 MLOAD [c8, a0, 22] Load ctr parameter from
memory location 80
91 91 SWAP2 [22, a0, c8]
92 90 SWAP1 [a0, 22, c8]
93 50 POP [22, c8]
94 56 JUMP [c8] Jump back to location 22
Valores da pilha:
c8 — valor do parâmetro de entrada ctr.
idx bytecode opcodes stack description
22 5b JUMPDEST [c8]
23 60 64 PUSH1 64 [64, c8] 0x64 = 100, processing...
require (start > 100, ...)
25 81 DUP2 [c8, 64, c8]
26 11 GT [01, c8] Is (c8 > 64)?
27 61 0062 PUSH2 0062 [62, 01, c8]
2a 57 JUMPI [c8] If (top-1 != 0) Jump to 62
Este código verificou a condição exigida em:
require (start > 100, “Too small”)
Se a verificação falhar, o código não retornará a uma posição anterior e a sequência de código a seguir será revertida.
Valores da pilha:
c8 — valor do parâmetro de entrada ctr.
idx bytecode opcodes description
2b 60 40 PUSH1 40 Following
2d 51 MLOAD this
2e 62 461bcd PUSH3 461bcd code
32 60 e5 PUSH1 e5 sequence
34 1b SHL it ultimately
35 81 DUP2 reverts
36 52 MSTORE as expected
37 60 20 PUSH1 20 from a
39 60 04 PUSH1 04 failed
3b 82 DUP3 require
3c 01 ADD clause
3d 52 MSTORE
3e 60 09 PUSH109
40 60 24 PUSH1 24
42 82 DUP3
43 01 ADD
44 52 MSTORE
45 68 151bdb PUSH9 151bdb
c81cdb c81cdb
585b1b 585b1b
4f 60 ba PUSH1 ba
51 1b SHL
52 60 44 PUSH1 44
54 82 DUP3
55 01 ADD
56 52 MSTORE
57 60 64 PUSH1 64
59 01 ADD
5a 60 40 PUSH1 40
5c 51 MLOAD
5d 80 DUP1
5e 91 SWAP2
5f 03 SUB
60 90 SWAP1
61 fd REVERT If require failed revert here.
Quando a condição require for satisfeita, o código continua a partir daqui...
idx bytecode opcodes stack description
62 5b JUMPDEST [c8]
63 60 00 PUSH1 00 [00, c8]
65 80 DUP1 [00, 00, c8]
66 54 SLOAD [00, 00, c8] Load state value key 00
i.e. the owner address
67 60 01 PUSH1 01 [01, 00, 00, c8]
69 60 01 PUSH1 01 [01, 01, 00, 00,
c8]
6b 60 a0 PUSH1 a0 [a0, 01, 01, 00,
00, c8]
6d 1b SHL [10000000000..., Shift (top-1) left by
01, 00, 00, c8] a0=20*8=address length
6e 03 SUB [fffffffffff..., Created a 20 byte mask!
00, 00, c8]
6f 19 NOT [fff...00000000, Invert the top value
00, 00, c8] !(top)
70 16 AND [00, 00, c8] top AND (top-1)
71 33 CALLER [addr, 00, 00, c8] get caller address
72 17 OR [addr, 00, c8]
73 90 SWAP1 [00, addr, c8]
74 55 SSTORE [c8] Store address at slot 0
owner = msg.sender
75 60 01 PUSH1 01 [01, c8]
77 55 SSTORE [] Store ctr parameter
counter = start
78 61 0095 PUSH2 0095 [95]
7b 56 JUMP [] Jump to location 95
Instruções de armazenamento:
owner = msg.sender
counter = start
idx bytecode opcodes stack description
95 5b JUMPDEST []
96 61 015b PUSH2 015b [15b] Push contract length
99 80 DUP1 [15b, 15b]
9a 61 00a4 PUSH2 00a4 [ a4, 15b, 15b] Push contract offset
within bytecode stream
9d 60 00 PUSH1 00 [00, a4, 15b, 15b] Push memory offset where
the code is to be copied
9f 39 CODECOPY [15b] Copy code length 15b
to memory offset 00 from
stream at offset a4
a0 60 00 PUSH1 00 [00, 15b] Return code to deploy
a2 f3 RETURN [] from memory offset 00
with length 15b.
a3 fe INVALID INVALID marks end of
initialization code.
contract code next
Este código lida com o caso de uma execução bem-sucedida do construtor, retornando o código do contrato inteligente para ser escrito na cadeia.
Artigo escrito por Alexander Zammit. Traduzido por Marcelo Panegali
Latest comments (0)