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

Escaneo de logs de BNB Smart Chain a escala: límites de rango de eth_getLogs y paginación segura

Aprende a paginar eth_getLogs en BNB Smart Chain sin chocar con los límites de rango, los timeouts ni los vacíos silenciosos, para que tu indexador construya un historial completo de transferencias ERC-20.

TL;DR

eth_getLogs en BNB Smart Chain está limitado por los topes de rango de bloques y los timeouts impuestos por el proveedor, por lo que escanear un historial grande requiere paginar por una ventana fija, persistir un cursor y deduplicar por (transactionHash, logIndex). El nodo debe escanear cada bloque del rango solicitado, así que los rangos amplios cuestan un trabajo y una memoria de O(bloques) y a menudo fallan con un error de 'block range too large' o un timeout. Descubre el tope real de tu endpoint de forma empírica, aplica backoff con jitter ante 429/timeout y vuelve a escanear una cola de confirmación para reparar los logs reorganizados. Para uso continuo en tiempo real, usa suscripciones o una API indexada; para escaneos enormes de historial completo, usa un endpoint de archivo o un indexador basado en rangos.

Por qué eth_getLogs no es una consulta de historial completo

El método eth_getLogs acepta fromBlock, toBlock, address[] y topics[][], y devuelve los logs coincidentes. Según la especificación de execution-apis de Ethereum, el nodo debe escanear cada bloque del rango solicitado y deserializar los logs que coincidan. Eso significa que un rango amplio cuesta un trabajo y una memoria de O(bloques), no de O(resultados).

BNB Smart Chain tiene una cantidad de bloques muy grande porque produce bloques cada pocos segundos durante años, y soporta un alto volumen de logs por la actividad ERC-20. Por lo tanto, una consulta ingenua con fromBlock: 0, toBlock: 'latest' es costosa y probablemente sea rechazada. La mayoría de los proveedores devuelven un error explícito como 'block range too large' o un mensaje de límite excedido, y históricamente se ha documentado un límite específico en torno a unos pocos miles de bloques en el issue #113 de bnb-chain/bsc en GitHub. Algunos endpoints truncan silenciosamente en lugar de dar error, lo cual es peor porque produce un escaneo irregular que parece correcto.

  • Los rangos amplios cuestan un trabajo y una memoria de O(bloques), no de O(resultados).
  • Los topes del proveedor están documentados / varían según el proveedor; nunca asumas un número universal.
  • El truncamiento silencioso es el modo de fallo más peligroso porque parece un éxito.

Cómo se manifiestan los topes de rango: error, timeout o truncamiento

Cuando un rango es demasiado amplio, el endpoint puede devolver un error JSON-RPC con un mensaje como 'block range too large' o 'limit exceeded'. También puede agotar el tiempo de espera en la capa HTTP, devolviendo un 504 o un reinicio de conexión. Una tercera posibilidad es el truncamiento silencioso: el nodo devuelve un subconjunto de logs sin error, por lo que tu indexador registra un historial incompleto.

Como estos comportamientos varían según el proveedor y el nivel del endpoint, debes tratar el tope como una propiedad empírica del endpoint que estás usando. La página Confiabilidad y timeouts de RPC de BNB Smart Chain cubre cómo interactúan los timeouts y los límites de tasa con los reintentos, y la página Endpoints RPC de BNB Smart Chain (RPC Assistant) enumera endpoints contra los que puedes probar.

  • Error explícito: 'block range too large' o límite excedido.
  • Timeout: HTTP 504 o reinicio de conexión.
  • Truncamiento silencioso: menos logs de los esperados sin error.

La estrategia correcta: paginación de ventana fija con un cursor persistido

Pagina por rango de bloques usando una ventana fija, por ejemplo de 500 a 2000 bloques, ajustada a tu endpoint. Establece fromBlock = lastScanned + 1 y toBlock = min(lastScanned + window, latest). Tras una página exitosa, avanza lastScanned hasta toBlock y persístelo. Al reiniciar, reanuda desde el cursor persistido para que nunca vuelvas a escanear ni te saltes bloques.

Persiste el cursor en almacenamiento duradero, no en memoria. Si procesas los logs antes de persistir el cursor, puedes reprocesarlos al reiniciar; si persistes antes de procesar, puedes saltártelos. El patrón seguro es escribir los logs y el cursor en la misma transacción, o hacer que las escrituras de logs sean idempotentes mediante deduplicación por clave compuesta.

  • El tamaño de la ventana es un parámetro ajustable, no una constante.
  • Avanza solo tras una página completamente exitosa.
  • Persiste el cursor de forma duradera y haz que las escrituras sean idempotentes.

Descubrir el tope real de tu endpoint de forma empírica

No confíes en el número de un artículo de blog. Haz una búsqueda binaria del tamaño de ventana que tiene éxito sin error ni timeout en tu endpoint. Empieza con una ventana pequeña que funcione, duplícala hasta que falle y luego acota entre el último éxito y el primer fallo. Registra el resultado en un archivo de configuración y vuelve a probar cuando cambies de proveedor o de nivel.

Cuando recibas un 429 o un timeout, aplica backoff exponencial con jitter. Un calendario simple es 250 ms, 500 ms, 1 s, 2 s, 4 s, con un tope de 30 s, con un jitter aleatorio de hasta 250 ms. Esto evita reintentos en estampida y es coherente con la guía de la página Confiabilidad y timeouts de RPC de BNB Smart Chain.

  • Búsqueda binaria de la ventana: duplica hasta el fallo y luego acota.
  • Registra el tope descubierto por endpoint y por nivel.
  • Aplica backoff ante 429/timeout con backoff exponencial más jitter.

Acotar la consulta: address y topics

Acota siempre por address (el contrato del token) y topics (la firma del evento Transfer y los from/to indexados). La firma del evento Transfer es 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. Filtrar por topic reduce los datos que el nodo debe deserializar y devolver, lo que disminuye la probabilidad de un timeout y reduce el ancho de banda.

La mecánica neutral respecto a la cadena de los topics y la semántica de filtros se cubre en Filtrar logs de eventos con eth_getLogs y topics. Usa esa página para la gramática de filtros; usa esta página para la estrategia de escaneo específica de BNB.

  • Filtra por la dirección del contrato del token.
  • Filtra por la firma del evento Transfer y los from/to indexados.
  • Los filtros más estrechos reducen el riesgo de timeout y el ancho de banda.

Ordenación, deduplicación y reparación de reorganizaciones

Ordena los resultados por (blockNumber, logIndex) y deduplica por la clave compuesta (transactionHash, logIndex). Una reorganización puede reemitir o mover logs, por lo que la misma transferencia lógica puede aparecer con un número de bloque o un índice de log diferente. La deduplicación por clave compuesta evita el doble conteo y permite que las reemisiones legítimas reemplacen entradas obsoletas.

Vuelve a escanear una cola de confirmación en cada pasada, por ejemplo los últimos N bloques, para reparar los logs reorganizados. Nunca trates un log de un bloque de la punta como definitivo. El tamaño de N depende de tu tolerancia al riesgo y de la profundidad de reorganización de la cadena; un punto de partida común es de 12 a 64 bloques, pero deberías medirlo para tu caso de uso.

  • Ordena por (blockNumber, logIndex).
  • Deduplica por (transactionHash, logIndex).
  • Vuelve a escanear una cola de confirmación en cada pasada; nunca finalices los logs de la punta.

Node.js ejecutable: escaneo paginado de transferencias con backoff y cursor

El siguiente script pagina los logs Transfer de un token en un rango grande con una ventana configurable, backoff exponencial ante 429/timeout, un cursor persistido y deduplicación por clave compuesta. Imprime el progreso y una tabla de resultados que puedes completar para tu endpoint. Reemplaza la URL de RPC y la dirección del token por las tuyas.

Ejecútalo con Node.js 18+ (fetch global). El cursor se almacena en un archivo JSON por simplicidad; en producción usa una transacción de base de datos.

// scan_transfers.js
// Usage: node scan_transfers.js
// Requires Node.js 18+ (global fetch)

const fs = require('fs');

const RPC_URL = process.env.RPC_URL || 'https://your-bnb-endpoint';
const TOKEN = process.env.TOKEN || '0x...';
const WINDOW = Number(process.env.WINDOW || 1000);
const CONFIRMATION_TAIL = Number(process.env.CONFIRMATION_TAIL || 32);
const CURSOR_FILE = './cursor.json';
const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';

function loadCursor() {
  if (fs.existsSync(CURSOR_FILE)) return JSON.parse(fs.readFileSync(CURSOR_FILE));
  return { lastScanned: 0, seen: {} };
}

function saveCursor(c) {
  fs.writeFileSync(CURSOR_FILE, JSON.stringify(c));
}

async function rpc(method, params, attempt = 0) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  if (res.status === 429 || res.status === 504) {
    const delay = Math.min(30000, 250 * 2 ** attempt) + Math.random() * 250;
    console.warn(`retry ${attempt + 1} after ${Math.round(delay)}ms (status ${res.status})`);
    await new Promise(r => setTimeout(r, delay));
    return rpc(method, params, attempt + 1);
  }
  const json = await res.json();
  if (json.error) {
    const msg = JSON.stringify(json.error);
    if (/range|limit|too large/i.test(msg) && attempt < 6) {
      const delay = Math.min(30000, 250 * 2 ** attempt) + Math.random() * 250;
      console.warn(`range error, backing off ${Math.round(delay)}ms`);
      await new Promise(r => setTimeout(r, delay));
      return rpc(method, params, attempt + 1);
    }
    throw new Error(msg);
  }
  return json.result;
}

async function latestBlock() {
  const hex = await rpc('eth_blockNumber', []);
  return parseInt(hex, 16);
}

async function getLogs(from, to) {
  return rpc('eth_getLogs', [{
    fromBlock: '0x' + from.toString(16),
    toBlock: '0x' + to.toString(16),
    address: TOKEN,
    topics: [TRANSFER_TOPIC]
  }]);
}

async function main() {
  const cursor = loadCursor();
  const latest = await latestBlock();
  let scanned = 0, found = 0, retries = 0;
  let from = cursor.lastScanned + 1;
  while (from <= latest) {
    const to = Math.min(from + WINDOW - 1, latest);
    let logs;
    try {
      logs = await getLogs(from, to);
    } catch (e) {
      console.error(`page ${from}-${to} failed: ${e.message}`);
      break;
    }
    for (const log of logs) {
      const key = `${log.transactionHash}:${log.logIndex}`;
      if (!cursor.seen[key]) {
        cursor.seen[key] = true;
        found++;
      }
    }
    cursor.lastScanned = to;
    saveCursor(cursor);
    scanned += to - from + 1;
    console.log(`scanned ${from}-${to} | logs ${logs.length} | total ${found}`);
    from = to + 1;
  }
  // Re-scan confirmation tail to repair reorgs
  const tailFrom = Math.max(0, cursor.lastScanned - CONFIRMATION_TAIL + 1);
  const tailLogs = await getLogs(tailFrom, cursor.lastScanned);
  console.log(`tail re-scan ${tailFrom}-${cursor.lastScanned} | logs ${tailLogs.length}`);
  console.log('\nResults Table (fill in for your endpoint):');
  console.log('| blocks scanned | logs found | window size | retries |');
  console.log(`| ${scanned} | ${found} | ${WINDOW} | ${retries} |`);
}

main().catch(e => { console.error(e); process.exit(1); });

Tabla de resultados: mide contra tu propio endpoint

Usa la tabla de abajo para registrar lo que tu endpoint hace realmente. Ejecuta el script con diferentes valores de WINDOW y anota dónde empiezan los errores o los timeouts. Esta es la única forma fiable de conocer tu tope.

Registra la URL del endpoint, el tamaño de ventana, si la página tuvo éxito, el número de logs devueltos y el conteo de reintentos. Repite con al menos tres tamaños de ventana para encontrar el límite.

  • | Endpoint | Ventana | ¿Éxito? | Logs | Reintentos |
  • |----------|--------|----------|------|---------|
  • | https://... | 500 | sí | ... | 0 |
  • | https://... | 1000 | sí | ... | 0 |
  • | https://... | 2000 | no (rango demasiado grande) | 0 | 2 |

Cuándo la paginación de eth_getLogs es la herramienta equivocada

Para uso continuo en tiempo real, usa suscripciones por WebSocket o la API indexada de un proveedor. La página Endpoints RPC de BNB Smart Chain (RPC Assistant) y la página Servicio de API describen las opciones. Para escaneos enormes de historial completo, considera un endpoint de archivo más un indexador basado en rangos o de terceros.

Se requiere acceso de archivo para los logs antiguos en un nodo completo podado. La página RPC histórico y datos de archivo de BNB Smart Chain explica los requisitos de archivo. Si estás escaneando otra cadena, la página Consultar datos históricos de Solana por RPC muestra un enfoque comparable.

  • Tiempo real: usa suscripciones o una API indexada.
  • Historial completo: usa un endpoint de archivo o un indexador basado en rangos.
  • Los nodos podados no pueden servir logs antiguos.

Fallos comunes y una lista de verificación para la solución de problemas

Los fallos más comunes son 'range too large', timeout de la solicitud, límite de tasa 429, páginas vacías que parecen correctas pero son truncamiento, logs duplicados o faltantes tras una reorganización, y consultar un nodo completo podado para logs antiguos. Cada uno tiene una solución distinta.

Revisa la lista de verificación de abajo antes de cambiar tu código. La mayoría de los problemas son de endpoint o de tamaño de ventana, no errores de lógica.

  • Rango demasiado grande: reduce el tamaño de ventana y vuelve a probar.
  • Timeout: reduce la ventana, añade backoff o usa un endpoint dedicado.
  • 429: aplica backoff con jitter y reduce la concurrencia.
  • Página vacía: verifica contra un bloque conocido con una transferencia conocida.
  • Duplicados/faltantes tras reorganización: vuelve a escanear la cola de confirmación y deduplica.
  • Faltan logs antiguos: cambia a un endpoint de archivo.

Limitaciones, compensaciones y próximos pasos

Paginar por rango de bloques es simple y portátil, pero es más lento que una API indexada y requiere que gestiones un cursor y la reparación de reorganizaciones. El tamaño de ventana es una compensación entre el número de solicitudes y el riesgo de fallo. El almacenamiento de deduplicación crece con el tamaño del historial, así que planifica la poda o un índice de base de datos.

A continuación, revisa la página Confiabilidad y timeouts de RPC de BNB Smart Chain para patrones de reintento, la página RPC histórico y datos de archivo de BNB Smart Chain para el acceso de archivo, y el centro de aprendizaje de OnFinality para guías relacionadas. Para opciones de endpoints y precios, consulta Precios de RPC y la página de la red BNB Smart Chain.

  • La paginación es portátil pero más lenta que las API indexadas.
  • El tamaño de ventana compensa el número de solicitudes con el riesgo de fallo.
  • El almacenamiento de deduplicación crece; planifica la poda o la indexació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