Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Guías de red y protocolo14 min de lectura

Retiros de Base OP Stack: Prueba y finalización de transferencias L2 a L1 vía RPC

Una guía completa basada en RPC sobre el ciclo de vida de los retiros de OP Stack en Base: inicio, construcción de la prueba, ventana de desafío y finalización en L1.

TL;DR

Un retiro de OP Stack L2 a L1 en Base es un proceso de dos fases: primero inicias el retiro en L2 llamando a initiateWithdrawal en el predeploy L2ToL1MessagePasser, que registra un hash de retiro en el mapeo sentMessages; luego, después de que haya transcurrido la ventana de desafío de prueba de fallas, pruebas y finalizas el retiro en L1 a través del contrato OptimismPortal. La prueba requiere una prueba de raíz de salida anclada a una salida L2 finalizada y una prueba de almacenamiento (eth_getProof) contra la raíz de almacenamiento de L2ToL1MessagePasser, lo que significa que el nodo L2 que consultas debe conservar el estado del bloque que contiene tu retiro. La finalización está condicionada por la ventana de desafío, un parámetro de protocolo que ha cambiado entre versiones de OP Stack y varía según la cadena y la actualización. Esta guía recorre cada etapa vía JSON-RPC, muestra cómo leer sentMessages y el estado del portal, y proporciona una tabla de estado reproducible para que puedas verificar cada paso de forma independiente.

El ciclo de vida del retiro L2 a L1 de OP Stack en Base

Base es una L2 de OP Stack, y su mecanismo de retiro sigue la especificación de OP Stack: un usuario inicia un retiro en L2, espera a que pase la ventana de desafío de prueba de fallas, luego prueba y finaliza el retiro en L1. La especificación de retiros de OP Stack define los contratos y el formato de mensaje, mientras que la guía de retiros de Optimism Docs describe el flujo operativo. A diferencia de los depósitos, que son impulsados por eventos de L1 y pueden reproducirse en L2, los retiros requieren una prueba explícita en L1 de que la transición de estado de L2 es final.

El ciclo de vida tiene cuatro etapas observables: inicio en L2, propuesta de raíz de salida en L1, prueba en L1 y finalización en L1. Cada etapa puede verificarse de forma independiente vía RPC, lo cual es útil cuando estás conciliando un retiro en un indexador o flujo de soporte. Si eres nuevo en el acceso RPC de Base, la página de la red Base y la guía del endpoint RPC de Base cubren la selección de endpoints y los IDs de cadena.

  • Inicio: llama a initiateWithdrawal en L2ToL1MessagePasser; el hash del retiro se agrega a sentMessages.
  • Propuesta de raíz de salida: un proponente sin permisos publica una raíz de salida de L2 en L1; la disponibilidad del índice puede retrasarse.
  • Prueba: llama a proveWithdrawalTransaction en OptimismPortal con una prueba de raíz de salida y una prueba de almacenamiento.
  • Finalización: después de la ventana de desafío, llama a finalizeWithdrawalTransaction para liberar fondos en L1.

El predeploy L2ToL1MessagePasser y initiateWithdrawal

El L2ToL1MessagePasser es un contrato predeploy en 0x4200000000000000000000000000000000000016 en cada cadena OP Stack, incluida Base. Su función initiateWithdrawal acepta los parámetros del retiro (target, gasLimit, data) y registra el retiro calculando un hash y estableciendo sentMessages[hash] = true. El valor enviado con la llamada queda en custodia en el contrato en L2; no se quema en sentido estricto, pero queda bloqueado hasta que la finalización correspondiente en L1 lo libere desde el OptimismPortal.

El hash del retiro se calcula sobre la tupla (nonce, sender, target, value, gasLimit, data). En la implementación de referencia de OP Stack esto es Hashing.hashWithdrawal, que hashea la estructura de retiro codificada en ABI. El nonce es el nonce del L2ToL1MessagePasser en el momento de la llamada, y el sender es la cuenta L2 que llamó a initiateWithdrawal. Debido a que el hash depende de todos estos campos, debes capturarlos exactamente para reproducirlo más tarde.

  • Dirección del predeploy: 0x4200000000000000000000000000000000000016
  • Función: initiateWithdrawal(address _target, uint256 _gasLimit, bytes _data) payable
  • Mapeo de almacenamiento: sentMessages[bytes32 withdrawalHash] => bool
  • Entradas del hash: nonce, sender, target, value, gasLimit, data

Cálculo del hash de retiro y lectura de sentMessages vía RPC de L2

Para confirmar que se inició un retiro, calculas el hash del retiro localmente y luego lees sentMessages[hash] del almacenamiento de L2ToL1MessagePasser. El mapeo está en un slot de almacenamiento conocido; el slot para un valor de mapeo es keccak256(abi.encode(key, slot)). El L2ToL1MessagePasser almacena sentMessages en el slot 0 en la implementación de referencia, pero debes verificar el slot contra el contrato desplegado para tu cadena y actualización.

El siguiente ejemplo de Node.js usa viem para calcular el hash del retiro y leer el slot de almacenamiento a través de un endpoint RPC de Base. Asume que tienes los parámetros del retiro del recibo de la transacción L2 o de tus propios datos de llamada. Reemplaza la URL RPC con el endpoint de tu proveedor; la página del servicio API describe cómo OnFinality expone RPC de Base.

import { createPublicClient, http, keccak256, encodeAbiParameters, parseAbiParameters } from 'viem';
import { base } from 'viem/chains';

const client = createPublicClient({ chain: base, transport: http('https://base.api.onfinality.io/public') });

const MESSAGE_PASSER = '0x4200000000000000000000000000000000000016';
const SENT_MESSAGES_SLOT = 0n;

// Withdrawal parameters captured from the L2 transaction
const withdrawal = {
  nonce: 0n,
  sender: '0xYourL2SenderAddress',
  target: '0xYourL1TargetAddress',
  value: 1000000000000000n,
  gasLimit: 100000n,
  data: '0x'
};

// Compute the withdrawal hash (matches Hashing.hashWithdrawal)
const encoded = encodeAbiParameters(
  parseAbiParameters('uint256, address, address, uint256, uint256, bytes'),
  [withdrawal.nonce, withdrawal.sender, withdrawal.target, withdrawal.value, withdrawal.gasLimit, withdrawal.data]
);
const withdrawalHash = keccak256(encoded);
console.log('withdrawalHash:', withdrawalHash);

// Compute the storage slot for sentMessages[withdrawalHash]
const slot = keccak256(
  encodeAbiParameters(parseAbiParameters('bytes32, uint256'), [withdrawalHash, SENT_MESSAGES_SLOT])
);

// Read the storage value over L2 RPC
const storage = await client.getStorageAt({
  address: MESSAGE_PASSER,
  slot,
  blockNumber: 'latest'
});
console.log('sentMessages[hash]:', storage);
// A non-zero value confirms the withdrawal was initiated.

Por qué la finalización está condicionada por la ventana de desafío de prueba de fallas

Un retiro no puede finalizarse en L1 inmediatamente después del inicio porque la transición de estado de L2 que incluye el retiro primero debe finalizarse a través del sistema de prueba de fallas. En cadenas OP Stack, esta es la ventana de desafío: un período durante el cual cualquiera puede disputar la raíz de salida de L2 propuesta. La ventana es un parámetro de protocolo que ha cambiado entre versiones de OP Stack y varía según la cadena y la actualización; la cifra comúnmente citada es de aproximadamente siete días, pero debes tratarla como documentada / varía según la cadena y la actualización en lugar de una constante fija.

La consecuencia práctica es que el momento de liberación de tus fondos en L1 está determinado por cuándo se finaliza la raíz de salida que incluye tu retiro, no por cuándo iniciaste el retiro. Las raíces de salida son propuestas por proponentes sin permisos, por lo que el índice de la raíz de salida que contiene tu retiro puede retrasarse respecto al head de L2. Si estás siguiendo la semántica de finalidad de manera más amplia, consulta Finalidad de Base OP Stack, bloques safe y finalized.

  • Ventana de desafío: un período de disputa para las raíces de salida de L2 propuestas; aproximadamente siete días en muchas implementaciones.
  • Propuesta de raíz de salida: sin permisos; la disponibilidad del índice puede retrasarse respecto al head de L2.
  • Condición de finalización: la raíz de salida que contiene tu retiro debe estar finalizada en L1.
  • Deriva de parámetros: la duración de la ventana y las interfaces de contrato han cambiado entre versiones de OP Stack.

El OptimismPortal y la llamada proveWithdrawalTransaction

El OptimismPortal es el contrato en L1 que mantiene los fondos en custodia y procesa los retiros. Su función proveWithdrawalTransaction toma los parámetros del retiro, una prueba de raíz de salida y una prueba de almacenamiento. La prueba de raíz de salida ancla tu retiro a una raíz de salida de L2 específica que ha sido propuesta a L1; la prueba de almacenamiento demuestra que sentMessages[withdrawalHash] está establecido en el almacenamiento de L2ToL1MessagePasser en el bloque L2 de esa raíz de salida.

La prueba de raíz de salida incluye la raíz de estado de L2, la raíz de almacenamiento del message passer, el hash del bloque L2 y el número de bloque L2. La prueba de almacenamiento se genera mediante eth_getProof contra la dirección L2ToL1MessagePasser en el mismo bloque L2. Debido a que la prueba está anclada a un bloque L2 específico, el nodo L2 que consultas debe conservar el estado de ese bloque. Un nodo que ha podado el bloque L2 necesario no puede producir la prueba, por lo que a menudo se requiere acceso de archivo para retiros que se prueban mucho después del inicio.

  • OptimismPortal: contrato en L1 que custodia fondos y procesa llamadas de prueba/finalización.
  • proveWithdrawalTransaction: envía el retiro, la prueba de raíz de salida y la prueba de almacenamiento.
  • Prueba de raíz de salida: raíz de estado de L2, raíz de almacenamiento del message passer, hash del bloque L2, número de bloque L2.
  • Prueba de almacenamiento: eth_getProof contra L2ToL1MessagePasser en el bloque L2 anclado.

Construcción de la prueba en L1 desde el estado de archivo de L2 con eth_getProof

La prueba de almacenamiento para proveWithdrawalTransaction se produce mediante eth_getProof en el nodo L2. La llamada toma la dirección L2ToL1MessagePasser, un arreglo de claves de almacenamiento (el slot de sentMessages para tu hash de retiro) y el número de bloque L2 que corresponde a la raíz de salida contra la que estás probando. La respuesta incluye la prueba de cuenta, la prueba de almacenamiento y la raíz de almacenamiento, que luego ensamblas en los argumentos de prueba de raíz de salida y prueba de almacenamiento.

El siguiente ejemplo muestra la forma de la llamada eth_getProof y cómo leer el estado del portal para determinar si la finalización está desbloqueada. Usa JSON-RPC sin procesar para la llamada de prueba para que la forma de la solicitud sea explícita, y viem para la lectura del portal. El portal expone una interfaz de params o dispute game que varía según la versión de OP Stack; debes leer el ABI desplegado para tu cadena y actualización.

import { createPublicClient, http, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';

const l1 = createPublicClient({ chain: mainnet, transport: http('https://eth.api.onfinality.io/public') });
const l2Rpc = 'https://base.api.onfinality.io/public';

const MESSAGE_PASSER = '0x4200000000000000000000000000000000000016';
const withdrawalHash = '0xYourWithdrawalHash';
const l2BlockNumber = '0xYourL2BlockNumberHex';

// Compute the storage slot for sentMessages[withdrawalHash]
// (slot 0 in the reference implementation; verify against the deployed contract)
const slot = '0xYourComputedStorageSlot';

// eth_getProof against the L2ToL1MessagePasser at the anchored L2 block
const proof = await fetch(l2Rpc, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'eth_getProof',
    params: [MESSAGE_PASSER, [slot], l2BlockNumber]
  })
}).then(r => r.json());

console.log('accountProof:', proof.result.accountProof.length, 'nodes');
console.log('storageProof:', JSON.stringify(proof.result.storageProof));

// Read the portal to check whether finalization is unlocked
const PORTAL = '0xYourOptimismPortalAddress';
const portalAbi = parseAbi([
  'function finalizedWithdrawals(bytes32) view returns (bool)',
  'function proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),uint256,(bytes32,bytes32,bytes32,bytes32),bytes[])'
]);

const isFinalized = await l1.readContract({
  address: PORTAL,
  abi: portalAbi,
  functionName: 'finalizedWithdrawals',
  args: [withdrawalHash]
});
console.log('finalizedWithdrawals[hash]:', isFinalized);

Lectura de la ventana de desafío y sondeo de elegibilidad para finalización

Para saber cuándo se permite la finalización, necesitas determinar cuánto queda de la ventana de desafío para la raíz de salida que ancla tu retiro. El OptimismPortal y los contratos de dispute game exponen estado que te permite calcular esto, pero la interfaz exacta varía según la versión de OP Stack. En implementaciones antiguas, el L2OutputOracle expone la marca de tiempo de la salida y el período de finalización; en implementaciones más nuevas, el dispute game expone el tiempo de creación del juego y el estado de resolución. Debes leer el ABI desplegado para tu cadena y actualización en lugar de asumir una única interfaz.

Una estrategia de sondeo práctica es leer el mapeo finalizedWithdrawals del portal y el estado relevante de salida o juego en cada sondeo, y tratar la finalización como desbloqueada solo cuando la raíz de salida esté finalizada y la ventana de desafío haya transcurrido. Debido a que las raíces de salida son propuestas por proponentes sin permisos, es posible que necesites sondear el índice de la raíz de salida que contiene tu retiro antes de poder probarlo. La página Derivación y marca de tiempo de L1 de Base OP Stack cubre cómo se relacionan las marcas de tiempo de L1 con los bloques L2, lo cual es útil al razonar sobre la expiración de la ventana.

  • Lee el mapeo finalizedWithdrawals del portal para verificar si la finalización ya ocurrió.
  • Lee el estado del output oracle o del dispute game para determinar la expiración de la ventana.
  • Sondea el índice de la raíz de salida que contiene tu retiro; el tiempo del proponente puede retrasarse.
  • Trata la finalización como desbloqueada solo cuando la raíz de salida esté finalizada y la ventana haya transcurrido.

Tabla de estado de retiro reproducible y verificación independiente

Debido a que el tiempo de retiro depende de parámetros de protocolo y del comportamiento del proponente, el enfoque más confiable es registrar cada etapa en una tabla de estado y verificarla de forma independiente. La tabla a continuación es una plantilla que completas por retiro; cada columna corresponde a una llamada RPC o transacción que puedes volver a ejecutar para confirmar la etapa. Esto hace que el flujo de trabajo sea auditable y te ayuda a aislar qué etapa se está retrasando cuando un retiro parece atascado.

Al conciliar retiros en un indexador, se aplica la misma disciplina: verifica el inicio en L2, la propuesta de raíz de salida, la transacción de prueba en L1 y la transacción de finalización en L1 como eventos separados. La página Reconciliación de indexador EVM bloque por bloque describe un enfoque general de reconciliación que se adapta bien al seguimiento de retiros.

  • Hash de tx L2: la transacción que llamó a initiateWithdrawal; verifica el estado y los logs.
  • Hash de retiro: calculado a partir de nonce, sender, target, value, gasLimit, data; verifícalo contra sentMessages.
  • Índice de raíz de salida: la propuesta de salida en L1 que incluye tu retiro; verifica que esté finalizada.
  • Tx de prueba: la transacción en L1 que llama a proveWithdrawalTransaction; verifica que la prueba fue aceptada.
  • Tx de finalización: la transacción en L1 que llama a finalizeWithdrawalTransaction; verifica que finalizedWithdrawals sea true.
  • Expiración de ventana: la marca de tiempo después de la cual se permite la finalización; verifícala contra el contrato desplegado.

Solución de problemas comunes de prueba y finalización de retiros

La mayoría de los fallos de retiro caen en unas pocas categorías: la prueba no se puede generar porque el nodo L2 ha podado el bloque necesario, el índice de la raíz de salida aún no está disponible, la ventana de desafío no ha transcurrido, o los argumentos de la prueba no coinciden con la raíz de salida anclada. Cada uno de estos produce un error o motivo de revert distinto, y cada uno puede diagnosticarse con una llamada RPC específica. Comienza confirmando que sentMessages[withdrawalHash] está establecido en L2 en el bloque contra el que pretendes probar.

Si eth_getProof falla o devuelve un error sobre estado faltante, el nodo que estás consultando probablemente ha podado el bloque L2. Necesitas un nodo de archivo o un proveedor que conserve el estado histórico del bloque relevante. Si proveWithdrawalTransaction hace revert, verifica que la prueba de raíz de salida coincida con la raíz de salida que realmente se propuso, y que el número de bloque L2 en la prueba corresponda al bloque que contiene tu retiro. Si finalizeWithdrawalTransaction hace revert, verifica la ventana de desafío y el mapeo finalizedWithdrawals.

  • Estado faltante: el nodo L2 podó el bloque; usa un nodo de archivo o un proveedor de estado histórico.
  • Discrepancia de raíz de salida: la prueba se ancla a una raíz de salida diferente a la propuesta.
  • Ventana no transcurrida: la finalización está condicionada por la ventana de desafío; sondea el estado del portal.
  • Ya finalizado: finalizedWithdrawals[hash] es true; el retiro ha sido liberado.
  • Slot de almacenamiento incorrecto: verifica el slot de sentMessages contra el contrato desplegado para tu cadena.

Limitaciones, compensaciones y deriva de parámetros de protocolo

La ventana de desafío es un parámetro de protocolo que ha cambiado entre versiones de OP Stack y varía según la cadena y la actualización; no debes codificar un único valor. Las raíces de salida son propuestas por proponentes sin permisos, por lo que la disponibilidad del índice para tu retiro puede retrasarse respecto al head de L2, y no hay garantía de que un proponente publique con una cadencia específica. El estado de L2 necesario para una prueba aún debe ser conservado por el nodo que consultas, lo que significa que a menudo se requiere acceso de archivo para retiros probados mucho después del inicio.

Un retiro finalizado es final en L1, pero la quema en L2 es irreversible si la prueba en L1 es incorrecta. En otras palabras, si pruebas contra la raíz de salida incorrecta o con argumentos de prueba malformados, es posible que no puedas finalizar, y la custodia en L2 permanece bloqueada. Por eso vale la pena el esfuerzo de verificar cada etapa de forma independiente, como se describe en la tabla de estado anterior. Para consideraciones de endpoint y precios al ejecutar estas consultas a escala, consulta Precios de RPC.

  • Ventana de desafío: documentada / varía según la cadena y la actualización; no la codifiques de forma fija.
  • Propuesta de raíz de salida: sin permisos; la disponibilidad del índice puede retrasarse.
  • Retención de estado de L2: el nodo que consultas aún debe tener el bloque necesario.
  • Irreversibilidad: una prueba incorrecta en L1 puede dejar la custodia en L2 bloqueada.
  • Deriva de interfaz: los ABI del portal y del oracle varían entre versiones de OP Stack.

Próximos pasos para el seguimiento de retiros en producción

Para sistemas en producción, el enfoque más robusto es tratar cada retiro como una máquina de estados con etapas verificables de forma independiente, y persistir el hash del retiro, el índice de la raíz de salida, la transacción de prueba y la transacción de finalización como registros separados. Esto te permite reintentar la prueba o la finalización sin volver a derivar todo el historial, y facilita mostrar qué etapa se está retrasando. Si estás construyendo sobre Base, la página de la red Base y el centro de aprendizaje de OnFinality son buenos puntos de partida para temas relacionados de RPC.

Si necesitas comparar el lado de depósito del flujo, la página Eventos de depósito y pruebas de retiro de Base OP Stack cubre los depósitos de L1 a L2 y el hash de depósito. Para la selección de endpoints y herramientas, la guía del endpoint RPC de Base y la página del servicio API describen cómo conectarte. Como siempre, verifica los parámetros de protocolo contra los contratos desplegados para tu cadena y actualización antes de confiar en ellos en automatización.

  • Modela cada retiro como una máquina de estados con etapas verificables de forma independiente.
  • Persiste el hash del retiro, el índice de la raíz de salida, la tx de prueba y la tx de finalización por separado.
  • Reintenta la prueba o la finalización sin volver a derivar todo el historial.
  • Verifica los parámetros de protocolo contra los contratos desplegados para tu cadena y actualización.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar