Base y otras cadenas OP-Stack exponen tres etapas de bloques: unsafe (publicado por el secuenciador, reemplazable), safe (derivado de datos de lote L1, aún reorganizable si L1 se reorganiza) y finalized (finalizado en L1, tratado como irreversible). A diferencia de Ethereum, la cabeza unsafe puede ser reemplazada en su totalidad por un bloque L2 diferente que derive un nuevo lote L1, por lo que contar la profundidad por hash padre por sí solo subestima la exposición. El límite de reversión correcto es el bloque L2 más alto cuyo origen L1 siga siendo canónico en L1, que puede estar muy por detrás de la altura L2. Este artículo ofrece un diseño de indexador consciente de la derivación con una ventana unsafe, una marca de agua de origen L1, un registro de deshacer, upserts idempotentes y una pasada de aserción posterior a la reversión. También proporciona una tabla de resultados para medir tu propio endpoint y una sección de solución de problemas para brechas de etiquetas del proveedor y backfills en curso.
El modelo OP-Stack de tres etapas y qué garantiza cada etapa
La especificación de OP Stack describe cómo se derivan los bloques L2 a partir de datos de lote L1, y la documentación de Optimism describe la progresión de unsafe a safe y finalized. La cabeza unsafe es producida por el secuenciador y aún no se deriva de L1, por lo que es reemplazable. Los bloques safe se derivan de datos de lote L1 pero siguen siendo reorganizables si L1 mismo se reorganiza. Los bloques finalized están finalizados en L1 y se tratan como irreversibles.
Aquí solo se cita el mecanismo. El comportamiento exacto de las etiquetas de bloque safe y finalized varía según el proveedor, por lo que debes medirlo contra tu propio endpoint. La especificación JSON-RPC de Ethereum define eth_getBlockByNumber con las etiquetas safe y finalized, y eth_blockNumber como la ruta de lectura que un indexador usa para detectar el progreso de derivación.
Para un indexador, el contrato práctico es: escribir datos unsafe solo dentro de una ventana acotada, tratar safe como una garantía más fuerte pero no absoluta, y tratar finalized como la única etapa adecuada para efectos secundarios irreversibles. Si necesitas repasar cómo se leen las etiquetas, consulta Finalidad de Base OP-Stack: etiquetas de bloque safe y finalized.
Ambas definiciones de etapa provienen de fuentes primarias y no de este artículo: la especificación de derivación de OP Stack define cómo se derivan los bloques L2 a partir de datos de lote L1, y la documentación de finalidad de transacciones de Optimism define la progresión unsafe, safe y finalized. Léelas juntas, porque la especificación explica el mecanismo mientras que la página de finalidad explica las etiquetas que realmente consultarás.
- Unsafe: publicado por el secuenciador, aún no derivado de L1, reemplazable.
- Safe: derivado de datos de lote L1, aún reorganizable si L1 se reorganiza.
- Finalized: finalizado en L1, tratado como irreversible.
- El comportamiento de las etiquetas varía según el proveedor; mide antes de confiar en él.
Por qué las reorganizaciones de OP-Stack difieren de las de Ethereum
En Ethereum, una reorganización es un evento de elección de bifurcación: una cadena competidora de bloques reemplaza la cadena canónica a cierta profundidad. En una cadena OP-Stack, la cabeza unsafe es producida por el secuenciador, por lo que un reinicio del secuenciador, un bloque inválido o un secuenciador detenido pueden hacer que el pipeline de derivación reemplace una serie de bloques unsafe en lugar de extenderlos. La cobertura pública de la parada del secuenciador de Base ilustra cómo un fallo del secuenciador puede producir una reorganización en lugar de un hueco.
Esto significa que el conteo normal de profundidad por hash padre subestima la exposición. Un detector que solo compara hashes padre en la altura N verá una discrepancia, pero no sabrá si el reemplazo fue una bifurcación superficial o un reemplazo total impulsado por la derivación de una serie más larga. La especificación de OP Stack es autoritativa sobre por qué la cabeza unsafe aún no se deriva de L1 y cómo un fallo del secuenciador produce una reorganización en lugar de un hueco.
La consecuencia práctica es que tu límite de reversión no debe derivarse solo de la profundidad L2. Debe derivarse del origen L1 de cada bloque L2, que es el tema de la siguiente sección.
- Las reorganizaciones de Ethereum son eventos de elección de bifurcación; las de OP-Stack pueden ser reemplazos impulsados por la derivación.
- Un reinicio del secuenciador, un bloque inválido o una parada pueden reemplazar una serie de bloques unsafe.
- El conteo de profundidad por hash padre por sí solo subestima la exposición.
- La especificación de OP Stack es autoritativa para la semántica de derivación.
La ventana de origen L1 y la marca de agua de L1 canónico
Cada bloque L2 está asociado con un bloque de origen L1. El límite de reversión correcto es el bloque L2 más alto cuyo origen L1 siga siendo canónico en L1. Este límite puede estar muy por detrás de la altura L2, especialmente cuando la cabeza unsafe ha avanzado más rápido que la derivación L1.
Para mantener una marca de agua de L1 canónico, lee el origen L1 por bloque L2 y compáralo con la cadena L1 canónica. Si el origen L1 ya no es canónico, cada bloque L2 derivado de él debe considerarse sospechoso. Este es el equivalente consciente de la derivación de un punto de bifurcación.
La ruta de lectura usa eth_getBlockByNumber en el endpoint L2 para obtener el bloque y sus metadatos de origen L1, y en el endpoint L1 para confirmar la canonicalidad. La especificación JSON-RPC de Ethereum es autoritativa para estos métodos. Para un detector genérico que puedes adaptar, consulta Detección de reorganizaciones de bloques en Ethereum y profundidad de confirmación.
- Límite de reversión = bloque L2 más alto cuyo origen L1 sigue siendo canónico en L1.
- El límite puede estar muy por detrás de la altura L2.
- Mantén una marca de agua de L1 canónico comparando los orígenes L1 con el L1 canónico.
- Usa
eth_getBlockByNumbertanto en L2 como en L1 para confirmar.
Elegir una política de escritura: unsafe con deshacer, solo safe o tabla sombra
Hay tres políticas de escritura prácticas. Aplicar unsafe con registro de deshacer escribe bloques unsafe inmediatamente pero registra un log de deshacer para que una reversión pueda eliminarlos o reemplazarlos. Aplicar solo safe espera la etiqueta safe, intercambiando latencia por menor riesgo. Aplicar unsafe con una tabla sombra escribe datos unsafe en una tabla sombra y los promueve a la tabla principal solo después de que se vuelven safe.
El intercambio latencia/riesgo es directo: unsafe con deshacer ofrece la latencia más baja y la mayor complejidad de reversión; solo safe ofrece la latencia más alta y la historia de corrección más simple; la tabla sombra queda en el medio. Una ventana unsafe acotada por profundidad es el compromiso recomendado: escribe datos unsafe solo dentro de un número fijo de bloques detrás de la cabeza unsafe, y trata cualquier cosa más antigua como solo safe.
El tamaño de la ventana debe medirse, no asumirse. Usa la tabla de resultados más adelante en este artículo para registrar la profundidad unsafe observada y la reversión más profunda contra tu propio endpoint.
- Unsafe con deshacer: latencia más baja, mayor complejidad de reversión.
- Solo safe: latencia más alta, corrección más simple.
- Tabla sombra: punto medio con paso de promoción.
- Recomendado: ventana unsafe acotada por profundidad, medida contra tu endpoint.
El procedimiento de reversión: detectar, confirmar, punto de bifurcación, eliminar, reaplicar
La detección comienza con una discrepancia de hash padre o hash en una altura que ya escribiste. Confirma la nueva cabeza canónica leyendo el bloque actual en esa altura. Encuentra el punto de bifurcación retrocediendo hasta que el hash almacenado coincida con el hash canónico. Elimina o reemplaza todas las filas escritas por encima del punto de bifurcación, luego reaplica hacia adelante.
Implementa la reaplicación como upserts idempotentes con clave en una identidad de bloque canónica (por ejemplo, hash de bloque más índice de log). Esto asegura que la reaplicación no pueda contar doble. La especificación JSON-RPC 2.0 es autoritativa para el sobre de objeto de error devuelto cuando un bloque solicitado ya no es canónico, lo cual es una señal útil durante la detección.
Para un patrón de reconciliación más amplio, consulta Reconciliación de indexadores EVM bloque por bloque.
- Detecta mediante discrepancia de hash padre o hash en una altura escrita.
- Confirma la nueva cabeza canónica.
- Encuentra el punto de bifurcación retrocediendo hasta un hash coincidente.
- Elimina o reemplaza filas por encima del punto de bifurcación.
- Reaplica hacia adelante con upserts idempotentes con clave en identidad canónica.
Probar que la reversión funcionó: reconciliación y contadores de reversión
Una reversión solo es observable si puedes probarla. Agrega una consulta de reconciliación que asegure que el hash de bloque de cada fila indexada siga siendo canónico. Agrega un contador de filas revertidas por evento de bifurcación para que una reversión sea observable en lugar de inferida.
La pasada de aserción debe ejecutarse después de cada reversión y según un calendario. Si la aserción falla, tienes corrupción silenciosa, no una reversión limpia. Esta es la diferencia entre una reversión y corrupción silenciosa, y es el contrato operativo que las páginas de especificación no te dan.
Mantén el contador por evento de bifurcación para que puedas correlacionar el tamaño de la reversión con la ventana de origen L1 y con el comportamiento de las etiquetas del proveedor.
- Asegura que el hash de bloque de cada fila indexada siga siendo canónico.
- Cuenta las filas revertidas por evento de bifurcación.
- Ejecuta la aserción después de cada reversión y según un calendario.
- Una aserción fallida significa corrupción silenciosa, no una reversión limpia.
Indexador Node.js ejecutable con ventana unsafe, marca de agua L1 y registro de deshacer
El siguiente ejemplo usa un almacén en memoria mínimo para mostrar la forma de la lógica. Reemplaza el almacén con tu base de datos y las llamadas RPC con tu proveedor. Lee la cabeza unsafe, mantiene una marca de agua de origen L1, detecta una bifurcación por discrepancia de hash y reaplica hacia adelante con upserts idempotentes.
El código es intencionalmente pequeño para que puedas adaptarlo. No incluye lógica de reintentos ni limitación de velocidad; agrégalos para producción. Para la configuración del endpoint, consulta URL RPC de Base, ID de cadena y configuración de endpoint (RPC Assistant).
const { JsonRpcProvider } = require('ethers');
const L2_URL = process.env.L2_RPC_URL;
const L1_URL = process.env.L1_RPC_URL;
const UNSAFE_WINDOW = 64;
const l2 = new JsonRpcProvider(L2_URL);
const l1 = new JsonRpcProvider(L1_URL);
// In-memory store: height -> { hash, l1Origin, rows: [] }
const store = new Map();
const undoLog = [];
let rolledBackRows = 0;
async function getL2Block(n) {
const b = await l2.send('eth_getBlockByNumber', ['0x' + n.toString(16), false]);
if (!b) return null;
// L1 origin is provider-specific; read from block metadata if available.
const l1Origin = b.l1BlockNumber ? parseInt(b.l1BlockNumber, 16) : null;
return { number: n, hash: b.hash, parentHash: b.parentHash, l1Origin };
}
async function isL1Canonical(l1Number, l1Hash) {
if (l1Number == null) return true;
const b = await l1.send('eth_getBlockByNumber', ['0x' + l1Number.toString(16), false]);
return b && b.hash === l1Hash;
}
async function upsertRows(block) {
// Idempotent upsert keyed on block hash + row index.
const rows = store.get(block.number)?.rows || [];
store.set(block.number, { hash: block.hash, l1Origin: block.l1Origin, rows });
}
async function detectAndRollback(headNumber) {
for (let n = headNumber; n >= 0; n--) {
const stored = store.get(n);
if (!stored) continue;
const canonical = await getL2Block(n);
if (!canonical || canonical.hash !== stored.hash) {
// Fork point found at n+1; roll back everything above n.
for (let m = headNumber; m > n; m--) {
const s = store.get(m);
if (s) {
rolledBackRows += s.rows.length;
undoLog.push({ height: m, hash: s.hash, rows: s.rows });
store.delete(m);
}
}
return n + 1;
}
}
return 0;
}
async function tick() {
const head = await l2.getBlockNumber();
const start = Math.max(0, head - UNSAFE_WINDOW);
for (let n = start; n <= head; n++) {
const b = await getL2Block(n);
if (!b) continue;
if (b.l1Origin != null) {
const ok = await isL1Canonical(b.l1Origin, null);
if (!ok) continue; // skip until L1 origin is canonical
}
await upsertRows(b);
}
const forkPoint = await detectAndRollback(head);
if (forkPoint > 0) {
console.log('Rolled back to', forkPoint, 'rows removed:', rolledBackRows);
}
}
setInterval(tick, 5000);Pasada de aserción posterior a la reversión y verificación de canonicalidad
Después de una reversión, ejecuta una pasada de aserción que lea cada altura almacenada y confirme que el hash almacenado aún coincide con el hash canónico. Esta es la prueba de que la reversión funcionó. El siguiente ejemplo es una pasada de aserción compacta que puedes ejecutar según un calendario.
Si la aserción falla, no reapliques a ciegas. Investiga si el proveedor devolvió un bloque obsoleto o si tu marca de agua de origen L1 es incorrecta. Para una ruta de lectura relacionada, consulta Estado de sincronización del nodo Base a través de Engine API.
async function assertCanonical() {
let checked = 0;
let mismatches = 0;
for (const [height, stored] of store.entries()) {
const canonical = await getL2Block(height);
checked++;
if (!canonical || canonical.hash !== stored.hash) {
mismatches++;
console.error('Mismatch at', height, 'stored', stored.hash, 'canonical', canonical && canonical.hash);
}
}
console.log('Assertion pass: checked', checked, 'mismatches', mismatches);
return mismatches === 0;
}
// Run after every rollback and on a schedule.
setInterval(assertCanonical, 60000);Tabla de resultados para medir tu propio endpoint
Usa la siguiente tabla para registrar mediciones contra tu propio endpoint. No confíes en números publicados; mide. Ejecuta el indexador durante una ventana fija y completa las columnas.
Las columnas capturan la profundidad unsafe observada, los eventos de bifurcación por ventana, la reversión más profunda y el retraso del origen L1. Estos son los cuatro números que determinan el tamaño de tu ventana unsafe y tu estrategia de reversión.
- Profundidad unsafe observada: cabeza unsafe menos cabeza safe, muestreada durante la ventana.
- Eventos de bifurcación por ventana: recuento de discrepancias de hash detectadas.
- Reversión más profunda: número máximo de bloques revertidos en un solo evento.
- Retraso del origen L1: cabeza L2 menos el bloque L2 más alto cuyo origen L1 es canónico.
Modos de fallo y solución de problemas
Un proveedor cuyas etiquetas safe/finalized no están pobladas es un modo de fallo común; el problema de op-reth 'op-reth is not informing safe and finalized blocks' es un ejemplo público. Si faltan las etiquetas, recurre al seguimiento del origen L1 y mide la brecha tú mismo.
Una reversión que llega mientras un backfill está en curso puede causar doble conteo si el backfill escribe filas por encima del punto de bifurcación. Pausa el backfill durante una reversión, o haz que el backfill sea idempotente. Un detector genérico de hash padre que pasa por alto un reemplazo impulsado por la derivación subestimará; agrega la verificación del origen L1. Las reorganizaciones cuya profundidad excede la ventana retenida requieren una resincronización completa desde el último bloque finalizado.
Para el comportamiento específico del proveedor, trátalo como documentado / varía según el proveedor y verifícalo contra tu endpoint. Para el contexto de la red Base, consulta Red Base.
- Etiquetas safe/finalized faltantes: recurre al seguimiento del origen L1.
- Backfill en curso durante la reversión: pausa o haz idempotente.
- Detector genérico de hash padre: agrega la verificación del origen L1.
- La profundidad excede la ventana retenida: resincroniza desde el último bloque finalizado.
Limitaciones, compensaciones y separar la finalidad de la ingesta
La verificación del origen L1 agrega un costo adicional de lectura L1 por bloque. Las escrituras solo safe agregan una penalización de latencia. Ambos son costos reales, y ninguno es gratis. La compensación es corrección frente a latencia y costo.
La finalidad debe ser una ruta de escritura separada de la ingesta. La ingesta escribe datos unsafe y safe con un registro de deshacer; la finalidad promueve filas a un estado irreversible. Mezclar ambas hace que la lógica de reversión sea más difícil de razonar.
Para el contexto de costos en lecturas L1, consulta Cálculo de tarifas L1 de OP-Stack y costo de transacción.
- La verificación del origen L1 agrega costo de lectura L1.
- Las escrituras solo safe agregan latencia.
- Mantén la finalidad como una ruta de escritura separada de la ingesta.
- Mezclar ingesta y finalidad complica el razonamiento de reversión.
Próximos pasos y lectura relacionada
Comienza midiendo tu endpoint con la tabla de resultados, luego elige una política de escritura e implementa el registro de deshacer y la pasada de aserción. Si eres nuevo en los endpoints de Base, comienza con URL RPC de Base, ID de cadena y configuración de endpoint (RPC Assistant).
Para temas adyacentes, consulta Depósitos, pruebas de retiro y eventos de depósito en Base y el centro de aprendizaje de OnFinality. Para opciones de infraestructura, consulta Servicio de API y Precios de RPC.
- Mide tu endpoint con la tabla de resultados.
- Elige una política de escritura e implementa el registro de deshacer más la pasada de aserción.
- Revisa las páginas adyacentes de finalidad, reorganización y reconciliación.
- Considera la infraestructura y los precios para escala de producción.