Mergulhe nas estruturas de dados EVM, recibos de transações e logs de eventos

Compreender as estruturas de dados que compõem um blockchain nos ajuda a pensar em formas criativas de analisar esses dados.

**Escrito por:**NOXX

Compile: Flush

Navegar em dados on-chain é uma habilidade essencial para quem deseja entender o espaço Web3. Compreender as estruturas de dados que compõem um blockchain nos ajuda a pensar em formas criativas de analisar esses dados. Ao mesmo tempo, esses dados on-chain constituem uma grande parte dos dados disponíveis. Esta postagem se aprofundará em uma estrutura de dados chave no EVM, o recibo da transação e seu log de eventos associado.

Por que logar

Antes de começar, vamos falar brevemente sobre por que precisamos usar logs de eventos como um desenvolvedor de solidity:

  • O log de eventos é uma opção mais barata para armazenamento de dados que não precisam ser acessados pelo contrato, podendo também reconstruir o estado armazenado testando variáveis específicas no contrato inteligente, indexando variáveis.
  • O log de eventos é uma maneira de acionar um aplicativo Web3 que escuta um log de eventos específico.

Os nós EVM não precisam manter logs para sempre e podem economizar espaço excluindo logs antigos. Os contratos não têm acesso ao armazenamento de logs, portanto, os nós não precisam deles para executar contratos. O armazenamento de contrato, por outro lado, é necessário para execução e, portanto, não pode ser excluído.

Ethereum Block Merkle Root

Na Parte 4, nos aprofundamos na estrutura do Ethereum, especialmente na parte da raiz Merkle do estado. A State Root é uma das três raízes Merkle incluídas no cabeçalho do bloco. Os outros dois são Raiz de Transação e Raiz de Recebimento.

Para entrada na construção desta estrutura, vamos nos referir ao bloco 15001871 no Ethereum, que contém 5 transações com seus recibos associados e logs de eventos enviados.

cabeçalho do bloco

Começaremos com 3 partes no cabeçalho do bloco, Transaction Root, Receipt Root e Logs Bloom (uma breve introdução ao cabeçalho do bloco pode ser revisada na Parte 4).

Fonte:

No cliente Ethereum em Transaction Root e Receipt Root, Merkle Patricia Tries contém todos os dados de transação e recibos no bloco. Este artigo se concentrará apenas em todas as transações e recebimentos que um nó pode acessar.

As informações do cabeçalho do bloco 15001871 encontradas por meio do nó Ethereum são as seguintes:

O logsBloom no cabeçalho do bloco é uma estrutura de dados chave, que será mencionada posteriormente neste artigo. Primeiro vamos começar com os dados localizados na Raiz da Transação, a Trie da Transação.

Transaction Tree Transaction Trie

Transaction Trie é um conjunto de dados que gera transactionRoot e registra vetores de solicitação de transação. Os vetores de solicitação de transação são informações necessárias para executar uma transação. Os campos de dados contidos em uma transação são os seguintes:

  • Tipo - tipo de transação (transação tradicional LegacyTxType, introdução AccessListTxType EIP-2930, introdução DynamicFeeTxType EIP-1559)
  • ChainId - ID da cadeia EIP155 da transação
  • Dados - dados de entrada da transação
  • AccessList - lista de acesso para transações
  • Gás - o limite de gás da transação
  • GasPrice - o preço do gás da transação
  • GasTipCap - o prêmio de incentivo para mineradores cuja unidade de transação de gás excede a taxa básica para empacotar primeiro, maxPriorityFeePerGas em Geth é definido por EIP1559
  • GasFeeCap - o limite superior da taxa de gás por unidade de transação, maxFeePerGas em Geth (GasFeeCap ≥ baseFee + GasTipCap)
  • Valor - a quantidade de Ethereum negociada
  • Nonce - o nonce do originador da conta de negociação
  • To - O endereço do destinatário da transação. Para transações de criação de contrato, To retorna um valor nulo
  • RawSignaturues - Valores de assinatura V, R, S dos dados da transação

Depois de entender os campos de dados acima, vamos dar uma olhada na primeira transação do bloco 15001871

Por meio da consulta ethclient do Geth, você pode ver que tanto ChainId quanto AccessList têm "omitempty", o que significa que, se o campo estiver vazio, ele será omitido na resposta para reduzir ou encurtar o tamanho dos dados serializados.

fonte do código:

Esta transação representa a transferência de tokens USDT para o endereço 0xec23e787ea25230f74a3da0f515825c1d820f47a. O endereço To é o endereço do contrato ERC20 USDT 0xdac17f958d2ee523a2206206994597c13d831ec7. Através de INPUT DATA, podemos ver que a assinatura da função 0xa9059cbb corresponde à função Transfer (Endereço, UINT256), e 42.251 USDT (precisão de 6) para 0x2b279b8 (45251000) é transferido para 0xEC23E787EA25230F para 0xEC23E787EA25230F. 74A3 Endereço DA0F515825C1D820F47A.

Você deve ter notado que esta estrutura de dados da transação não nos diz nada sobre o resultado da transação, então a transação foi bem-sucedida? Quanto de gás ele consome? Quais registros de eventos são acionados? Neste ponto, vamos introduzir o Trie Receipt.

Teste de Recibo

Assim como um recibo de compra registra o resultado de uma transação, um objeto no Receipt Trie faz o mesmo para uma transação Ethereum, mas também registra alguns detalhes adicionais. Voltando à pergunta sobre recibos de transações acima, vamos nos concentrar nos logs que acionaram os eventos a seguir.

Consultar novamente os dados on-chain de 0x311b e obter o recibo da transação, neste momento serão obtidos os seguintes campos:

fonte do código:

  • Tipo - tipo de transação (LegacyTxType, AccessListTxType, DynamicFeeTxType)
  • PostState(root) - StateRoot, nó raiz da árvore de estados gerada após a execução da transação, o valor correspondente encontrado na figura é 0x, provavelmente por causa do EIP98
  • CumulativeGasUsed - Gás total cumulativo consumido por esta transação e todas as transações anteriores no mesmo bloco
  • Bloom(logsBloom) - filtro Bloom para logs de eventos, usado para pesquisar e acessar com eficiência logs de eventos de contrato no blockchain, permitindo que os nós recuperem rapidamente se um determinado evento ocorreu em um bloco sem analisar totalmente o bloco Todos os recibos de transação no bloco
  • Logs - uma matriz de objetos de log contendo entradas de log geradas por eventos de contrato acionados durante a execução da transação
  • TxHash - o hash da transação associado ao recibo
  • ContractAddress - Se a transação for para criar um contrato, o endereço onde o contrato foi implantado. Se a transação não for uma criação de contrato, mas como uma transferência ou interação com um contrato inteligente implantado, o campo ContractAddress estará vazio
  • GasUsed - o gás consumido por esta transação
  • BlockNumber - o número do bloco onde esta transação ocorreu
  • TransactionIndex - O índice da transação dentro do bloco, o índice determina qual transação é executada primeiro. Esta transação está no topo do bloco, então índice 0

Agora que sabemos a composição do recibo da transação, vamos dar uma olhada mais de perto nos logsBloom e nos logs da matriz de logs no recibo da transação.

Registros de eventos

Por meio do código do contrato USDT na rede principal da Ethereum, podemos ver que o evento Transfer é declarado na linha 86 do contrato e os dois parâmetros de entrada possuem a palavra-chave "indexed".

(fonte do código:

Quando uma entrada de evento é "indexada", isso nos permite encontrar logs rapidamente por meio dessa entrada. Por exemplo, ao utilizar o índice "from" acima, é possível obter todos os logs de eventos do tipo Transfer com endereço "from" 0x5041ed759dd4afc3a72b8192c143f72f4724081a entre os blocos X e Y. Também podemos ver que quando a função de transferência é chamada na linha 138, o log de eventos é disparado. Vale a pena notar que o contrato atual usa uma versão anterior de solidity, então a palavra-chave emit está faltando.

Volte para os dados on-chain obtidos:

fonte do código:

Vamos nos aprofundar um pouco mais nos campos de endereço, tópicos e dados.

Tópicos temáticos

Tópicos é um valor de índice. Na figura acima, podemos ver que existem 3 parâmetros de índice de tópicos nos dados da consulta na cadeia, enquanto o evento Transfer possui apenas 2 parâmetros de índice (de e para). Isso ocorre porque o primeiro tópico é sempre o hash de assinatura da função do evento. A assinatura da função de evento no exemplo atual é Transfer(address, address, uint256). Fazendo hash com keccak256, obtemos o resultado ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.

(ferramenta on-line:

Quando consultamos o campo de como mencionado acima, mas ao mesmo tempo queremos limitar o tipo de log de eventos de consulta a apenas logs de eventos do tipo Transferência, precisamos filtrar por tipo de evento indexando assinaturas de eventos.

Podemos ter até 4 tópicos, cada tópico tem um tamanho de 32 bytes (se o tipo do parâmetro de índice for maior que 32 bytes (ou seja, string e bytes), os dados reais não são armazenados, mas um resumo keccak256 dos dados está armazenado). Podemos declarar 3 parâmetros de índice porque o primeiro parâmetro é obtido pela assinatura do evento. Mas há uma situação em que o primeiro tópico não é uma assinatura de evento de hash. Este é o caso ao declarar eventos anônimos. Isso abre a possibilidade de usar 4 parâmetros de indexação em vez dos 3 anteriores, mas perde a capacidade de indexar nomes de eventos. Outra vantagem dos eventos anônimos é que eles são menos dispendiosos para implantar, pois não impõem um tópico adicional. Os outros tópicos são os valores dos índices "from" e "to" do evento Transfer.

DadosDados

A seção de dados contém os parâmetros restantes (não indexados) do log de eventos. No exemplo acima, há um valor 0x00000000000000000000000000000000000000000000000000000002b279b8, que é 45251000 em decimal, que é o valor mencionado acima de US$ 45,251. Se houver mais desses parâmetros, eles serão anexados ao item de dados. O exemplo abaixo mostrará o caso de mais de 1 parâmetro não indexado.

O exemplo atual adiciona um campo "tax" extra ao evento Transfer. Suponha que o imposto definido seja 20%, então o valor do imposto deve ser 45251000 * 20% = 9050200, seu valor hexadecimal é 0x8a1858, pois o tipo desse número é uint256 e o tipo de dados é 32 bytes, você precisa O o valor hexadecimal é preenchido com 32 bytes, e o resultado do item de dados é 0x00000000000000000000000000000000000000000000000000000000002b279b80000000000000000000000000000000000 000 00000000000000000000008a1858.

Endereço

O campo address é o endereço do contrato que emitiu o evento, uma observação importante sobre este campo é que ele será indexado mesmo não estando incluído na seção de tópicos. O motivo é que o evento Transferência faz parte do padrão ERC20, ou seja, quando for necessário filtrar os logs dos eventos de transferência ERC20, os eventos de transferência serão obtidos de todos os contratos ERC20. E ao indexar o endereço do contrato, a pesquisa pode ser reduzida a um contrato/token específico, como USDT no exemplo.

Opcodes Opcodes

Finalmente, há o opcode LOG. Eles variam de LOG0 quando não há tópicos a LOG4 quando há 4 tópicos. LOG3 é o que usamos em nosso exemplo. Contém o seguinte:

  • offset - deslocamento de memória, indicando a posição inicial da entrada do campo de dados
  • length - comprimento dos dados a serem lidos da memória
  • tópico x(0 - 4) - o valor do tópico x

(Fonte:

deslocamento e comprimento definem onde os dados estão localizados na seção de dados na memória.

Depois de entender a estrutura do log e como um tópico é indexado, vamos entender como os itens de índice são pesquisados.

Bloom Filters Bloom Filters

O segredo para indexar itens que estão sendo pesquisados mais rapidamente é o filtro Bloom.

O artigo Llimllib tem uma boa definição e explicação dessa estrutura de dados.

"O filtro Bloom é uma estrutura de dados que pode ser usada para determinar se um elemento está em uma coleção. Ele tem as características de operação rápida e pequeno consumo de memória. O custo de inserção e consulta eficientes é que o Filtro Bloom é um filtro de dados baseado em probabilidade estrutura: só pode nos dizer que um elemento definitivamente não está no conjunto ou possivelmente no conjunto. A estrutura de dados subjacente de um filtro Bloom é um vetor de bits.

Abaixo está um exemplo de um vetor de bits. As células brancas representam bits com valor 0 e as células verdes representam bits com valor 1.

Esses bits são definidos como 1, obtendo alguma entrada e hash, o valor de hash resultante é usado como um índice de bit no qual o bit deve ser atualizado. O vetor de bits acima é o resultado da aplicação de 2 hashes diferentes ao valor "ethereum" para obter um índice de 2 bits. O hash representa um número hexadecimal e, para obter o índice, você pega o número e o converte em um valor entre 0 e 14. Existem muitas maneiras de fazer isso, como o mod 14.

Análise

Com um filtro Bloom para transações, ou seja, um vetor de bits, ele pode ser hash no Ethereum para determinar quais bits no vetor de bits atualizar.A entrada é o campo de endereço e o tópico do log de eventos. Vamos revisar o logsBloom no recibo da transação, que é um filtro Bloom específico da transação. Uma transação pode ter vários logs, que contém o endereço/tópico de todos os logs.

Se você olhar de volta para o cabeçalho do bloco, encontrará outro arquivo logsBloom. Este é um filtro Bloom para todas as transações dentro do bloco. Que contém todos os endereços/tópicos em cada log para cada transação.

Esses filtros Bloom são expressos em hexadecimal e não em binário. Eles têm 256 bytes de comprimento e representam um vetor de 2048 bits. Se nos referirmos ao exemplo Llimllib acima, nosso comprimento de vetor de bits é 15 e os índices de bits 2 e 13 são invertidos como 1. Vamos ver o que obtemos quando convertemos isso em hexadecimal.

Embora a representação hexadecimal não pareça um vetor de bits, ela parece em logsBloom.

Consultas de consulta

Uma consulta foi mencionada anteriormente, "Encontre todos os logs de eventos do tipo Transferência cujo endereço "de" seja 0x5041ed759dd4afc3a72b8192c143f72f4724081a entre os blocos X e Y". Podemos obter o tópico de assinatura do evento, que representa um tópico do tipo Transferência e valor de (0x5041…), e determinar quais índices de bits no filtro Bloom devem ser definidos como 1.

Se você usar logsBloom no cabeçalho do bloco, poderá verificar se algum desses bits não está definido como 1. Caso contrário, pode ser determinado que não há logs que correspondam à condição no bloco. E se esses bits forem definidos, sabemos que um log correspondente provavelmente está no bloco. Mas não tenho certeza absoluta, porque o cabeçalho do bloco logsBloom consiste em vários endereços e tópicos. Outros logs de eventos podem ter bits de correspondência definidos. É por isso que um filtro de Bloom é uma estrutura de dados probabilística. Quanto maior o vetor de bits, menos provável é que ocorram colisões de índices de bits com outros logs. Depois de ter um filtro Bloom correspondente, você pode usar o mesmo método para consultar logsBloom para recibos individuais. Quando uma correspondência é obtida, a entrada de log real pode ser visualizada para recuperar o objeto.

Execute as operações acima nos blocos X a Y para localizar e recuperar rapidamente todos os logs que atendem aos critérios. É assim que o filtro Bloom funciona conceitualmente.

Agora vamos ver a implementação usada no Ethereum.

Implementação Geth - Bloom Filters

Agora que sabemos como funciona o filtro Bloom, vamos aprender como o filtro Bloom completa a triagem passo a passo do endereço/tópico até logsBloom em um bloco real.

Em primeiro lugar, a partir da definição do Ethereum Yellow Paper:

Fonte:

"Definimos uma função de filtro Bloom M que reduz as entradas de log em um único hash de 256 bytes:

em é um filtro Bloom especializado que define três bits em 2048, dada uma sequência arbitrária de bytes. Isso é feito tomando os 11 bits inferiores de cada um dos três primeiros pares de bytes no hash Keccak-256 da sequência de bytes. "

Um exemplo e uma referência a uma implementação de cliente Geth são fornecidos abaixo para simplificar o entendimento das definições acima.

Aqui está o log de transações que vimos no Etherscan.

O primeiro tópico é a assinatura do evento 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef e converte esse valor no índice de bits que deve ser atualizado.

Abaixo está a função bloomValues da base de código Geth.

Esta função recebe o tópico de assinatura do evento, como: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef e outros dados, e retorna o índice de bits que precisa ser atualizado no filtro Bloom.

fonte do código:

  1. A função bloomValues recebe como entrada um tópico (assinatura de evento no exemplo) e um hashbuf (um array de bytes vazio de comprimento 6).
  1. Consulte o trecho do Yellow Paper, "Os três primeiros pares de bytes em um hash Keccak-256 de uma sequência de bytes". Esses três pares de bytes são 6 bytes, que é o comprimento de hashbuf.

  2. Dados de amostra: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.

  1. O comando sha entre as linhas 140 - 144 faz o hash dos dados de entrada e carrega a saída em hashbuf.
  1. O resultado hexadecimal da saída sha usando keccak256 é (ao usar keccak 256 como a assinatura da função, a entrada é do tipo texto, mas aqui é do tipo hexadecimal): ada389e1fc24a8587c776340efb91b36e675792ab631816100d55df0b5cf3cbc.

  1. O conteúdo atual de hasbuf [ad, a3, 89, e1, fc, 24] (hexadecimal). Cada caractere hexadecimal representa 4 bits.

3 Calcular v1.

1)hashbuf [1] = 0xa3 = 10100011 para bit a bit AND com 0x7. 0x7 = 00000111.

  1. Um byte consiste em 8 bits.Se você deseja obter um índice de bit, você precisa garantir que o valor obtido esteja entre 0 e 7 da matriz de índice zero. Use bit a bit AND para hashbuf [1] Restrito a valores entre 0 e 7. Calculado no exemplo, 10100011 & 00000111 = 00000011 = 3.

  2. Este valor de índice de bit é usado com um operador de deslocamento de bit, ou seja, deslocado 3 bits para a esquerda, resultando em um índice de byte de 8 bits 00001000, para criar um bit invertido.

  3. v1 é o byte inteiro em vez do índice de bits real, porque esse valor será ORed bit a bit no filtro Bloom posteriormente. A operação OR garantirá que todos os bits correspondentes no filtro Bloom também sejam invertidos.

  1. Agora temos valores de bytes, mas ainda precisamos de índices de bytes. O filtro Bloom tem 256 bytes de comprimento (2048 bits), então precisamos saber em qual byte executar o OR bit a bit. O valor i1 representa este índice de bytes.
  1. Coloque o hashbuf por ordem de byte big-endian uint16, o que faz com que limite os 2 primeiros bytes do array de bits, que é 0xada3 = 1010110110100011 no exemplo.

  2. E bit a bit este valor com 0x7ff = 0000011111111111. Existem 11 bits onde 0x7ff é definido como 1. Conforme mencionado no artigo amarelo, "ele faz isso pegando os 11 bits mais baixos de cada um dos três primeiros pares". Isso resultará no valor 0000010110100011, que é 1010110110100011 e 00000111111111111.

  3. Em seguida, desloque o valor para a direita em 3 bits. Isso converte um número de 11 dígitos em um número de 8 dígitos. Queremos um índice de bytes, e o comprimento de byte do filtro Bloom é 256, portanto, o valor do índice de bytes precisa estar nesse intervalo. E um número de 8 bits pode ser qualquer valor entre 0 e 255. Em nosso exemplo, esse valor é 0000010110100011 deslocado para a direita em 3 bits 10110100 = 180.

  4. Calcule nosso índice de bytes por BloomByteLength, sabendo que é 256 menos os 180 calculados, menos 1. Subtraia 1 para manter o resultado entre 0 e 255. Isso nos dá o índice de bytes a ser atualizado, que neste caso é o byte 75, que é como calculamos i1.

  1. Atualize o índice de bits 3 no 75º byte do filtro Bloom (0 é o índice, portanto, o 4º bit), o que pode ser feito executando uma operação OU bit a bit de v1 no 75º byte no filtro Bloom.
  1. Cobrimos apenas o primeiro par de bytes 0xada3, o que foi feito novamente para os pares de bytes 2 e 3. Cada endereço/tópico atualizará 3 bits em um vetor de 2048 bits. Conforme mencionado no Yellow Paper, "o filtro Bloom define três bits em 2048, dada uma sequência arbitrária de bytes".

  2. O status do par de bytes 2 atualiza o índice de bits 1 no byte 195 (execute de acordo com os procedimentos 3 e 4, o resultado é mostrado na figura).

  3. Par de bytes 3, índice de bits de atualização de status 4 no byte 123.

  4. Caso o bit a ser atualizado já tenha sido invertido por outro tópico, ele permanecerá como está. Irá virar para 1 se não.

Por meio do processo de operação acima, pode-se determinar que o tópico de assinatura de evento inverterá os seguintes bits no filtro Bloom:

  • Índice de bits 3 no byte 75
  • índice de bits 1 no byte 195
  • índice de bits 4 no byte 123

Observar os logBlooms no recibo da transação, convertidos em binário, verifica se esses índices de bits estão definidos.

Enquanto isso, para os leitores interessados em aprender mais sobre a implementação da pesquisa de log e do filtro Bloom, você pode consultar o artigo BloomBits Trie.

Neste ponto, nossa discussão aprofundada sobre a série de artigos EVM chegou ao fim e traremos a você mais artigos técnicos de alta qualidade no futuro.

Ver original
O conteúdo serve apenas de referência e não constitui uma solicitação ou oferta. Não é prestado qualquer aconselhamento em matéria de investimento, fiscal ou jurídica. Consulte a Declaração de exoneração de responsabilidade para obter mais informações sobre os riscos.
  • Recompensa
  • Comentar
  • Partilhar
Comentar
0/400
Nenhum comentário
  • Pino
Negocie cripto em qualquer lugar e a qualquer hora
qrCode
Digitalizar para transferir a aplicação Gate.io
Novidades
Português (Portugal)
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • ไทย
  • Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)