Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Solución de problemas de RPC13 min de lectura

Monad eth_getLogs: límites de rango de bloques inválidos y paginación de logs

Por qué Monad devuelve 'invalid block range' para eth_getLogs, cómo su protección de rango documentada difiere de Ethereum, y un algoritmo de paginación acotado que demuestra completitud.

TL;DR

Monad documenta un límite de rango de bloques para eth_getLogs en su sección de Límites JSON-RPC que difiere de los límites prácticos de la red principal de Ethereum, y excederlo devuelve un error en lugar de un resultado parcial. El remedio documentado es paginar el rango y fusionar los resultados. Debido a que la protección rechaza la solicitud antes de escanear, obtienes 'invalid block range' en lugar de un array vacío, por lo que los reintentos ingenuos y los tiempos de espera más amplios no ayudan. Este artículo separa el comportamiento documentado del protocolo del comportamiento específico del proveedor, y luego ofrece un algoritmo de paginación acotado que descubre el rango efectivo por solicitud mediante bisección, asegura cobertura contigua con nextFrom = lastTo + 1, y verifica el orden monotónico de blockNumber. Incluye un paginador Node.js ejecutable con reintentos con jitter, una tabla de resultados que completas con tu propio endpoint de Monad, y una sección de limitaciones sobre la retención del índice de logs y la disponibilidad de nodos de archivo versus nodos completos.

Semántica de error de la protección de rango de Monad eth_getLogs

Cuando un cliente envía eth_getLogs con un span de fromBlock/toBlock que excede el límite configurado del nodo, Monad devuelve un objeto de error JSON-RPC con un mensaje como 'invalid block range' en lugar de un array de resultados vacío. Esto es una protección, no un escaneo: el nodo valida el span solicitado antes de tocar el índice de logs, por lo que el fallo es determinista para un rango y configuración de endpoint dados. La especificación JSON-RPC 2.0 define esta forma como una respuesta de error con un código numérico y un mensaje, por lo que los clientes bien comportados deben ramificar según el error en lugar de tratarlo como 'no se encontraron logs'.

La distinción importa porque un array vacío es una respuesta válida y exitosa que significa 'este rango no contiene logs coincidentes'. Un error significa 'esta solicitud fue rechazada'. Si tu indexador trata ambos como vacío, omitirá silenciosamente el historial. La sección de Límites de la documentación de Monad es el lugar autorizado para confirmar el límite actual, y señala explícitamente que el límite difiere de la red principal de Ethereum. Para la mecánica general de fragmentación, consulta límites de rango de bloques de eth_getLogs y fragmentación segura.

  • Respuesta de error: el nodo rechazó la solicitud; no se escanearon logs.
  • Array vacío: el nodo escaneó el rango y no encontró coincidencias.
  • Nunca colapses ambos en la misma ruta de código en un indexador.
  • El límite es comportamiento documentado en Monad y no es idéntico a la red principal de Ethereum.

Límites documentados de Monad versus límites prácticos de la red principal de Ethereum

La especificación JSON-RPC para eth_getLogs de Ethereum define el objeto de filtro y su semántica fromBlock/toBlock pero no exige un span máximo; en la práctica, los proveedores públicos de Ethereum imponen sus propios límites, y el techo práctico a menudo está determinado por el tamaño de la respuesta y el tiempo de espera en lugar de un número fijo de bloques. La documentación JSON-RPC de Monad enumera un límite explícito de rango de bloques en su sección de Límites, y los mismos documentos describen cómo el comportamiento de Monad difiere de Ethereum.

Debido a que el límite está documentado pero las implementaciones del proveedor pueden diferir, trata el número como 'documentado / varía según el proveedor'. No codifiques una constante única que copiaste de una publicación de blog. En su lugar, descubre el rango efectivo aceptado contra tu propio endpoint y luego almacénalo en caché. La página Endpoints RPC de Monad (RPC Assistant) es el lugar correcto para confirmar qué endpoint estás consultando realmente antes de medir. Si estás ejecutando contra un endpoint dedicado de Monad mainnet, el límite que observes es el que tu paginador debe respetar.

  • Especificación de Ethereum: semántica de filtro definida; no se exige un span máximo universal.
  • Documentación de Monad: límite explícito de rango de eth_getLogs documentado en Límites.
  • Remedio documentado: paginar el rango y fusionar resultados.
  • Las implementaciones del proveedor pueden diferir; descubre y almacena en caché el límite efectivo.

Por qué la bisección descubre el rango efectivo sin adivinar

En lugar de asumir un límite, comienza desde un span que sabes que es demasiado grande y reduce a la mitad hasta que el nodo acepte la solicitud. Esta es una búsqueda acotada: cada rechazo elimina la mitad del span restante, por lo que el descubrimiento se completa en O(log n) sondas. El primer span aceptado es un límite superior seguro; luego puedes usarlo directamente o reducirlo ligeramente para tener margen. Este método es reproducible contra cualquier endpoint y no depende de constantes no documentadas.

La misma lógica de bisección maneja el caso límite donde un solo bloque excede el límite. Si fromBlock es igual a toBlock y el nodo aún rechaza, el problema no es el ancho del span sino el bloque en sí, típicamente porque el bloque contiene más logs de los que el nodo devolverá en una respuesta. En ese caso debes reducir por dirección o tema, o recurrir a una estrategia basada en trazas o recibos. Registra el resultado para poder distinguir 'rango demasiado amplio' de 'bloque único demasiado pesado'.

async function discoverRange(rpcUrl, probeBlock) {
  let span = 1024;
  while (span >= 1) {
    const fromBlock = '0x' + (probeBlock - span + 1).toString(16);
    const toBlock = '0x' + probeBlock.toString(16);
    const body = {
      jsonrpc: '2.0', id: 1, method: 'eth_getLogs',
      params: [{ fromBlock, toBlock }]
    };
    const res = await fetch(rpcUrl, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(body)
    });
    const json = await res.json();
    if (!json.error) return span;
    if (!/invalid block range/i.test(json.error.message || '')) {
      throw new Error('unexpected error: ' + JSON.stringify(json.error));
    }
    span = Math.floor(span / 2);
  }
  throw new Error('single block rejected; narrow by address or topic');
}

Contigüidad y monotonicidad como pruebas de completitud

La paginación solo es correcta si la unión de páginas es igual al rango solicitado sin huecos ni solapamientos. Haz cumplir esto con dos invariantes. Primero, contigüidad: el fromBlock de la siguiente página debe ser igual al toBlock de la página anterior más uno, es decir, nextFrom = lastTo + 1. Segundo, monotonicidad: dentro de cada página, los valores de blockNumber de los logs deben ser no decrecientes, y entre páginas el primer blockNumber de la página N+1 debe ser mayor o igual que el último blockNumber de la página N. Si cualquiera de los invariantes falla, detente y expón la anomalía en lugar de fusionar silenciosamente.

Estas comprobaciones capturan los modos de fallo que producen un estado de indexador incorrecto: una página perdida tras un error transitorio, un off-by-one en el bucle, o un proveedor que devuelve logs desordenados. También hacen que el paginador sea auditable, porque puedes registrar el rango aceptado y el número de páginas y compararlos con el rango solicitado. La misma disciplina se aplica a los flujos basados en recibos descritos en ciclo de vida de transacciones de Monad y estado de recibo, donde las suposiciones de orden importan igualmente.

  • Contigüidad: nextFrom = lastTo + 1 para cada página después de la primera.
  • Monotonicidad: blockNumber no decreciente dentro y entre páginas.
  • En caso de violación: detente, registra los límites de página y no fusiones.
  • Registra el rango solicitado, el rango aceptado y el número de páginas para auditoría.

Un paginador Node.js ejecutable con reintentos con jitter

El paginador a continuación descubre el rango efectivo y luego recorre la ventana solicitada en páginas del tamaño aceptado. Reintenta fallos transitorios con retroceso exponencial más jitter, pero no reintenta 'invalid block range' a ciegas; en su lugar, reduce a la mitad el tamaño de página y reintenta una vez, lo que converge rápidamente. Asegura contigüidad y monotonicidad antes de devolver, por lo que un retorno exitoso significa que el conjunto fusionado es demostrablemente completo para la ventana solicitada.

Ejecútalo contra tu propio endpoint y captura el rango solicitado, el rango aceptado, el número de páginas y los milisegundos de reloj de pared. Esos cuatro números son la única forma confiable de caracterizar tu endpoint, porque dependen de tu proveedor, la selectividad de tu filtro y las condiciones de red. No sustituyas los números de otra persona por tus propias mediciones.

async function rpc(rpcUrl, method, params, attempt = 0) {
  const res = await fetch(rpcUrl, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: attempt + 1, method, params })
  });
  if (res.status === 429 || res.status >= 500) {
    if (attempt >= 5) throw new Error('retries exhausted: HTTP ' + res.status);
    const base = Math.min(2000, 100 * 2 ** attempt);
    const jitter = Math.floor(Math.random() * 100);
    await new Promise(r => setTimeout(r, base + jitter));
    return rpc(rpcUrl, method, params, attempt + 1);
  }
  return res.json();
}

async function pageLogs(rpcUrl, fromBlock, toBlock, filter = {}) {
  let pageSize = await discoverRange(rpcUrl, toBlock);
  const logs = [];
  let cursor = fromBlock;
  let lastBlock = -1;
  let pages = 0;
  while (cursor <= toBlock) {
    const end = Math.min(cursor + pageSize - 1, toBlock);
    const params = [{
      ...filter,
      fromBlock: '0x' + cursor.toString(16),
      toBlock: '0x' + end.toString(16)
    }];
    const json = await rpc(rpcUrl, 'eth_getLogs', params);
    if (json.error) {
      if (/invalid block range/i.test(json.error.message || '') && pageSize > 1) {
        pageSize = Math.floor(pageSize / 2);
        continue;
      }
      throw new Error('eth_getLogs failed: ' + JSON.stringify(json.error));
    }
    const page = json.result || [];
    for (const log of page) {
      const bn = parseInt(log.blockNumber, 16);
      if (bn < lastBlock) throw new Error('monotonicity violated at block ' + bn);
      lastBlock = bn;
    }
    logs.push(...page);
    pages += 1;
    cursor = end + 1;
  }
  if (cursor !== toBlock + 1) throw new Error('contiguity violated');
  return { logs, pages, pageSize };
}

Tabla de resultados para medición específica del endpoint

Completa esta tabla con tu propio endpoint de Monad. Ejecuta el paginador con un rango solicitado fijo y un filtro fijo, repítelo varias veces y registra el rango aceptado que devolvió el paso de descubrimiento, el número de páginas y los milisegundos de reloj de pared. Debido a que el rango aceptado puede cambiar si el proveedor ajusta los límites, vuelve a medir después de cualquier cambio de endpoint o migración de proveedor.

Trata la tabla como una línea base local, no como una afirmación de benchmark. Si necesitas comparar proveedores, mantén constantes el rango solicitado, el filtro y la máquina cliente, y anota la URL del endpoint y la marca de tiempo para cada fila. La página de precios de RPC te ayuda a mapear el volumen de solicitudes medido con el costo, y la página del servicio API describe el acceso gestionado si prefieres no operar el paginador tú mismo.

  • Rango solicitado: fromBlock..toBlock que pediste.
  • Rango aceptado: span que confirmó el paso de descubrimiento.
  • Número de páginas: número de llamadas exitosas a eth_getLogs.
  • Milisegundos de reloj de pared: tiempo transcurrido para la fusión completa.
  • Endpoint y marca de tiempo: necesarios para que las filas sean comparables.

Solución de problemas persistentes de errores de rango de bloques inválido

Si la bisección llega a un solo bloque y el nodo aún rechaza, el bloque en sí es demasiado pesado para una respuesta. Reduce el filtro por dirección de contrato o topic0, o divide por conjunto de direcciones. Si el error persiste solo para rangos históricos, es posible que estés consultando un nodo completo que no retiene logs antiguos; cambia a un endpoint de archivo como se describe en nodo de archivo de Monad y RPC histórico. Si las solicitudes se cuelgan en lugar de dar error, esa es una clase de fallo diferente cubierta en tiempo de espera de RPC de Monad.

También verifica que no estés mezclando etiquetas de bloque. La descripción general de JSON-RPC de Monad documenta el manejo de etiquetas de bloque y señala diferencias con Ethereum; pasar 'latest' como toBlock mientras paginas una ventana histórica puede producir resultados confusos. Fija tanto fromBlock como toBlock a cantidades hexadecimales al paginar. Finalmente, confirma que el endpoint que crees que estás llamando es el que responde, ya que una URL de RPC obsoleta en la configuración es una causa común de informes de 'funcionó ayer'.

  • Rechazo de bloque único: reduce por dirección o tema.
  • Fallo solo histórico: probablemente un nodo completo sin retención de logs antiguos.
  • Cuelgues en lugar de errores: investiga los tiempos de espera por separado.
  • Fija fromBlock y toBlock a cantidades hexadecimales; evita mezclar etiquetas.
  • Verifica que la URL de RPC configurada coincida con el endpoint que mediste.

Retención del índice de logs y disponibilidad de nodo de archivo versus nodo completo

La disponibilidad de logs está limitada por lo que retiene el nodo. Un nodo completo puede podar o no indexar logs antiguos, por lo que eth_getLogs sobre una ventana histórica puede fallar o devolver datos incompletos incluso cuando el rango está dentro del límite. Un nodo de archivo retiene el estado histórico y los índices de logs, que es lo que necesitas para rellenos y reindexación segura ante reorganizaciones. Esta es una propiedad de retención, no una propiedad de límite de rango, y las dos a menudo se confunden al depurar.

La consecuencia práctica es que la corrección de tu paginador depende de la ventana de retención del endpoint. Si paginas un rango anterior a la retención, puedes obtener un error o un resultado vacío que en realidad no está vacío. Confirma la retención con tu proveedor antes de rellenar, y prefiere endpoints de archivo para cualquier ventana más antigua que la retención documentada del nodo. El artículo nodo de archivo de Monad y RPC histórico cubre las ventajas y desventajas con más detalle.

  • Nodo completo: puede no retener ni indexar logs antiguos.
  • Nodo de archivo: retiene estado histórico e índices de logs.
  • Los límites de retención son independientes del límite de rango de eth_getLogs.
  • Los rellenos deben apuntar a endpoints de archivo por defecto.

Compensaciones entre tamaño de página, latencia y volumen de solicitudes

Páginas más pequeñas reducen la probabilidad de alcanzar la protección de rango y disminuyen el tamaño de respuesta por solicitud, pero aumentan el número de solicitudes y el tiempo total de reloj de pared. Páginas más grandes amortizan la latencia de ida y vuelta pero arriesgan rechazo y payloads más grandes. El óptimo es específico del endpoint, por lo que existen el paso de descubrimiento y la tabla de resultados: te permiten elegir un tamaño de página que se acepte de manera consistente sin fragmentar en exceso.

También hay una dimensión de costo. Cada página es una solicitud facturable en la mayoría de los proveedores, por lo que un paginador que fragmenta en exceso eleva el costo incluso cuando tiene éxito. Equilibra el tamaño de página con el modelo de precios de tu proveedor, y almacena en caché el rango aceptado descubierto para no volver a sondear en cada ejecución. Para contexto de acceso gestionado y precios, consulta precios de RPC y la descripción general del servicio API.

  • Páginas pequeñas: más seguras, más solicitudes, mayor latencia total.
  • Páginas grandes: menos solicitudes, mayor riesgo de rechazo.
  • Almacena en caché el rango aceptado descubierto entre ejecuciones.
  • Ten en cuenta la facturación por solicitud al elegir el tamaño de página.

Próximos pasos para la indexación de logs en producción en Monad

Mueve el paginador a tu indexador detrás de un almacén de puntos de control: persiste el último toBlock completamente fusionado y reanuda desde el punto de control + 1 al reiniciar. Eso hace que el invariante de contigüidad sea duradero a través de reinicios del proceso. Agrega un reescaneo periódico de una pequeña ventana final para manejar reorganizaciones, y alerta si la monotonicidad o la contigüidad fallan alguna vez en producción.

Para la selección de endpoints y la conmutación por error, mantén al menos dos URL de RPC configuradas y verifica cada una con el paso de descubrimiento antes de usarla. La página Endpoints RPC de Monad (RPC Assistant) enumera opciones, y el centro de aprendizaje de OnFinality recopila análisis relacionados. Si también estás siguiendo la semántica del saldo nativo, saldo de reserva de Monad explica el modelo de reserva que afecta las lecturas de eth_getBalance y reserveBalance.

  • Persiste un punto de control del último bloque completamente fusionado.
  • Reescanea una ventana final para seguridad ante reorganizaciones.
  • Alerta sobre violaciones de contigüidad o monotonicidad.
  • Valida cada endpoint configurado con el paso de descubrimiento.

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