WEB3DEV

Cover image for Como Usar As Provas Ethereum
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Como Usar As Provas Ethereum

27 de fevereiro de 2023

https://s3.amazonaws.com/infura-blog-content/2023/02/How-to-use-Ethereum-Proofs_1200x628_image-2.png

A comunidade Web3 discute frequentemente o impacto dos provedores de nós centralizados para atender aos usuários. Por exemplo, as empresas de Web3 sediadas nos EUA foram obrigadas a bloquear o envio de transações para o contrato TornadoCash no final de 2022. Isso gerou um debate sobre até onde isso poderia ir. Os blocos podem ser censurados? Os provedores de nós poderiam bloquear indiscriminadamente o acesso ao contrato? Embora a censura de transações seja um tópico complexo que vai muito além de uma postagem técnica em um blog, este artigo se concentrará na censura de dados. A raiz do problema se resume a suposições de confiança.

Podemos confiar que um provedor de nó está fornecendo os dados corretos? Podemos confiar que o estado da blockchain está correto? A Ethereum é uma tecnologia enraizada em um princípio de confiança com verificação, “trust but verify”. Utiliza incentivos para influenciar o bom comportamento onde são necessários níveis mais elevados de confiança. A Ethereum foi construída para ser um banco de dados acessível publicamente com mecanismos integrados para verificar seu estado. Exploraremos alguns desses tópicos de uma perspectiva técnica aqui.

Recentemente, encontramos esta postagem que alerta que o banco de dados de estado a respeito do TornadoCash estava sendo censurado. Embora seja verdade que os regulamentos do OFAC (Office of Foreign Assets Control - escritório de controle de ativos estrangeiros) exigiam que as empresas sediadas nos EUA impedissem o envio de transações para os contratos do TornadoCash, o acesso de leitura não foi afetado. Isso seria alarmante se o acesso de leitura fosse censurado. Os provedores de nós estariam censurando informações públicas. Acontece que a afirmação feita na postagem original está incorreta, mas você não precisa confiar em mim. Podemos provar isso!

Tenho que admitir que me preparar para escrever esse artigo foi divertido. Envolveu desenvolver uma compreensão mais profunda do Ethereum Name Service (ENS) ou Serviço de Nome de Domínio Ethereum, brincar muito com o Cast - uma das minhas novas ferramentas de desenvolvedor favoritas - e mergulhar profundamente na compreensão das provas na Ethereum. Faremos uma jornada semelhante nesse artigo. Preparado? Vamos!

Agradecimentos especiais ao nosso engenheiro principal, Ryan Schneider, que ajudou muito a descobrir o que estava acontecendo e ajudou a desenvolver parte do conteúdo deste artigo.

Uma Exploração Rápida do ENS da Ethereum

A postagem mencionada acima nos inspirou a nos aprofundarmos mais. Eu sei que o Infura não censura o acesso de leitura nem altera dados. Eu tive que tentar por mim mesmo:

ETH_RPC_URL=https://mainnet.infura.io/v3/<apiKey> cast call 0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8 "contenthash(bytes32 node)" tornadocash.eth
Enter fullscreen mode Exit fullscreen mode

Resultado:

0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Recriar seu método tanto no Infura quanto em meu nó hospedado localmente produziu a mesma resposta descrita pelo autor: uma resposta nula. O Infura não era o problema. Algo estava acontecendo aqui, mas eu não entendia o suficiente sobre como o ENS funcionava para entender o que estava vendo.

Comecei por onde qualquer engenheiro começaria: os documentos. A arquitetura do ENS possui três conceitos principais:

namehash: o ENS não usa strings brutas de nomes de domínio para armazená-los na blockchain. Por vários motivos importantes que você pode ler nos documentos, eles usam um esquema de hash para armazenar valores na blockchain.

registro: o principal ponto de entrada para a resolução de um domínio ENS está no Registro. Este contrato armazena o proprietário do domínio, o endereço do Resolvedor e o TTL.

resolvedor: um contrato que faz a resolução real de um namehash para o valor de destino.

Além disso, o ENS suporta vários tipos de resolvedores definidos para diferentes casos de uso. Aqui estão alguns:

  • O método addr(bytes32) resolve para endereços Ethereum.
  • O método contenthash(bytes32) permite um sistema melhor definido de mapeamento de nomes para endereços de rede e conteúdo.
  • O método abi(bytes32) é um mecanismo para armazenar definições de ABI no ENS, para facilitar a consulta de interfaces de contrato pelos chamadores.

Um pequeno aparte: usando uma ferramenta de linha de comando como o cast, é fácil pesquisar domínios ENS.

ETH_RPC_URL=https://mainnet.infura.io/v3/<apiKey> cast resolve-name tornadocash.eth
Enter fullscreen mode Exit fullscreen mode

Nos bastidores, resolve-name está convertendo tornadocash.eth em seu namehash e fazendo uma eth_call em addr(bytes32) para obter o valor do endereço de destino. Se você realizar essa chamada, obterá um valor de retorno nulo para tornadocash.eth, que é o valor correto para a chamada addr(bytes32).

Então, de volta ao nosso experimento, tentamos replicar a chamada contenthash(bytes32) e obtivemos a mesma resposta. Como estamos fazendo uma chamada eth_call direta (usando cast call neste caso), a ferramenta não está traduzindo o nome de domínio em um namehash para nós, então corrigimos isso primeiro:

ETH_RPC_URL=https://mainnet.infura.io/v3/<apiKey> cast call 0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8 "contenthash(bytes32 node)" $(cast namehash tornadocash.eth)
Enter fullscreen mode Exit fullscreen mode

Resultado:

0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Ainda parece que o resultado está sendo censurado. A documentação do ENS diz:

A maioria dos contratos utiliza um Resolvedor padrão que é definido no momento do registro. No entanto, o Resolvedor padrão foi alterado algumas vezes para adicionar novas funcionalidades (por exemplo: tipo de moeda). Para descobrir todos os endereços de Resolvedores, é necessário obter o endereço do Resolvedor por meio do Registro ENS (ENSRegistry).

O autor original usa um endereço de Resolvedor codificado de: 0x22…8b8. Dando um passo atrás, vamos validar se o contrato Resolvedor está correto usando o Registro ENS. O endereço do contrato Registro ENS está publicado aqui em seu site como 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e. Reconheço que confio que o site do ENS publica informações legítimas. Mas como também posso verificar isso no Etherscan, estou convencido de que o endereço do contrato Registro está correto.

ETH_API_URL=https://mainnet.infura.io/v3/<apiKey> cast call 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e resolver(bytes32) $(cast namehash tornadocash.eth)
Enter fullscreen mode Exit fullscreen mode

Resultado:

0x0000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41
Enter fullscreen mode Exit fullscreen mode

Usando essa chamada, obtivemos um contrato Resolvedor diferente: 0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41

Eureka! Há um contrato Resolvedor diferente listado no registro ENS. Quando chamamos o método contracthash neste endereço:

cast call 0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41 "contenthash(bytes32)" $(cast namehash tornadocash.eth)
Enter fullscreen mode Exit fullscreen mode

Agora obtivemos o contenthash codificado corretamente como valor de retorno.

0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000026e30101701220d422ef6e800db34f50101daa4ea6b04365ab44b49bf58c00b54c1067befb73700000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Executando este resultado através do Cast mais uma vez:

cast --abi-decode "contenthash(bytes32)(bytes)" 0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000026e30101701220d422ef6e800db34f50101daa4ea6b04365ab44b49bf58c00b54c1067befb73700000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Resultado:

0xe30101701220d422ef6e800db34f50101daa4ea6b04365ab44b49bf58c00b54c1067befb7370
Enter fullscreen mode Exit fullscreen mode

Com uma compreensão mais profunda de como o ENS funciona e brincando com o cast, conseguimos encontrar o verdadeiro valor do contenthash do tornadocash.eth. Como vimos acima, o contenthash(bytes32) permite armazenar e acessar endereços de conteúdo. Você pode validar o destino decodificando o valor e interrogando o DAG (Directed Acyclic Graph ou Grafo Acíclico Dirigido) do IPFS para descobrir que este é o site do TornadoCash. Embora a exploração do IPFS esteja além do escopo deste artigo, avise-nos se quiser uma parte 2 e podemos explorar essa parte!

Acontece que o aparecimento da censura foi apenas um mal-entendido sobre as ferramentas e os contratos ENS. Voltando à ideia de censura, como podemos confiar que o contrato retornado é o Resolvedor ENS canônico e não algum tipo de honeypot elaborado? A grande vantagem da Ethereum é que podemos fazer exatamente isso. Na próxima seção, exploraremos o uso de provas para validar o estado da blockchain.

Provas Ethereum Explicadas

Há muito material publicado sobre os benefícios de blockchains como a Ethereum, sendo que um deles é que você não precisa construir todo o banco de dados de estado para provar que algo está correto. O banco de dados Ethereum é implementado como um Modified Merkle Patricia Trie. Existem várias propriedades exclusivas da Merkle Patricia Trie. Uma dessas propriedades aqui é que você pode utilizar as provas Merkle como um meio computacionalmente eficiente para provar uma série de afirmações diferentes sobre a blockchain, incluindo estado da conta e valores de armazenamento.

Há algumas informações bastante básicas a serem revisadas para garantir que entendemos o que está acontecendo. Os usuários enviam transações para a blockchain para atualizar o estado do banco de dados. Existem três tentativas principais da Merkle atualizadas quando uma transação é incluída:

  • A trie de recebimento (representando a transação que atualizou o estado);
  • A trie de estado (rastreando alterações nas contas);
  • A árvore de armazenamento (uma trie por conta que rastreia alterações no estado do contrato).

Quando se envia uma transação que executa uma gravação na blockchain, o armazenamento e as tries da conta mudam. Essa mudança é representada pelo valor de stateRoot no cabeçalho do bloco. Quando você chama um método JSONRPC, como eth_getBlockByNumber, você vê o cabeçalho do bloco com hashes criptográficos que representam o estado da blockchain em um determinado momento. Para os propósitos deste artigo, a parte importante a ser lembrada é que stateRoot é um hash criptográfico que você pode usar para validar se tudo está correto e inalterado. É a raiz da Merkle Trie.

Como Usar As Provas Ethereum

Como a principal diferença entre o artigo de origem e o caminho que acabamos de seguir para validar o destino do ENS acabou sendo um endereço de Resolvedor diferente, esse parecia ser um bom lugar para começar a explorar as provas. Se pudermos provar que o Resolvedor de endereço que usamos é o contrato correto e não retorna uma cópia ilegítima do contrato sob o controle de um invasor (ou censor), podemos confiar no valor da chamada contenthash(bytes32) que é retornada. Por uma questão de brevidade, vou apenas avançar na prova do registro. O mesmo exercício também pode ser feito com o método contenthash no contrato Resolvedor, mas é basicamente o mesmo método e é deixado como exercício para o leitor.

Usaremos o método eth_getProof para obter as provas Merkle e verificar a correção. Para usar eth_getProof precisaremos do endereço do contrato, do slot de armazenamento e da referência do bloco. Se você fizer referência ao contrato ENS , poderá ver que os valores são armazenados em um tipo de variável map. Os slots de armazenamento do tipo map são um dos índices de armazenamento mais difíceis de calcular. Para os curiosos, você pode encontrar detalhes sobre mapeamento de armazenamento aqui. Um método para encontrar o slot de armazenamento é usar eth_createAccessList. Então começamos com o contrato Resolvedor 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e. Você pode fazer isso através do Cast via:

cast access-list --from <EOA that has eth for gas> 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e "resolver(bytes32)" $(cast namehash tornadocash.eth)
Enter fullscreen mode Exit fullscreen mode

No momento em que este artigo foi escrito, o Infura ainda não suportava eth_createAccessList, por isso, precisava executar isso em um nó local. No entanto, esta exploração mostra que a utilidade desse método é bastante clara, por isso pretendemos adicionar esse método no futuro. Os valores-chave retornados são:

[                     "0xf8b3ca70e07afc9d3c9a4f37fd6adccac81587450545d4b161205b35bf9b1ecd",
"0xf8b3ca70e07afc9d3c9a4f37fd6adccac81587450545d4b161205b35bf9b1ece"
]
Enter fullscreen mode Exit fullscreen mode

Por que existem 2 slots de armazenamento que são devolvidos? Examinando o contrato Registro ENS, podemos ver que a estrutura de dados armazenada para um determinado nó é:

struct Record {
    address owner;
    address resolver;
    uint64 ttl;
}
Enter fullscreen mode Exit fullscreen mode

O registro abrangerá 3 slots de armazenamento se todo o registro tiver valores. A ausência de um terceiro slot significa que não há TTL definido para este registro.

Com esses 2 valores, agora podemos chamar eth_getProof e processar os resultados.

cast proof --rpc-url https://mainnet.infura.io/v3/384418b1eb3743ac82e784d0ebab61f5 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e 0xf8b3ca70e07afc9d3c9a4f37fd6adccac81587450545d4b161205b35bf9b1ecd 0xf8b3ca70e07afc9d3c9a4f37fd6adccac81587450545d4b161205b35bf9b1ece
Enter fullscreen mode Exit fullscreen mode

O método retorna o seguinte objeto:

{
    address: Address
    balance: uint256
    codeHash: string
    nonce: uint256
    storageHash: string  accountProof: string[]
    storageProofs: [
    { key: string, proof: string[], value: string },
    …
    ]
}
Enter fullscreen mode Exit fullscreen mode

Então, vamos dar uma olhada no que temos agora. Temos alguns dados que nos dizem qual endereço estamos provando, alguns atributos da conta (nonce, balance, storageHash - hash de armazenamento - e hash de código) e algumas provas Merkle que podemos usar para validar a conta e validar a trie de armazenamento. O que tudo isso significa e como os usamos? Agora chegamos à parte divertida.

Para fazer uso dessas provas, precisamos usar uma implementação da Merkle Patricia Trie. Implementar o código para uma árvore Merkle compatível com a Ethereum não é trivial de acertar, então usaremos a implementação ethereumjs. Provar a correção consistirá em recriar uma árvore Merkle a partir dos valores de prova, validando diante do stateRoot e validando o resultado que a _trie _produz ao buscar uma chave diante dela. No restante deste artigo, mudaremos para o Typescript para que possamos brincar com as tentativas Merkle.

Provas Merkle com EthereumJS

Usaremos as seguintes bibliotecas para construir as tentativas Merkle e realizar algumas transformações de dados quando necessário. Todo o código usado neste artigo está disponível por meio deste modelo Replit.

import {Trie} from “@ethereumjs/trie”
import * as ethers from ‘ethers’
Enter fullscreen mode Exit fullscreen mode

Primeiro, para validar a conta diante do cabeçalho do bloco atual, buscamos o bloco mais recente e referenciamos o valor stateRoot. Usando sua chave de API Infura, podemos usar o JsonRpcProvider de ethers para gerenciar a conexão com a rede e buscar os dados que precisamos por meio de chamadas JSONRPC padrão.

const rpc = new ethers.providers.JsonRpcProvider(
'https://mainnet.infura.io/v3/&lt;api-key>',
'mainnet'
);
const latestBlockNumber = await rpc.send('eth_blockNumber', []);
const { stateRoot } = await rpc.send('eth_getBlockByNumber', [latestBlockNumber, false]);
// Obtém a prova
const proof = await rpc.send('eth_getProof', [
ENS_REGISTRY,
[
"0xf8b3ca70e07afc9d3c9a4f37fd6adccac81587450545d4b161205b35bf9b1ecd",
"0xf8b3ca70e07afc9d3c9a4f37fd6adccac81587450545d4b161205b35bf9b1ece"
]
latestBlockNumber
]);
Enter fullscreen mode Exit fullscreen mode

Criamos uma Merkle trie com o valor stateRoot do bloco como a raiz da trie. Usando a biblioteca, podemos preencher a trie com uma chamada para trie.fromProof(). Se os valores de prova estiverem incorretos ou uma trie não puder ser construída com os valores, a biblioteca lançará uma exceção aqui. Primeiro, crie uma nova trie usando o valor da raiz do estado que obtivemos do cabeçalho do bloco.

const trie = new Trie({root: stateRoot, useKeyHashing: true})
await trie.fromProof(proof.accountProof.map((p:string) => toBuffer(p)))
Enter fullscreen mode Exit fullscreen mode

Nota: a implementação da Merkle-Patricia-Trie requer que a entrada e a saída sejam convertidas em buffers (em vez de usar os dados brutos da string). Funções utilitárias em toBuffer e bufferToHex no @ethereumjs/util podem ser usadas para converter valores para frente e para trás.

No exemplo acima, a biblioteca lançará uma exceção se a prova for inválida. Além disso, se tentarmos acessar um valor não incluído nesta árvore, também será lançada uma exceção.

Quando tentamos obter o valor da trie pela chave (o endereço original), obtemos um array codificado em RLP de [nonce, value, storageHash, codeHash] que pode ser validado diante dos valores retornados pelo getProof. Tudo isso é validado em relação ao hash stateRoot, então sabemos que tudo isso está correto.

const val = await trie.get(toBuffer(ENS_REGISTRY), true)
Enter fullscreen mode Exit fullscreen mode

Então, o que acabamos de provar? Provamos que a conta Ethereum pertencente ao contrato Registro está correta em relação ao stateRoot calculado para o último bloco. Das informações protegidas no stateRoot, o storageHash representa a raiz de uma trie que armazena todos os valores em um contrato. Para provar o valor do armazenamento, podemos agora construir uma trie adicional usando esse storageHash como raiz.

const storageTrie = new Trie({root: toBuffer(proof.storageHash), useKeyHashing: true})
Enter fullscreen mode Exit fullscreen mode

Como fizemos na outra prova, chamamos trie.fromProof() como fizemos na validação anterior usando storageProofs.

for (var i = 0; i < RESOLVER_KEYS.length; i++) {
  const proofBuffer = proof.storageProof[i].proof.map((p: string) =>
    toBuffer(p)
  );
  storageTrie.fromProof(proofBuffer);
     // Examina os registros do Resolvedor
  const storageVal = await storageTrie.get(toBuffer(RESOLVER_KEYS[i]));
  if (storageVal == null) {
    console.log("Nothing returned");
  } else {
    if (i == 0) {
     // O armazenamento dos Resolvedores é um registro. O primeiro campo é o proprietário
      console.log(`Owner: ${ethers.utils.RLP.decode(bufferToHex(storageVal))}`);
    } else if (i == 1) {
      // O segundo campo é o contrato Resolvedor
      console.log(
        `Resolver: ${ethers.utils.RLP.decode(bufferToHex(storageVal))}`
      );
    } else if (i == 2) {
      // O terceiro campo é o TTL se estiver definido
      console.log(`TTL: ${ethers.utils.RLP.decode(bufferToHex(storageVal))}`);
    } else {
      console.log("Field unknown");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Podemos então usar trie.get() usando o índice do slot de armazenamento e obtemos 2 valores (um para cada slot): o proprietário (verificado como TornadoCash no Etherscan) e o contrato Resolvedor.

Se alguma informação estivesse incorreta (as provas Merkle não produziram uma trie válida ou as chaves que estávamos realizando o get não existiam), a biblioteca teria lançado uma exceção.

Compreender as propriedades das Merkle tries, das provas Merkle e como usá-las fornece uma compreensão mais profunda do que está acontecendo na blockchain. Esses métodos são usados ​​extensivamente em clientes leves que funcionam sem a necessidade de manter uma cópia completa do banco de dados. Embora os fatos do artigo original não sejam totalmente precisos, a necessidade de ser capaz de compreender e verificar as informações que você está recebendo de um provedor de nó é um bom argumento.

Apoiamos totalmente a proposta do artigo de executar um cliente leve de validação como o Helios em cima do Infura ou usar eth_getProof de maneiras mais manuais para provar a exatidão de suas solicitações. Como valor central, o Infura não censura ou altera dados na blockchain. Os dados da cadeia pública são apenas isso: públicos e devem ser representados de forma inalterada.

Todos nós estamos sobre ombros de gigantes. Agradecimentos especiais aos desenvolvedores do conjunto de ferramentas Foundry que desenvolveram o Cast. Além disso, aos desenvolvedores do ethereumjs por manterem um excelente conjunto de módulos para trabalhar na Ethereum em javascript.

Atualmente, o Infura não suporta eth_createAccessList porque sentimos que havia métodos mais urgentes para ativar primeiro, como métodos de rastreamento, e o benefício do método não foi bem compreendido. Agora que clientes leves como o Helios exigem o uso do método, estamos avaliando quando poderemos adicioná-lo.

Esse artigo foi escrito por Kris Shinn e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Oldest comments (0)