Quando aprofundamos no contrato inteligente Solidity, obtemos informações sobre os hashes de opcode (código de operação) do bytecode e muitas outras coisas.
E se falarmos sobre bytecode, que não tem um formato que os humanos podem entender? É algo que só a EVM (máquina virtual Ethereum) consegue entender.
Então, e se em qualquer momento quisermos conhecer o fluxo interno, o processo de execução da EVM e a forma como a EVM gera a saída com base neste bytecode?
Para lidar com essa situação existe outra coisa que é chamada de opcode como PUSH1, JUMP, MSTORE, SSTORE, CALLVALUE, STOP… e outros mais.
O que é o opcode? Como é que ele funciona na EVM? Como é que o opcode se divide em partes?... vamos abordar neste artigo essas questões e muitas outras coisas. Vamos começar com a introdução ao opcode.
Visão geral do Opcode
Os opcodes são as instruções de baixo nível do programa, compreensíveis para os humanos. Cada opcode tem um valor hexadecimal correspondente, por exemplo, o "MSTORE" é "0x52", o "SSTORE" é "0x55" etc. Todos os opcodes e seus valores hexadecimais do Solidity podem ser encontrados no repositório do GitHub Pyethereum.
As linhas acima estão apenas fornecendo as informações sobre o que o opcode realmente é.
Mas a questão principal é: onde encontraremos o opcode para um código de contrato inteligente em particular?
Como já discutimos, quando compilamos o código do Solidity, o compilador gera o bytecode correspondente ao nosso código. Portanto, quando compilamos nosso código no Remix e clicamos nos detalhes do contrato, vemos muitas informações. Dentro dessas muitas informações, há uma seção que contém o opcode, algo que desejamos. Interessante, não é?
Com base no opcode, podemos entender o fluxo e a execução do contrato inteligente Solidity. Para entender o fluxo do opcode, primeiro precisamos entender em quantas partes o opcode se divide e o que cada parte faz.
As partes do opcode:
Como podemos ver no diagrama acima, nosso opcode se divide em diferentes partes. Assim, será fácil entender o fluxo dele.
Vamos discutir cada parte e entender o fluxo e a execução.
Parte da Criação
O código de tempo de execução (runtime), que é o código real do contrato, é retornado após a execução do código de criação em uma transação. O código de criação é executado apenas uma vez durante a compilação do contrato e não estará presente no opcode depois que o contrato for implantado.
Como vimos no diagrama acima, nosso constructor está presente na parte da criação, mas não na parte do tempo de execução. Isso ocorre porque todos sabemos que o constructor é executado apenas uma vez e, como dissemos, o código de criação também é executado uma vez. Portanto, o código de criação é executado uma vez porque cria o ambiente para o código de tempo de execução e executa o constructor.
Vamos comentar cada seção do código de criação.
Ponteiro de memória livre
000, 002, 004 e assim por diante são as instruções e PUSH1, PUSH1 40 e MSTORE são os opcodes.
Um byte é simplesmente colocado no topo da pilha (stack) usando PUSH1.
Os dois itens finais são retirados da pilha por MSTORE, que armazena um deles na memória.
Mstore armazena o número 0x80 na posição 0x40 da memória. Aqui, PUSH1 80 ocupa dois bytes, por isso PUSH1 40 está na instrução 003.
Verificação não pagável
A quantidade de wei da transação de criação é impulsionada por CALLVALUE e o primeiro item da pilha é duplicado por DUP1.
Se o valor do topo da pilha for zero, ISZERO adiciona 1 à pilha.
PUSH2 é semelhante a PUSH1, mas possui a habilidade de empurrar dois bytes ao invés de apenas um.
Essa seção só ocorrerá quando tivermos um constructor que seja do tipo não pagável. A verificação não pagável checará..
_if(msg.value != 0) reverte();_
Esse código não faz parte do nosso contrato, mas é injetado pelo compilador porque criamos nosso constructor, que é do tipo não pagável, mas se, por engano, tentarmos executar nosso contrato com alguma quantidade de wei, ele será revertido.
Recuperar parâmetros do constructor
As instruções 17 a 20 estão apenas recuperando o valor da memória que armazenamos anteriormente da instrução 000 a 004. Você se lembra? E armazenando na pilha.
Da instrução 021 a 027, PUSH1 20 empurra 0x20 e duplica o valor, depois PUSH1 0235 empurra o valor e duplica.
Na instrução 028, o local de memória de destino, o número da instrução e o número de bytes do código a ser copiado são as três entradas que o CODECOPY aceita.
Recuperar o parâmetro do constructor:
Esta seção recupera o parâmetro do constructor e o armazena na memória sobre a qual já falamos.
Copiar o código de tempo de execução para a memória:
Essa seção copia todo o código de tempo de execução gerado pelo código de criação e o armazena na memória. Ela copia o código porque, na última parte da execução, o código de criação retorna o código de tempo de execução, que é o código real com o qual queremos interagir.
Até agora, desenvolvemos nosso entendimento sobre o código de criação. Agora, discutiremos o código de tempo de execução.
Código de tempo de execução:
O diagrama acima fornece informações sobre como funciona a execução do opcode de tempo de execução. Com a ajuda do diagrama acima, você poderá entender o fluxo com muita facilidade. Vamos discutir alguns novos opcodes que estão atualmente presentes no opcode de tempo de execução.
Ponteiro de Memória Livre:
Isso é algo que já discutimos na parte do código de criação.
Verificação breve de calldata:
Verificação de calldata
Calldata: O que é calldata? O calldata é um bloco codificado de números hexadecimais que contém informações sobre a função do contrato que queremos chamar e seus argumentos ou dados. Simplificando, ele consiste nos dados dos parâmetros empacotados seguidos de um "ID da função" que é criado pelo hashing da assinatura da função (truncado nos primeiros quatro bytes que iniciam a assinatura).
Como já sabemos, da nossa discussão anterior, já falamos sobre PUSH1, JUMP e muitos outros, mas aqui falaremos sobre o CALLDATASIZE e o LT.
O CALLDATASIZE simplesmente empurra o segundo 4 para a pilha.
LT garante que o hash ou seletor de função que colocamos na pilha contenha o comprimento de pelo menos 4 bytes ou não.
Extraindo o hash da função do calldata:
Nesta seção, extraímos o hash da função do calldata que já colocamos na pilha enquanto realizamos uma breve verificação de calldata.
A instrução 050, que é CALLDATALOAD é o opcode da EVM para obter 32 bytes do calldata.
Aqui ele extrai os primeiros 4 bytes do calldata (hash de assinatura de função).
E faz a correspondência com os outros seletores de função, um a um, que estão presentes no contrato.
Exemplo: Digamos que em nosso contrato haja 3 funções:
- balanceOf( )
- toatalSupply( )
- transfer( )
- Se o usuário executar a função totalsupply, nesse caso, novos dados de chamada serão gerados e, com base nessa assinatura de função de calldata, serão gerados e armazenados na pilha de execução, usando a verificação breve de calldata.
- Então, no próximo passo, usando **CALLDATALOAD, **ele extrai os primeiros 4 bytes da assinatura da função.
- Agora, na terceira etapa, como mencionamos, há três funções presentes e o usuário acessa a função totalSupply. Nesse caso, a EVM tenta corresponder as assinaturas de função com todas as funções.
- Sempre que a assinatura da função corresponder, ele executa a função wrapper para a função correspondente.
- Se a assinatura da função não corresponder, nesse caso ele verificará se há fallback e, se o fallback também falhar, ele reverterá o processo.
Função Wrapper:
Para ver como a operação Jump (saltar) será executada, basta ir para a seção do diagrama de opcode, onde se encontra a instrução nº 063 jump1, que significa Saltar sobre a instrução 081 jumpdest. Você pode ver na instrução nº 083 que a seção da função wrapper será iniciada.
Vamos examinar as diferentes seções de opcode da seção da função wrapper.
- Verificação Não-Pagável: Essa é a mesma verificação que discutimos no cenário do constructor, que é gerada pelo compilador para verificar se _(msg.value !=0 ) _reverter. Ou seja, se a condição for confirmada, ele reverte.
- Desempacotador de CallData: Se chamamos totalSupply() sem definir o valor, nesse caso a seção executará a instrução 077, o jumpdest e com a ajuda do pop da instrução 78, limpa um zero que foi deixado na pilha e, em seguida, 082 sem ir para o corpo da função porque nós não definimos o valor de totalsupply.
- Se o valor de totalsupply já estiver definido, no caso ele entrará na instrução 100 jumpdest, que é o corpo da função.
- Dentro do corpo da função, ele executa a função e, em seguida, retorna à função wrapper, como você pode ver no diagrama acima, e a função wrapper finalmente retornará ao usuário o valor recebido pelo corpo da função.
Metadata (metadados):
O hash de metadados pode ser encontrado nos últimos opcodes do bytecode de tempo de execução de um contrato. Ele contém todas as informações sobre o código do contrato.
Nesta seção, você encontra alguns novos opcodes.
- Os opcodes Log1 to Log4 são utilizados para eventos de registro na blockchain Ethereum.
- STOP é uma parada válida para o contrato. As alterações de estado são mantidas e o gas não utilizado é reembolsado.
- A transação será lançada, sua alteração de estado consumirá todo o gas associado a ela, se ela for INVALID.
- O opcode DELEGATECALL utiliza dados que são armazenados na memória, por isso devemos primeiramente copiar o calldata lá.
Referências:
Solidity Bytecode and Opcode Basics
Ethereum Virtual Machine Opcodes
Deconstructing a Solidity Contract — Part I: Introduction
Esse artigo foi escrito por Manmeet Singh Parmar e traduzido por Fátima Lima. O original pode ser lido aqui.
Top comments (0)