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

Filtros de suscripción WebSocket de Solana: por qué los filtros pierden eventos

Cómo se evalúan los filtros de suscripción de Solana, por qué un filtro bien formado puede no coincidir con nada silenciosamente y cómo verificar que una suscripción entrega lo que pretendías.

TL;DR

Los filtros de suscripción de Solana se evalúan del lado del servidor contra los bytes crudos de la cuenta o el texto del log, y un filtro sintácticamente válido puede no coincidir con nada sin producir ningún error. Solo programSubscribe acepta un arreglo de filtros compuesto por entradas memcmp y datasize; accountSubscribe toma una única pubkey de cuenta y logsSubscribe toma la cadena all o un arreglo mentions, por lo que un arreglo de filtros de getProgramAccounts copiado a una suscripción es rechazado o mal aplicado. memcmp compara bytes crudos en un offset nombrado contra un valor en base58 o bytes, lo que significa que el offset ya debe tener en cuenta cualquier discriminador o encabezado que el programa escriba antes del campo, y un discriminador Anchor desfasado por ocho bytes es el clásico fallo silencioso. datasize fija la longitud de los datos de la cuenta y se rompe cuando un programa cambia su diseño, y como el arreglo de filtros es una conjunción, una sola entrada obsoleta puede anular toda la suscripción. La única verificación confiable es reconciliar una suscripción filtrada contra una suscripción sin filtro o una lectura HTTP getProgramAccounts con el mismo filtro.

Vocabulario de filtros de suscripción entre los métodos WebSocket de Solana

Solana expone tres suscripciones WebSocket orientadas a cuentas y logs, y cada una acepta una forma de filtro diferente. La documentación de programSubscribe de Solana define un objeto de filtro con entradas memcmp y datasize, mientras que la documentación de logsSubscribe define la cadena literal all o un objeto con un arreglo mentions. accountSubscribe es la excepción: toma una pubkey de cuenta sin filtro alguno, así que no hay nada que configurar mal ni nada que acotar.

Esta asimetría es la primera fuente de fallo silencioso. Los desarrolladores que ya usan la API de filtros HTTP getProgramAccounts asumen naturalmente que el mismo arreglo funciona en todas partes, pero los métodos de suscripción no comparten ese vocabulario. Si todavía estás mapeando la capa de conexión, la guía Métodos WebSocket de RPC de Solana y ciclo de vida de la conexión cubre cómo se abren, confirman y cierran las suscripciones, y la página API WebSocket de Solana (RPC Assistant) es la forma más rápida de inspeccionar la firma de un método en vivo antes de comprometerte con una forma de filtro.

La regla práctica es elegir el método que coincida con la granularidad que realmente necesitas. Si quieres todas las cuentas propiedad de un programa, programSubscribe es el único método con un filtro compuesto. Si quieres una sola cuenta, accountSubscribe es correcto y filtrar es innecesario. Si quieres observar la actividad del programa en lugar del estado de las cuentas, logsSubscribe con mentions es la herramienta adecuada, y la guía Análisis de payloads de notificación de logsSubscribe cubre qué llega en el cuerpo de la notificación.

  • programSubscribe: objeto de filtro con entradas memcmp y datasize, evaluado contra los bytes de datos de la cuenta.
  • logsSubscribe: la cadena all, o { mentions: [pubkey] }, evaluado contra el texto del log.
  • accountSubscribe: una única pubkey de cuenta, sin campo de filtro, sin posibilidad de acotar.
  • Copiar un arreglo de filtros de getProgramAccounts a accountSubscribe o logsSubscribe es rechazado o ignorado según el proveedor.

Semántica de memcmp: offset de bytes, codificación y la trampa del discriminador Anchor

Un filtro memcmp nombra un offset de bytes dentro de los datos de la cuenta y un valor codificado en base58 o como arreglo de bytes, y coincide solo cuando los bytes crudos en ese offset son iguales al valor proporcionado. La comparación es byte a byte y sensible a mayúsculas; no hay normalización, ni alineación, ni conocimiento del diseño de campos de tu programa. El offset es una posición absoluta en el búfer de datos de la cuenta, no un índice de campo.

Esa regla de offset absoluto es donde se originan la mayoría de los fallos silenciosos. Los programas basados en Anchor escriben un discriminador de ocho bytes antes del cuerpo de la cuenta, por lo que un campo que aparece primero en la estructura Rust en realidad comienza en el offset 8 de los datos serializados. Un filtro creado en el offset 0 contra ese campo comparará los bytes del discriminador y no coincidirá con nada, para siempre, sin error. La misma clase de error aparece con cualquier encabezado, byte de versión o prefijo de longitud escrito a mano que el programa escriba antes del campo que te interesa.

La codificación es la segunda trampa. Un campo de pubkey almacenado como 32 bytes crudos no coincidirá con una cadena base58 que decodifica a la misma pubkey si pasas la cadena donde se esperan bytes, y un campo numérico almacenado en little-endian no coincidirá con un arreglo de bytes big-endian. Deriva siempre el valor del filtro desde la misma ruta de serialización que usa tu programa, y confirma el offset leyendo una cuenta conocida con getAccountInfo e inspeccionando los datos crudos en base64 antes de suscribirte.

  • El offset es absoluto en el búfer de datos de la cuenta, no un índice de campo.
  • Los programas Anchor colocan un discriminador de ocho bytes antes del cuerpo; los offsets de campo comienzan en 8.
  • memcmp es byte a byte y sensible a mayúsculas; las codificaciones base58 y arreglo de bytes no son intercambiables.
  • Verifica el offset contra los datos crudos de una cuenta real antes de abrir la suscripción.

Semántica de datasize y fragilidad del diseño

Un filtro datasize es una coincidencia de longitud sobre los datos de la cuenta. Es útil para excluir cuentas cerradas, que aún pueden observarse brevemente, y para separar cuentas que comparten propietario pero difieren en forma. También es el filtro más frágil del vocabulario, porque codifica una suposición sobre el diseño serializado de tu programa que cambia en el momento en que agregas un campo, reordenas una estructura o incrementas una versión.

El modo de fallo es silencioso. Cuando un programa lanza un diseño versión 3 con una cuenta más grande, un filtro datasize fijado a la longitud de la versión 2 deja de coincidir con las cuentas nuevas mientras sigue coincidiendo con cualquier cuenta heredada que aún exista. Ves un flujo parcial, no un error, y las cuentas faltantes son exactamente las que más probablemente querías. Trata datasize como un marcador de versión que debes actualizar al mismo ritmo que los despliegues del programa, y prefíerelo como filtro secundario en lugar del selector principal.

Como el arreglo de filtros es una conjunción, datasize interactúa mal con memcmp cuando los diseños divergen. Un memcmp sobre un campo indexado por propietario combinado con un datasize sobre un diseño versión 2 no produce nada una vez que el programa lanza la versión 3, aunque cada filtro sea individualmente bien formado. Si necesitas rastrear múltiples diseños durante una migración, ejecuta suscripciones separadas por diseño en lugar de intentar expresar la unión en un solo arreglo de filtros.

  • datasize coincide con la longitud total de los datos de la cuenta, no con la longitud de un campo.
  • Deja de coincidir silenciosamente cuando un programa cambia el diseño de su cuenta.
  • Combinar datasize con memcmp durante una migración de diseño puede anular toda la suscripción.
  • Ejecuta una suscripción por diseño durante las migraciones en lugar de codificar una unión.

Semántica de conjunción y la ambigüedad de la suscripción inactiva

Cada entrada del arreglo de filtros de programSubscribe debe coincidir para que se entregue una notificación. No hay OR, ni negación, ni crédito parcial. Esto es sencillo cuando creas el arreglo deliberadamente, pero se convierte en un peligro cuando los filtros se ensamblan desde diferentes fuentes, como un memcmp copiado de un indexador y un datasize copiado de un script de migración. Una sola entrada obsoleta basta para suprimir todas las notificaciones.

El problema más profundo es que una suscripción que no coincide con nada es indistinguible de una suscripción correcta pero inactiva. El nodo no envía error, ni advertencia, ni latido periódico vinculado a la evaluación del filtro. Una suscripción silenciosamente rota se ve exactamente igual que una suscripción saludable sobre un programa tranquilo. Por eso la corrección del filtro no puede validarse observando solo la suscripción; debe validarse contra una lectura independiente.

La verificación confiable es la reconciliación. Abre un programSubscribe sin filtro junto al filtrado, o emite una lectura HTTP getProgramAccounts con el mismo filtro, y compara los conteos durante la misma ventana. Si la suscripción filtrada devuelve cero mientras la sin filtro devuelve cuentas que satisfacen tu predicado previsto, el filtro está mal. La guía Filtros y paginación de getProgramAccounts cubre el lado HTTP de esa comparación, y la guía Construcción de un indexador de conjunto de cuentas reanudable cubre cómo mantener ambas vistas consistentes a lo largo del tiempo.

  • El arreglo de filtros es un AND lógico; cada entrada debe coincidir.
  • No se emite ningún error cuando un filtro no coincide con nada.
  • Una suscripción rota y una suscripción inactiva son observacionalmente idénticas.
  • Reconcilia contra una suscripción sin filtro o una lectura HTTP getProgramAccounts.

Ejemplo ejecutable: contar notificaciones filtradas versus sin filtro

El siguiente script de Node.js abre dos suscripciones programSubscribe contra el mismo programa, una sin filtro y otra con un filtro memcmp más datasize, cuenta las notificaciones durante una ventana fija e informa la discrepancia. Usa el paquete estándar ws y el sobre de suscripción JSON-RPC 2.0 público. Reemplaza el id del programa, el offset de memcmp y el valor del filtro con valores derivados del diseño serializado de tu propio programa.

Ejecútalo contra un programa que sepas que está activo. Si la suscripción sin filtro recibe notificaciones y la filtrada no recibe ninguna, tu filtro está mal; si ambas no reciben nada, el programa simplemente está inactivo en esa ventana y deberías extender la ventana o elegir un programa más ocupado. Este es el método de medición, no un benchmark: los números que registres son específicos de tu endpoint, tu programa y tu ventana de observación.

const WebSocket = require('ws');

const WS_URL = process.env.SOLANA_WS_URL || 'wss://api.mainnet-beta.solana.com';
const PROGRAM_ID = process.env.PROGRAM_ID;
const WINDOW_MS = 60000;

// Derive these from your program's serialized layout.
// Anchor programs write an 8-byte discriminator before the body.
const MEMCMP_OFFSET = 8;
const MEMCMP_BYTES = Buffer.from(process.env.MEMCMP_BASE58 || '', 'base64');
const DATASIZE = Number(process.env.DATASIZE || 165);

function subscribe(label, filter, counter) {
  const ws = new WebSocket(WS_URL);
  ws.on('open', () => {
    ws.send(JSON.stringify({
      jsonrpc: '2.0',
      id: label,
      method: 'programSubscribe',
      params: [PROGRAM_ID, filter ? { encoding: 'base64', filters: filter } : { encoding: 'base64' }]
    }));
  });
  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    if (msg.method === 'programNotification') counter.count += 1;
    if (msg.error) console.error(label, 'error', msg.error);
  });
  ws.on('error', (err) => console.error(label, 'socket error', err.message));
  return ws;
}

const unfiltered = { count: 0 };
const filtered = { count: 0 };

const wsA = subscribe('unfiltered', null, unfiltered);
const wsB = subscribe('filtered', [
  { memcmp: { offset: MEMCMP_OFFSET, bytes: MEMCMP_BYTES.toString('base64') } },
  { dataSize: DATASIZE }
], filtered);

setTimeout(() => {
  console.log('window_ms', WINDOW_MS);
  console.log('unfiltered_notifications', unfiltered.count);
  console.log('filtered_notifications', filtered.count);
  console.log('mismatch', unfiltered.count > 0 && filtered.count === 0);
  wsA.close();
  wsB.close();
  process.exit(0);
}, WINDOW_MS);

Tabla de resultados: tipo de filtro, con qué coincide y el fallo silencioso

Usa la siguiente tabla como hoja de trabajo. Llena la columna derecha con lo que realmente observas en tu propio endpoint y programa, luego compáralo con el comportamiento documentado. El punto no es confiar en un número publicado sino reproducir la comparación tú mismo, porque la corrección del filtro depende enteramente del diseño serializado de tu programa.

Registra la ventana de observación, el id del programa, el arreglo de filtros exacto que enviaste y los conteos de ambas suscripciones. Si el conteo filtrado es cero mientras el conteo sin filtro es distinto de cero, el fallo está en el filtro, no en la red. Si ambos son cero, extiende la ventana antes de sacar cualquier conclusión.

  • Tipo de filtro: memcmp | Con qué coincide: bytes crudos en un offset nombrado iguales al valor proporcionado | Fallo silencioso común: el offset no tiene en cuenta un discriminador u encabezado de 8 bytes | Tu observación: ____
  • Tipo de filtro: datasize | Con qué coincide: la longitud total de los datos de la cuenta es igual al entero proporcionado | Fallo silencioso común: el programa cambió el diseño y la longitud fijada está obsoleta | Tu observación: ____
  • Tipo de filtro: memcmp + datasize | Con qué coincide: ambas condiciones son verdaderas simultáneamente | Fallo silencioso común: una entrada obsoleta suprime toda la suscripción | Tu observación: ____
  • Tipo de filtro: logsSubscribe mentions | Con qué coincide: líneas de log que mencionan la pubkey | Fallo silencioso común: esperar semántica de estado de cuenta de un filtro de logs | Tu observación: ____
  • Tipo de filtro: accountSubscribe | Con qué coincide: una única pubkey de cuenta, sin filtro | Fallo silencioso común: pasar un arreglo de filtros que el método no acepta | Tu observación: ____

Solución de problemas: deriva de offset, codificación, mayúsculas y commitment

La deriva de offset tras una actualización del programa es la causa más común de una suscripción que antes funcionaba y ahora no devuelve nada. Cuando un programa agrega un campo antes del que filtras, todos los offsets posteriores se desplazan. Vuelve a derivar el offset desde el diseño serializado actual y vuelve a ejecutar el script de reconciliación. Si mantienes un indexador, la guía Construcción de un indexador de conjunto de cuentas reanudable cubre cómo detectar y recuperarte de esta clase de deriva sin perder eventos.

Las discrepancias de codificación son la segunda causa más común. Un valor memcmp proporcionado en base58 cuando el método espera bytes, o viceversa, comparará la secuencia de bytes equivocada y no coincidirá con nada. Confirma la codificación que documenta tu proveedor y deriva el valor desde la misma ruta de serialización que usa tu programa. La sensibilidad a mayúsculas se deriva del mismo principio: base58 distingue mayúsculas y minúsculas, y no hay coincidencia insensible a mayúsculas en ningún lugar del vocabulario de filtros.

El nivel de commitment afecta lo que observas, no con qué coincide el filtro. Una suscripción en commitment processed puede entregar notificaciones de cuentas que luego se revierten, mientras que el commitment confirmed o finalized entrega menos notificaciones y más tardías. Si tus suscripciones filtrada y sin filtro usan niveles de commitment diferentes, tus conteos de reconciliación discreparán por razones ajenas al filtro. Mantén el commitment constante en ambos lados de la comparación. El comportamiento específico de cada proveedor en torno a los valores predeterminados de commitment y los tiempos de notificación está documentado por proveedor y varía, así que consulta la documentación de tu endpoint en lugar de asumir paridad.

  • Vuelve a derivar los offsets de memcmp después de cada actualización del programa que cambie el diseño.
  • Confirma la codificación base58 versus bytes contra la firma del método documentada por tu proveedor.
  • No hay coincidencia insensible a mayúsculas en memcmp; base58 distingue mayúsculas y minúsculas.
  • Mantén el nivel de commitment constante entre suscripciones filtradas y sin filtro.
  • Los valores predeterminados de commitment y los tiempos de notificación varían por proveedor; consulta la documentación del endpoint.

Verificar una suscripción con una comprobación cruzada HTTP

Una segunda ruta de verificación usa HTTP en lugar de un segundo WebSocket. Emite una llamada getProgramAccounts con el mismo arreglo de filtros que pasaste a programSubscribe, y compara el conjunto de cuentas devuelto con las notificaciones que observaste. La llamada HTTP es una lectura puntual, así que no coincidirá exactamente con un conteo en streaming, pero te dirá de inmediato si el filtro coincide con alguna cuenta.

Esta comprobación cruzada es barata y detecta la clase de error más costosa: un filtro sintácticamente válido y semánticamente vacío. La guía Filtros y paginación de getProgramAccounts cubre la API de filtros HTTP en detalle, incluida la forma en que la paginación interactúa con conjuntos de resultados grandes. Si la llamada HTTP devuelve cuentas y la suscripción no devuelve nada, el problema está en los parámetros de la suscripción, no en el predicado del filtro.

curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getProgramAccounts",
  "params": [
    "YOUR_PROGRAM_ID",
    {
      "encoding": "base64",
      "filters": [
        { "memcmp": { "offset": 8, "bytes": "YOUR_BASE58_OR_BASE64_VALUE" } },
        { "dataSize": 165 }
      ]
    }
  ]
}'

Limitaciones y compensaciones del filtrado de suscripciones del lado del servidor

El filtrado del lado del servidor reduce el ancho de banda y el trabajo del cliente, pero traslada la corrección a un lugar que no puedes observar. El nodo evalúa tu filtro contra bytes crudos y no devuelve nada cuando no coincide, así que cada error de filtro se convierte en un vacío de datos silencioso en lugar de un error visible. Esa compensación es aceptable para trabajo exploratorio y peligrosa para cualquier cosa que deba ser completa.

Los filtros tampoco pueden expresar uniones, negaciones ni predicados sobre campos decodificados. Si necesitas cuentas que coincidan con uno de varios diseños, o cuentas cuyo campo de propietario decodificado esté en un conjunto, debes ejecutar múltiples suscripciones o suscribirte sin filtro y filtrar del lado del cliente. El filtrado del lado del cliente cuesta ancho de banda pero te da visibilidad: puedes registrar lo que fue rechazado y detectar la deriva de inmediato.

Por último, la semántica de los filtros está ligada al diseño serializado, que es un detalle de implementación de tu programa más que una interfaz estable. Cualquier filtro que crees está acoplado a una versión específica del programa. Trata las definiciones de filtros como artefactos versionados que se publican junto con los despliegues del programa, y vuelve a verificarlas con el método de reconciliación cada vez que cambie el diseño. Para la selección de endpoints y la planificación de capacidad en torno a suscripciones de alto volumen, consulta Precios de RPC y la descripción general del servicio de API, y explora la página de la red Solana para opciones de endpoint.

  • El filtrado del lado del servidor oculta la corrección; un error de filtro es un vacío de datos silencioso.
  • No se pueden expresar uniones, negaciones ni predicados sobre campos decodificados en el arreglo de filtros.
  • El filtrado del lado del cliente cuesta ancho de banda pero hace observables los rechazos.
  • Las definiciones de filtros están acopladas a una versión específica del programa y deben versionarse.

Próximos pasos: instrumentar suscripciones para verificación continua

La solución duradera es hacer que la verificación sea continua en lugar de una comprobación única. Ejecuta una suscripción sin filtro de baja frecuencia o una lectura getProgramAccounts periódica junto a tu suscripción filtrada, y alerta cuando el flujo filtrado se quede en silencio mientras la referencia sin filtro esté activa. Esto convierte un fallo silencioso en uno detectable.

Combínalo con una pequeña cantidad de validación del lado del cliente: decodifica las cuentas que sí recibes y afirma que satisfacen el predicado que pretendías, no solo el predicado que codificaste. Si una cuenta recibida no cumple el predicado previsto, tu filtro es demasiado laxo; si el flujo de referencia muestra cuentas que tu filtro debería haber coincidido pero no llegaron, tu filtro es demasiado estricto. Ambas direcciones son informativas.

Para equipos que construyen sobre infraestructura gestionada, la página API WebSocket de Solana (RPC Assistant) documenta la superficie de métodos, y el centro de aprendizaje de OnFinality reúne las guías relacionadas sobre ciclo de vida de la conexión, análisis de logs y construcción de indexadores. Comienza desde la página de la red Solana para elegir un endpoint, luego integra el script de reconciliación en tu pipeline de despliegue para que la deriva de filtros se detecte en el momento del lanzamiento en lugar de en producción.

  • Ejecuta un flujo de referencia y alerta cuando el flujo filtrado se quede en silencio.
  • Decodifica las cuentas recibidas y afirma el predicado previsto, no solo el codificado.
  • Trata las definiciones de filtros como artefactos versionados verificados en el momento del lanzamiento.
  • Revisa las guías de ciclo de vida de la conexión y análisis de logs antes de escalar el número de suscripciones.

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