eth_getTransactionReceipt devuelve null siempre que no haya un recibo disponible para un hash de transacción, lo que incluye los estados pendiente, desconocido y descartado o reemplazado. La especificación JSON-RPC de Ethereum define que un recibo existe solo después de la inclusión en un bloque, por lo que una respuesta null no es un error y no se puede distinguir de un nodo retrasado sin una segunda consulta. Un sondeador correcto consulta tanto eth_getTransactionByHash como eth_getTransactionReceipt, clasifica el par y termina en un intervalo acotado, un tiempo máximo transcurrido y una fecha límite basada en bloques derivada del nonce de la transacción. Este artículo cubre los cuatro estados de transacción, la autoridad del campo status del recibo, el manejo de reorganizaciones y un sondeador Node.js ejecutable con una tabla de resultados para medir contra tu propio endpoint.
Qué significa un recibo nulo según la especificación JSON-RPC de Ethereum
La especificación JSON-RPC de Ethereum define eth_getTransactionReceipt como el método que devuelve el objeto de recibo de un hash de transacción, o null cuando no hay ningún recibo disponible. Un recibo se crea solo después de que una transacción se ha incluido en un bloque y se ha ejecutado, por lo que una respuesta null es el resultado esperado para cualquier transacción que aún no haya alcanzado ese punto. Este es un comportamiento documentado del protocolo, no un defecto del proveedor, y se aplica por igual a Ethereum mainnet y a cadenas compatibles con EVM.
El método complementario eth_getTransactionByHash devuelve el objeto de transacción en sí. Mientras una transacción está solo en el mempool, ese objeto se devuelve con blockNumber establecido en null y no existe ningún recibo. La especificación JSON-RPC 2.0 rige el sobre de solicitud y respuesta, por lo que un resultado null es una respuesta exitosa con un valor null, no un error JSON-RPC. Tratarlo como un error es la causa más común de bucles de sondeo rotos.
- El recibo existe solo después de la inclusión y ejecución.
- Null es un valor de respuesta válido y exitoso.
- eth_getTransactionByHash devuelve el objeto pendiente con blockNumber null.
- Un error JSON-RPC es una condición diferente de un resultado null.
Los cuatro estados de transacción y el par de métodos que los distingue
Un hash de transacción observado por un cliente puede estar en uno de cuatro estados: desconocido, conocido y pendiente, incluido y ejecutado, o descartado o reemplazado. Cada estado produce una combinación distinta de valores de retorno de eth_getTransactionByHash y eth_getTransactionReceipt. Clasificar el par es la única forma fiable de saber en qué estado te encuentras, porque un recibo null por sí solo es ambiguo.
En el estado desconocido, ambos métodos devuelven null. En el estado pendiente, eth_getTransactionByHash devuelve un objeto de transacción con blockNumber null mientras el recibo sigue siendo null. En el estado incluido y ejecutado, ambos métodos devuelven objetos, y el recibo lleva el campo status. En el estado descartado o reemplazado, el objeto de transacción puede desaparecer por completo y el recibo permanece null para siempre. Ese estado final es lo que hace que los bucles de sondeo ingenuos se ejecuten indefinidamente.
- Hash desconocido: ambos métodos devuelven null.
- Conocido y pendiente: objeto de transacción con blockNumber null, recibo null.
- Incluido y ejecutado: ambos objetos presentes, el recibo lleva status.
- Descartado o reemplazado: el objeto de transacción puede desaparecer, el recibo permanece null.
Por qué el recibo es la autoridad para el éxito, no el objeto de transacción
El objeto de transacción devuelto por eth_getTransactionByHash no te dice si la ejecución tuvo éxito. El campo status vive en el recibo, donde 0x1 indica éxito y 0x0 indica una reversión. Una transacción minada con status 0x0 igualmente consume gas, por lo que la existencia de un recibo significa que la transacción fue incluida, no que tuvo éxito. Las aplicaciones que tratan la presencia del recibo como éxito aceptarán silenciosamente transacciones fallidas.
Esta distinción importa para cualquier integración que actúe sobre el resultado de una transacción. El recibo también lleva gasUsed, logs y el precio efectivo del gas, que son los campos necesarios para la contabilidad y el procesamiento de eventos. Para la recuperación masiva de un bloque completo, eth_getBlockReceipts: recibos masivos en una sola llamada devuelve los mismos objetos de recibo en una sola solicitud.
- status 0x1 significa éxito; status 0x0 significa revertida.
- Una transacción revertida igualmente consume gas.
- La presencia del recibo significa inclusión, no éxito.
- Los recibos llevan gasUsed, logs y precio efectivo del gas.
Por qué un null no se puede distinguir de un nodo retrasado sin una segunda consulta
Un recibo null puede significar que la transacción está genuinamente pendiente, o puede significar que el nodo que consultaste aún no ha visto el bloque que la contiene. Una sola llamada a eth_getTransactionReceipt no puede distinguir estos casos. La especificación JSON-RPC de Ethereum no exige que un nodo devuelva un recibo antes de haber procesado el bloque contenedor, y el comportamiento específico del proveedor varía en la rapidez con que un nodo sigue la cabeza de la cadena.
El enfoque correcto es consultar ambos métodos y clasificar el par. Si eth_getTransactionByHash devuelve un objeto de transacción con un blockNumber no nulo mientras el recibo es null, el nodo ha visto la inclusión pero aún no ha producido el recibo, lo cual es una condición de retraso en lugar de una transacción pendiente. Si ambos son null, el hash es desconocido o la transacción ha sido descartada. Esta clasificación basada en el par es la base de un sondeador correcto.
- Una sola consulta de recibo no puede separar pendiente de retrasado.
- Consulta tanto eth_getTransactionByHash como eth_getTransactionReceipt.
- blockNumber no nulo con recibo null indica retraso del nodo.
- Ambos null indica desconocido o descartado.
Diseño de tiempos de espera y presupuesto para un bucle de sondeo seguro
Un bucle de sondeo necesita tres límites independientes para terminar correctamente: un intervalo de sondeo acotado, un tiempo máximo transcurrido y una fecha límite basada en bloques. El intervalo de sondeo evita la inundación de solicitudes y debe elegirse para coincidir con el tiempo de bloque esperado de la cadena objetivo. El tiempo máximo transcurrido limita el presupuesto total de tiempo real para que un llamador no pueda colgarse indefinidamente. La fecha límite basada en bloques es la señal más fiable porque se deriva del estado de la cadena en lugar del tiempo local.
La fecha límite basada en bloques utiliza el nonce de la transacción. Consulta eth_getTransactionCount con la etiqueta de bloque latest para ver si el nonce ha sido consumido. Si la cabeza actual ha avanzado más de un número configurable de bloques más allá del bloque en el que se consumió el nonce, y no existe ningún recibo, es probable que la transacción haya sido descartada o reemplazada. Este patrón está estrechamente relacionado con Gestión de nonces en EVM con eth_getTransactionCount, que cubre en detalle las brechas de nonce y el reemplazo.
- Intervalo de sondeo acotado ajustado al tiempo de bloque esperado.
- Tiempo máximo transcurrido como presupuesto de tiempo real.
- Fecha límite basada en bloques derivada del consumo del nonce.
- Sonda de nonce mediante eth_getTransactionCount('latest').
Exponer el caso descartado o reemplazado con una sonda de nonce
Cuando eth_getTransactionByHash devuelve null y el recibo también es null, la transacción puede haber sido descartada del mempool o reemplazada por otra transacción con el mismo nonce. Una sonda de nonce distingue estos casos. Si eth_getTransactionCount('latest') muestra que el nonce ha sido consumido pero el hash original no tiene recibo, es probable que una transacción de reemplazo haya ocupado su lugar. El error Replacement transaction underpriced es la señal común de que un intento de reemplazo fue rechazado.
Si el nonce no ha sido consumido y la cabeza ha avanzado mucho más allá de la ventana de inclusión esperada, es probable que la transacción haya sido descartada debido a condiciones de tarifas o desalojo del mempool. En ambos casos, el sondeador debe detenerse y reportar una clasificación terminal en lugar de continuar sondeando. Seguir sondeando una transacción descartada es el modo de fallo que produce bucles infinitos en producción.
- Nonce consumido sin recibo sugiere reemplazo.
- Nonce no consumido con cabeza avanzada sugiere descarte.
- Ambos casos son terminales para el sondeador.
- Reporta la clasificación en lugar de sondear para siempre.
Recibos afectados por reorganización frente a recibos desconocidos
Un recibo puede desaparecer después de una reorganización de la cadena. Una transacción que fue incluida en un bloque puede moverse a un bloque diferente o volver al mempool, y el recibo del bloque original ya no estará disponible. Un sondeador de producción debe tratar un recibo que desaparece como una señal para volver a verificar contra una etiqueta de bloque más profunda en lugar de como un fallo permanente. La especificación JSON-RPC de Ethereum permite etiquetas de bloque como finalized y safe, cuyo soporte por parte del proveedor varía según el proveedor.
El enfoque práctico es registrar el número de bloque del recibo y, para operaciones de alto valor, volver a consultar el recibo después de que haya pasado una profundidad de confirmación. Si el recibo sigue presente en el mismo número de bloque, la inclusión es estable. Si se ha movido, el sondeador debe actualizar su registro. Esta es la misma disciplina de reconciliación utilizada en Reconciliación de indexadores EVM bloque por bloque.
- Los recibos pueden desaparecer después de una reorganización.
- Vuelve a verificar contra una etiqueta de bloque más profunda.
- Registra el número de bloque del recibo para comprobaciones de estabilidad.
- La lógica de reconciliación pertenece a los indexadores y flujos de alto valor.
Sondeador de recibos Node.js ejecutable con clasificación por intento
El siguiente sondeador Node.js consulta ambos métodos en cada intento, imprime una clasificación y termina al recibir un recibo, un descarte, un reemplazo o el agotamiento del presupuesto. Utiliza un intervalo acotado, un tiempo máximo transcurrido y una fecha límite basada en bloques. Reemplaza la URL del endpoint con tu propio endpoint RPC y ejecútalo contra una transacción de prueba.
El sondeador no tiene dependencias intencionalmente para que pueda incorporarse a cualquier proyecto Node.js. Utiliza la API fetch global disponible en Node.js 18 y posteriores. La función de clasificación es la lógica central y puede reutilizarse en un servicio más grande.
const RPC_URL = 'https://your-endpoint.example';
const TX_HASH = '0x...';
const POLL_INTERVAL_MS = 4000;
const MAX_ELAPSED_MS = 180000;
const MAX_BLOCKS_PAST_NONCE = 20;
async function rpc(method, params) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(json.error.message);
return json.result;
}
function hexToInt(hex) {
return hex === null || hex === undefined ? null : parseInt(hex, 16);
}
async function classify(txHash) {
const [tx, receipt, headHex, nonceHex] = await Promise.all([
rpc('eth_getTransactionByHash', [txHash]),
rpc('eth_getTransactionReceipt', [txHash]),
rpc('eth_blockNumber', []),
rpc('eth_getTransactionCount', ['latest'])
]);
const head = hexToInt(headHex);
const nonce = hexToInt(nonceHex);
if (receipt) {
return { state: 'included', receipt, head, nonce };
}
if (tx && tx.blockNumber !== null) {
return { state: 'lagging', tx, head, nonce };
}
if (tx && tx.blockNumber === null) {
return { state: 'pending', tx, head, nonce };
}
return { state: 'unknown-or-dropped', head, nonce };
}
async function poll(txHash) {
const start = Date.now();
let lastNonceBlock = null;
while (Date.now() - start < MAX_ELAPSED_MS) {
const result = await classify(txHash);
console.log(new Date().toISOString(), result.state, 'head=' + result.head, 'nonce=' + result.nonce);
if (result.state === 'included') {
return result.receipt;
}
if (result.state === 'unknown-or-dropped') {
if (lastNonceBlock !== null && result.head - lastNonceBlock > MAX_BLOCKS_PAST_NONCE) {
console.log('terminal: dropped or replaced');
return null;
}
lastNonceBlock = result.head;
}
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
console.log('terminal: budget exhausted');
return null;
}
poll(TX_HASH).then((receipt) => {
if (receipt) {
console.log('status', receipt.status, 'block', receipt.blockNumber);
}
});Tabla de resultados para medir contra tu propio endpoint
Debido a que la latencia y el momento de inclusión varían según la cadena, el proveedor y las condiciones de la red, la única medición significativa es la que se toma contra tu propio endpoint. Ejecuta el sondeador anterior contra una transacción conocida y registra el tiempo transcurrido desde el primer sondeo hasta el recibo, el número de intentos y la secuencia de clasificación. Completa la tabla a continuación con tus propias observaciones.
No compares estos números entre proveedores sin controlar el tiempo de bloque y la carga de la red. El propósito de la tabla es establecer una línea base para tu propia integración, de modo que puedas detectar regresiones cuando cambies endpoints o parámetros de sondeo.
- Número de intento: recuento secuencial de iteraciones de sondeo.
- Elapsed ms: milisegundos desde el primer sondeo.
- Clasificación: pending, lagging, included o unknown-or-dropped.
- Bloque cabeza: eth_blockNumber en el momento del intento.
- Nonce: eth_getTransactionCount('latest') en el momento del intento.
Compensaciones frente a suscripciones y recuperación masiva de recibos
El sondeo no es la única opción. Las suscripciones WebSocket pueden entregar nuevas cabezas o logs con menor latencia, pero requieren una conexión persistente y un soporte del proveedor que varía según el proveedor. Para los indexadores que procesan bloques completos, eth_getBlockReceipts: recibos masivos en una sola llamada recupera todos los recibos de un bloque en una sola solicitud, lo que es mucho más eficiente que sondear por transacción.
El sondeo sigue siendo la opción correcta cuando el cliente es de corta duración, cuando el entorno no admite WebSockets o cuando el número de transacciones rastreadas es pequeño. La guía de endpoints RPC (RPC Assistant) cubre la selección de endpoints y las opciones de transporte. Para planificación de precios y rendimiento, consulta Precios de RPC.
- Las suscripciones ofrecen menor latencia pero necesitan conexiones persistentes.
- eth_getBlockReceipts es eficiente para indexadores de bloques completos.
- El sondeo se adapta a clientes de corta duración y conjuntos pequeños de transacciones.
- La elección de transporte y endpoint afecta la fiabilidad.
Solución de problemas de recibos nulos persistentes
Si un recibo permanece null mucho más allá de la ventana de inclusión esperada, revisa primero el objeto de transacción. Un blockNumber no nulo con un recibo null apunta a un retraso del nodo, así que reintenta contra un endpoint diferente o espera a que el nodo se ponga al día. Un objeto de transacción null con un nonce no consumido apunta a un descarte, y la transacción debe reenviarse con tarifas actualizadas.
Si el nonce ha sido consumido pero el hash original no tiene recibo, es probable que una transacción de reemplazo haya usado el mismo nonce. Consulta el hash de reemplazo o inspecciona el bloque en el punto de consumo del nonce. Para un tratamiento más profundo del reemplazo y las brechas de nonce, consulta Replacement transaction underpriced y Gestión de nonces en EVM con eth_getTransactionCount.
- blockNumber no nulo con recibo null: retraso del nodo, reintenta en otro lugar.
- Objeto de transacción null con nonce no consumido: descartada, reenviar.
- Nonce consumido sin recibo: comprobar si hay reemplazo.
- Reorganización: volver a verificar contra una etiqueta de bloque más profunda.
Próximos pasos para el manejo de recibos en producción
Pasa de un único sondeador a una pequeña máquina de estados que registre la clasificación de cada transacción a lo largo del tiempo. Persiste el número de bloque y el estado del recibo para que los consumidores posteriores puedan actuar sobre el éxito o la reversión sin volver a consultar. Añade una profundidad de confirmación antes de tratar un recibo como final, y vuelve a verificar después de una ventana de reorganización.
Para patrones de integración más amplios, comienza en el centro de aprendizaje de OnFinality y revisa la documentación del servicio de API para el comportamiento de los endpoints. La guía de endpoints RPC (RPC Assistant) explica cómo elegir y configurar endpoints para cargas de trabajo de producción.
- Persiste la clasificación y el número de bloque del recibo.
- Añade una profundidad de confirmación antes de la finalidad.
- Vuelve a verificar después de una ventana de reorganización.
- Elige endpoints con comportamiento documentado para tu cadena.