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.
- 1Rama A
- 2Fork
- 3Rollback
- 4Rama B ×2
- 5Reporte
Antes de empezar
- Node.js 20 o superior. No necesita Foundry.
Pasos
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.mjsComprueba
- — 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 .
# 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.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 | Qué hacer |
|---|---|---|
| storedEvents mayor que 3 | La 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 25 | El rollback no borró los eventos de los bloques reemplazados. | Borra todo bloque con número >= forkPoint antes de aplicar la rama nueva. |