WEB3DEV

Cover image for Aprofundamento em opcode no Solidity
Fatima Lima
Fatima Lima

Posted on

Aprofundamento em opcode no Solidity

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 é?

Image description

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:

Image description

Image description

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.

Image description

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.

Image description

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

Image description

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

Image description

Parâmetro 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:

Image description

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:

Image description

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.

Image description

Seletor de função

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):

Image description

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.

Latest comments (0)