Saltar al contenido
TrautsLablab

Fixture determinista

Indexador ante reorgs

Ver cómo un indexador sobrevive a un reorg: deshace los bloques reemplazados, aplica la rama nueva y puede reprocesarla sin duplicar eventos.

Duración
15–25 min
Nivel
Intermedio datos
Pasos
1
Versión
3.29.0-alpha.1

Modelo mental

Un evento se identifica por (blockHash, transactionHash, logIndex), no por el número de bloque. Si el bloque cambia de hash, sus eventos dejan de existir y hay que borrarlos.

  1. 1Rama A
  2. 2Fork
  3. 3Rollback
  4. 4Rama B ×2
  5. 5Reporte

Antes de empezar

  • Node.js 20 o superior. No necesita Foundry.

Pasos

  1. Paso 1Simular el reorg

    Indexar la rama A, revertir desde el bloque 2, aplicar la rama B dos veces y medir el resultado.

    Simulación
    node tools/labs/ethereum-indexer-observability-lab/simulate-reorg.mjs

    Comprueba

    • — forkPoint es 2 y removedEvents es 2 (los logs de 0x02a y 0x03a).
    • — canonicalHead es "0x04b".
    • — storedEvents es 3 aunque la rama B se aplicó dos veces.
    • — aggregateValue es 25 (10 + 7 + 8) y replaySafe es true.
    • — El script termina con código 0.

    Evidencia esperada

    JSON completo del reporte.

El código del laboratorio

Estos son los archivos que ejecutan los pasos, tal como están en el repositorio .

tools/labs/ethereum-indexer-observability-lab/runbook.md
# Replay-safe Ethereum indexer lab

Version: `2.0.44-alpha.3`

Run `node tools/labs/ethereum-indexer-observability-lab/simulate-reorg.mjs`.

The simulation indexes branch A, rolls back from the fork point, applies branch B twice and proves idempotency through the `(blockHash, transactionHash, logIndex)` event identity.

Record fork point, removed events, canonical head, aggregate and replay-safe result. Only Node.js 20+ is required.
tools/labs/ethereum-indexer-observability-lab/simulate-reorg.mjs
const canonicalA = [
  { number: 1, hash: '0x01a', parent: '0x00', logs: [{ tx: '0xa1', index: 0, value: 10 }] },
  { number: 2, hash: '0x02a', parent: '0x01a', logs: [{ tx: '0xa2', index: 0, value: 20 }] },
  { number: 3, hash: '0x03a', parent: '0x02a', logs: [{ tx: '0xa3', index: 0, value: 30 }] },
];
const replacement = [
  { number: 2, hash: '0x02b', parent: '0x01a', logs: [{ tx: '0xb2', index: 0, value: 7 }] },
  { number: 3, hash: '0x03b', parent: '0x02b', logs: [{ tx: '0xb3', index: 0, value: 8 }] },
  { number: 4, hash: '0x04b', parent: '0x03b', logs: [] },
];

const blocks = new Map();
const events = new Map();
const eventKey = (block, log) => `${block.hash}:${log.tx}:${log.index}`;
const apply = (block) => {
  blocks.set(block.number, block);
  for (const log of block.logs) events.set(eventKey(block, log), { ...log, block: block.number, blockHash: block.hash });
};
canonicalA.forEach(apply);

const forkPoint = replacement[0].number;
let removed = 0;
for (const [number, block] of [...blocks]) {
  if (number >= forkPoint) {
    for (const log of block.logs) removed += events.delete(eventKey(block, log)) ? 1 : 0;
    blocks.delete(number);
  }
}
replacement.forEach(apply);
replacement.forEach(apply); // replay proves idempotent keys

const total = [...events.values()].reduce((sum, event) => sum + event.value, 0);
const report = { forkPoint, removedEvents: removed, canonicalHead: blocks.get(4).hash, storedEvents: events.size, aggregateValue: total, replaySafe: events.size === 3 };
console.log(JSON.stringify(report, null, 2));
if (report.canonicalHead !== '0x04b' || total !== 25 || !report.replaySafe) process.exit(1);

Criterios de aceptación

  • El script termina con código 0.
  • Puedes explicar por qué el agregado no es 60 (10 + 20 + 30).
  • Puedes explicar qué pasaría si la clave del evento fuera solo el número de bloque.

Limpieza

Limpiar: No se genera estado persistente.

Si algo falla

Síntoma, causa probable y cómo corregirlo
SíntomaCausaQué hacer
storedEvents mayor que 3La clave del evento dejó de incluir blockHash, tx o logIndex.Vuelve a la clave blockHash:tx:index para que el replay sea idempotente.
aggregateValue distinto de 25El rollback no borró los eventos de los bloques reemplazados.Borra todo bloque con número >= forkPoint antes de aplicar la rama nueva.