WEB3DEV

Cover image for Mergulhos profundos na EVM: O caminho para o Shadowy Super Coder - Parte 3
Fatima Lima
Fatima Lima

Posted on • Atualizado em

Mergulhos profundos na EVM: O caminho para o Shadowy Super Coder - Parte 3

Desmistificando Store Slot Packing

Este é o terceiro episódio da série “Mergulhos profundos na EVM”, este artigo será construído sobre o conhecimento adquirido na parte 1 & parte 2, então, se você ainda não leu estes eu te encorajo a fazê-lo.

Neste episódio nós vamos nos aprofundar em como a armazenagem de contratos funciona, oferecer alguns modelos mentais para ajudar a compreensão e mergulhar profundamente em armazenamento de slot packing. Se o termo “slot packing” é novo para você não se preocupe, conhecimento de slot packing é vital para hackers e você terá um entendimento profundo ao final deste artigo.

Se você alguma vez experimentou a série Ethernaut Solidity Wargame ou outro jogo tipo “capture a bandeira” em Solidity você saberá que conhecimento em slot packing frequentemente é a chave para resolver os quebra-cabeças/hacks.

Fundamentos em armazenagem

Uma revisão de alto nível em fundamentos de armazenagem foi feita brilhantemente nesta postagem “Programe o Blockchain”. Eu vou oferecer uma reciclagem dos pontos chave necessários para este artigo, porém eu recomendo ler o artigo completo.

Estrutura de Dados

Nós vamos começar com a estrutura de dados da armazenagem dos contratos, isto vai nos oferecer uma base sólida sobre a qual o resto do nosso conhecimento vai repousar.

Armazenamento de contratos é simplesmente uma chave para mapear valor. Ele mapeia uma chave de 32 bytes para um valor de 32 bytes. Dado que nossa chave tem um tamanho de 32 bytes nós podemos ter um máximo de (2256)-1 chaves.

32 bytes é igual a 256 bits o que nos dá (2256)-1 números binários para escolher como chave.

Todos os valores são inicializados como 0 e zeros não são explicitamente armazenados. Isto faz sentido uma vez que 2256 é aproximadamente o número de átomos no universo conhecido e observável.

Nenhum computador consegue suportar tantos dados. Esta é a razão pela qual definir um valor de armazenagem em zero retorna para você um pouco de gas uma vez que o key-value não precisa mais ser armazenado pelos nós da rede.

Conceitualmente, a armazenagem pode ser vista como um array astronomicamente grande. Nossa primeira chave com valor binário 0 representa o item 0 no array, a chave com valor binário 1 representa o item 1 no array, etc.

Image description

Variáveis de Tamanho Fixo

Variáveis de contrato que são declaradas como variáveis de armazenamento podem ser separadas em dois campos, tamanho fixo e tamanho dinâmico. Nós vamos focar nas variáveis de tamanho fixo e como a EVM pode agrupar múltiplas variáveis em um único slot de armazenamento de 32-bytes.

Para aprender mais sobre variáveis de tamanho dinâmico procure o artigo “Programe a Blockchain”.

Agora que nós sabemos que a armazenagem é um mapeamento do key-value, a próxima questão é como as chaves são distribuídas para as variáveis. Digamos que nós temos o seguinte código em solidity:

Image description

Dado que todas essas variáveis têm tamanho fixo, a EVM pode usar locais de armazenagem reservados (chaves) começando do slot 0 (chave de valor binário 0) e movendo adiante linearmente para o slot 1, 2, etc.

Isto vai ser feito baseado na ordem em que as variáveis são declaradas no contrato. A primeira variável de armazenagem declarada será armazenada no slot 0.

Neste exemplo, o slot 0 vai pegar a variável “value1”, variável “value2” é um array de tamanho fixo 2 então vai pegar slots 1&2 e finalmente, o slot 3 vai ficar com a variável “value3”. O diagrama abaixo mostra isso.

Image description

Agora vamos observar um contrato semelhante e inspecionar como as variáveis são armazenadas neste cenário.

Image description

Note que os tipos de variáveis não são uint256

Você pode esperar que vão ser usados os slots 0 a 3 como no exemplo anterior. Nós tínhamos 4 valores para armazenar no nosso exemplo anterior (considerando um array de tamanho 2) e nós temos 4 valores para armazenar neste exemplo.

Você poderá se surpreender ao ouvir que neste exemplo apenas o slot 0 de armazenagem é usado. A diferença fundamental são os tipos de unidades que são usadas para as variáveis.

Antes, todas as variáveis eram do tipo uint256, o qual representa 32 bytes de dados. Aqui nós usamos uint32, uint64 & uint128, os quais representam 4,8 e 16 bytes de dados, respectivamente.

Slot Packing

É aqui que surge o termo slot packing. O compilador do solidity sabe que ele consegue armazenar 32 bytes de dados em um slot de armazenagem. Como resultado, quando “uint32 value1”, que apenas usa 4 bytes, é armazenado no slot 0 quando a próxima variável é lida o compilador vai ver se ele pode empacotá-la no slot de armazenagem atual.

Dado que o slot 0 tem 32 bytes de espaço e value1 apenas usou 4 deles, a próxima variável pode também ser armazenada no slot 0, desde que seja menor que 28 bytes.

Para o exemplo acima nós começamos com 32 bytes no slot 0;

  • Value1 é armazenada no slot 0 o qual usa 4 bytes
  • Slot 0 tem 28 bytes sobrando
  • Value2 é 4 bytes o qual é <= 28 portanto pode ser armazenado no slot 0
  • Slot 0 tem 24 bytes sobrando
  • Value3 tem 8 bytes o qual é <= 24 portanto pode ser armazenado no slot 0
  • Slot 0 tem 16 bytes sobrando
  • Value4 tem 16 bytes o qual é <= 16 portanto pode ser armazenado no slot 0
  • Slot 0 tem 0 bytes sobrando

Note que uint8 é o menor tipo do solidity, portanto o packing não pode ser menor que 1 byte (8 bits)

A imagem abaixo mostra como os 32 bytes de dados no slot 0 acomodam todas as 4 variáveis.

Image description

Opcodes de armazenamento da EVM

Agora que entendemos a estrutura de dados de armazenamento e o conceito de slot packing vamos dar uma olhada nos dois opcodes de armazenamento SSTORE & SLOAD.

SSTORE

Iremos começar com SSTORE, ela recebe uma chave de 32 bytes e um valor de 32 bytes da stack de chamadas e armazena aquele valor de 32 bytes naquela chave de localização de 32 bytes. Veja este EVM playground pra ver como ela funciona.

SLOAD

Em seguida temos SLOAD que recebe uma chave de 32 bytes da call stack (pilha de chamadas) e leva o valor de 32-byte armazenado naquela chave local de 32 bytes para a call stack. Veja este EVM playground para ver como ela funciona.

Neste estágio você deve estar se perguntando: se SSTORE e SLOAD trabalham apenas com valores de 32 bytes como você consegue extrair uma variável que foi agrupada em um slot de 32 bytes.

Se você considerar o exemplo acima, quando executamos SLOAD no slot 0 vamos conseguir todo o valor de 32 bytes armazenados naquele local.

Este valor incluirá os dados para value1, value2, value3 & value4. Como a EVM extrai os bytes específicos daquele slot de 32 bytes para retornar o valor que nós precisamos?

O mesmo vale para quando executamos SSTORE, se estamos armazenando 32 bytes por vez, como a EVM garante que quando armazenamos value2 ele não sobrescreve o value1. Quando armazenamos value3 ele não sobrescreve value2, etc.

Estas são as perguntas que nós buscamos responder em seguida.

Armazenando & Recuperando Variáveis Agrupadas

Abaixo está um contrato simples que imita o exemplo que nós vimos acima. A única adição é uma função store que define os valores das variáveis e tem que ler uma variável para realizar alguma aritmética.

Image description

A função store() no solidity acima realizará as exatas operações sobre as quais tínhamos perguntas.

Armazenando múltiplas variáveis em um único slot sem sobrescrever dados existentes e recuperando os bytes específicos de uma variável de um slot 32-byte.

Vamos começar olhando para o estado final do slot 0 e trabalhar de trás para frente a partir daí. Abaixo estão ambas as representações binárias e hexadecimais do slot 0.

Lembre-se de que números hexadecimais, em última análise, são vistos como números binários pela máquina. Isto é importante já que um número de operações bitwise são usadas no slot packing.

Image description

Note os valores que você pode ver no hexadecimal, 0x115c o qual é igual a 4444 em decimal, 0x14d = 333, 0x16 = 22 & 0x01 = 1. Estes correspondem ao que vemos em nosso código solidity. Um slot suporta 32 bytes de dados, o que são 60 caracteres hexadecimais ou 256 bits.

Operações Bitwise

Slot packing usa 3 operações bitwise, AND, OR & NOT. Isto corresponde a 3 opcodes EVM com a mesma nomenclatura.

AND

Abaixo temos 2 números binários de 8 bits. Em uma operação AND o primeiro bit no primeiro número é comparado com o primeiro_ bit_ no segundo número.

Se ambos os valores forem 1 então a declaração AND retorna verdade e o primeiro bit do nosso resultado será igual a 1, a não ser no caso da declaração retornar falsa e o bit ser igual a 0.

Isto continua, isto é, o 2o bit do nosso primeiro número é comparado com o 2o bit do nosso segundo número etc.

Image description

OR

Na operação OR, apenas um dos valores precisa ter um valor 1 para a declaração retornar verdadeiro. Usando os mesmo inputs como acima nós obtemos um output completamente diferente.

Image description

NOT

NOT é ligeiramente diferente, já que ele apenas recebe um valor, ao invés de realizar uma comparação entre 2 valores. NOT realiza negação lógica em cada bit. Bits que são 0 tornam-se 1 e aqueles que são 1 tornam-se 0.

Image description

Vamos agora mergulhar em como estes são usados no exemplo do solidity acima.

Manipulação de Slot - Slot Packing SSTORE

Vamos focar na linha 18 do solidity.

value2 = 22;

Neste estágio, alguns dados, value1 foram armazenados no slot 0, nós agora precisamos agrupar alguns dados adicionais dentro do mesmo slot.

Toda a lógica que nós vemos neste exemplo é a mesma que se usa quando value3 & value4 são armazenados. Vamos ver como isto é feito conceitualmente e um playground EVM será oferecido para você explorar mais.

Começamos com os seguintes valores.

Image description

Note “0xffffffff” é igual a “1111111111111111111111111111111” em binária.

A primeira coisa que a EVM faz é usar o opcode EXP que recebe uma base integer e um exponent e retorna o valor.

Aqui a gente usa 0x0100 como a base integer que representa 1 byte e o aumenta para exponent 0x04 que é a posição inicial para “value2”. A imagem abaixo mostra porque o valor retornado é útil.

Image description

Nós podemos ver que o resultado da função EXP nos permite inserir nossos dados na posição correta.

Entretanto, nós não podemos usar isto, pois ela iria sobrescrever value1 que já havia sido armazenado. Aqui é que as bitmasks são utilizadas.

Image description

A imagem acima mostra como uma bitmask pode ser utilizada para pegar todos os dados de um slot exceto aqueles que você está buscando sobrescrever. Neste caso, os bytes do value2 já estavam definidos para 0, entretanto, se eles não estivessem, nós veríamos esses dados serem apagados.

Aqui está outro exemplo para cristalizar o que está acontecendo. Este é o mesmo processo, mas vendo o que aconteceria se todos os 4 valores já estivessem armazenados e nós procurássemos atualizar value2 de 22 para 99. Veja o valor existente 0x016 sendo zerado.

Image description

Você já deve estar pensando como um bitwise OR poderia nos ajudar a combinar os valores que temos. A imagem abaixo mostra os próximos passos.

Image description

Nós podemos agora usar SSTORE neste valor de 32 bytes no slot 0 que contém os dados para ambos value1 & value2 nas posições corretas de byte.

Manipulação de Slot - Recuperando uma Variável SLOAD Agrupada

Para recuperação, vamos focar na linha 22 da solidity.

uint96 value5 = value3 + uint32(666)

Nós não estamos interessados na aritmética, estamos interessados no value3 sendo recuperado para realizar o cálculo.

Nós temos uma série de valores iniciais ligeiramente diferentes.

Image description

Muito do que já vimos será reutilizado para recuperação, com algumas modificações.

Image description

Nós recuperamos value3 do nosso slot 0 agrupado. Hexadecimal 0x14 é igual a 333 que é o que nós definimos no código solidity acima.

Novamente, operações bitmasks e bitwise são usadas para ajudar a extrair bytes específicos do slot de 32 bytes. Este valor está agora na stack, e pode então ser usado pela EVM para calcular “value3 + uint32(666)”.

Playground EVM

Eu peguei todos os opcodes executados na função store() que acabamos de explorar e os coloquei em um playground EVM. Aqui você poderá brincar interativamente com os opcodes que são usados e ver como a call stack e o armazenamento de contratos mudam à medida que você salta entre elas.

Eu deixei comentários próximos aos opcodes nas 2 seções que exploramos (linhas 18 & 22 do solidity). Eu encorajo bastante vocês a verificarem e passarem pelos opcodes, isto vai aumentar muito o seu entendimento.

Confira aqui.

Image description

Esse artigo pertence a uma série de três, foi publicado na Noxx e traduzido por Diogo Jorge. Seu original pode ser lido aqui.

Top comments (0)