Pular para o conteúdo
ZYNQORALABS

Reorganizações de cadeia em indexadores EVM

Indexadores que tratam blocos recentes como definitivos acumulam dados órfãos. Como detectar reorganizações, fazer rollback e expor o nível de confirmação.

Por ZynqoraLabs · · 4 min de leitura

CompartilharLinkedInXWhatsApp

Um indexador de eventos parece simples: ler blocos em ordem, decodificar logs e gravar em um banco. O problema é que os blocos mais recentes de uma blockchain ainda podem ser substituídos. Quando isso acontece, ocorre uma reorganização (reorg), e um indexador ingênuo continua exibindo transferências, saldos ou eventos que não existem mais na cadeia canônica.

Por que reorganizações acontecem

Em redes como a Ethereum, nós diferentes podem ver, por um curto período, versões diferentes da ponta da cadeia. Quando a rede converge, a versão perdedora é descartada e seus blocos ficam órfãos. As transações desses blocos geralmente voltam ao mempool e podem ser incluídas em outro bloco, com outro número, outra posição e até outro resultado.

Desde a transição para prova de participação, a Ethereum tem um conceito explícito de finalidade. Um slot dura 12 segundos e uma época tem 32 slots; em condições normais, um bloco é finalizado depois de cerca de duas épocas, aproximadamente 13 minutos. Antes disso, reorganizações curtas na ponta da cadeia são possíveis. Redes de camada 2 têm regras próprias: a confirmação do sequenciador é rápida, mas a finalidade depende da publicação e da finalização dos dados na camada 1.

Use os níveis de confirmação que o próprio nó oferece

A API JSON-RPC aceita, além de números de bloco, os marcadores latest, safe e finalized. Eles são a forma mais direta de saber até onde é seguro confiar:

  • latest: o bloco mais recente que o nó conhece. Pode ser reorganizado.
  • safe: um bloco com baixa probabilidade de reorganização em condições normais de rede.
  • finalized: um bloco finalizado pelo protocolo. Revertê-lo exigiria uma falha grave de consenso.

Uma estratégia simples é indexar até finalized e nunca lidar com reorganizações. O custo é a latência: os dados chegam com minutos de atraso. Muitos produtos precisam de dados mais recentes, então o indexador precisa acompanhar a ponta da cadeia e saber desfazer o que gravou.

Guarde o hash de cada bloco

O número do bloco não muda em uma reorganização. O hash muda. Por isso, todo registro derivado de um bloco deve guardar o hash do bloco de origem, e o indexador deve manter os hashes dos blocos recentes que processou.

schema.sql
CREATE TABLE indexed_blocks (
  chain_id     integer NOT NULL,
  number       bigint  NOT NULL,
  hash         bytea   NOT NULL,
  parent_hash  bytea   NOT NULL,
  PRIMARY KEY (chain_id, number)
);
 
CREATE TABLE token_transfers (
  chain_id      integer      NOT NULL,
  block_number  bigint       NOT NULL,
  block_hash    bytea        NOT NULL,
  tx_hash       bytea        NOT NULL,
  log_index     integer      NOT NULL,
  sender        bytea        NOT NULL,
  recipient     bytea        NOT NULL,
  amount        numeric(78,0) NOT NULL, -- uint256 cabe em 78 dígitos decimais
  PRIMARY KEY (chain_id, tx_hash, log_index)
);

O tipo numeric(78,0) merece nota: valores uint256 não cabem em bigint, e convertê-los para ponto flutuante perde precisão.

Detecte a reorganização pelo parentHash

Ao receber o bloco N, compare o parentHash dele com o hash que você gravou para N - 1. Se forem iguais, a cadeia continua. Se forem diferentes, houve reorganização, e você precisa recuar até encontrar o ancestral comum: o maior bloco cujo hash armazenado coincide com o hash canônico atual.

reorg.ts
async function findCommonAncestor(chainId: number, head: Block): Promise<bigint> {
  let height = head.number - 1n;
  while (true) {
    const stored = await db.blockHash(chainId, height);
    if (!stored) throw new Error(`Reorg deeper than the stored window at ${height}`);
    const canonical = await rpc.getBlockByNumber(height);
    if (canonical.hash === stored) return height;
    height -= 1n;
  }
}

Mantenha uma janela de hashes maior que a reorganização mais profunda que você considera plausível. Se a busca sair da janela, pare e alerte: é melhor interromper a indexação do que gravar dados inconsistentes.

Desfaça e reaplique na mesma transação

Encontrado o ancestral, apague tudo o que foi derivado de blocos acima dele e reprocesse a partir dali. Faça as duas coisas na mesma transação do banco, para que nenhum consumidor veja o estado intermediário.

rollback.sql
BEGIN;
DELETE FROM token_transfers WHERE chain_id = $1 AND block_number > $2;
DELETE FROM indexed_blocks  WHERE chain_id = $1 AND number > $2;
-- reprocessa os blocos canônicos a partir de $2 + 1 e insere os novos registros
COMMIT;

Repare que o rollback é feito por número de bloco, não por transação. Depois de uma reorganização, a mesma transação pode reaparecer em outro bloco com outro log_index; apagar pelo bloco garante que não sobra nenhuma versão antiga.

Saldos derivados e efeitos externos

Duas armadilhas aparecem depois que o básico funciona:

  • Agregados. Se você mantém saldos somando transferências, o rollback também precisa desfazer essas somas. Muitas vezes é mais simples recalcular os agregados afetados a partir dos registros brutos.
  • Efeitos fora do banco. Webhooks, e-mails e chamadas a outros sistemas não podem ser desfeitos com ROLLBACK. Dispare efeitos externos apenas para blocos com a profundidade de confirmação adequada, usando uma outbox para garantir que cada efeito seja enviado uma única vez.

Exponha a confirmação para quem consome

Diferentes consumidores toleram riscos diferentes. Um painel pode exibir dados de latest; um processo de liquidação deve esperar finalized. Em vez de escolher um único nível para todos, exponha o estado de cada registro na API e deixe o consumidor decidir.

GET /transfers?address=0x…
{ "tx_hash": "0x9f…", "amount": "2500000000000000000", "block_number": 21480211, "confirmation": "safe" }

Resumo

Reorganizações não são exceções raras a ignorar: são parte do funcionamento normal de uma blockchain. Guardar o hash de cada bloco, detectar divergências pelo parentHash, desfazer e reaplicar atomicamente e expor o nível de confirmação transformam um indexador frágil em uma fonte de dados confiável.