El sendTransaction de Solana ejecuta una ruta de dos fases: el nodo primero simula la transacción contra un bank seleccionado por preflightCommitment, y luego la reenvía al clúster. Los fallos de preflight devuelven códigos de error JSON-RPC como -32002 (simulación fallida), -32003 (fallo de verificación de firma) y -32005 (el nodo está retrasado), que deben ramificarse por separado de los fallos de transporte. Debido a que la firma de una transacción se deriva de su mensaje, reenviar la transacción firmada byte a byte idéntica es idempotente: el clúster deduplica por firma en lugar de ejecutarla dos veces. Una rutina de envío correcta vuelve a consultar getSignatureStatuses antes de cada reenvío, se detiene ante un estado terminal y vuelve a firmar con un blockhash nuevo una vez que el original expira. Este artículo proporciona el contrato de parámetros, una taxonomía de errores, un bucle de reintento ejecutable en Node.js y una tabla de resultados para medir contra tu propio endpoint.
La ruta de dos fases de sendTransaction y preflightCommitment
La documentación de JSON-RPC de Solana para sendTransaction describe una ruta de dos fases. En la fase uno, el nodo receptor ejecuta una simulación de preflight de la transacción contra un bank; en la fase dos, solo si el preflight tiene éxito, el nodo reenvía la transacción al clúster para que la procese el líder. El parámetro preflightCommitment selecciona contra qué bank se ejecuta la simulación, por lo que una transacción simulada en processed puede pasar localmente mientras falla contra el bank confirmed o finalized que el clúster realmente usa.
Esta distinción es la raíz de un error de producción común: un llamador establece preflightCommitment: "processed" para reducir la latencia, ve un preflight exitoso y luego observa un fallo en la cadena. La simulación era precisa para el bank contra el que se ejecutó, pero ese bank no era en el que aterrizó la transacción. Para rutas de envío donde la corrección importa más que unos pocos milisegundos, alinea preflightCommitment con el commitment en el que pretendes confirmar, y revisa Niveles de commitment de Solana: processed vs confirmed vs finalized antes de elegir.
El preflight es una conveniencia del lado del nodo, no una garantía de consenso. Detecta fallos obvios (firmas faltantes, lamports insuficientes, errores de programa) antes de que la transacción consuma recursos del clúster, pero no puede predecir cambios de estado que ocurren entre la simulación y la ejecución. Trata un preflight exitoso como un filtro, no como una promesa.
- Fase 1: el nodo simula contra el bank seleccionado por preflightCommitment.
- Fase 2: el nodo reenvía al clúster solo si la fase 1 tiene éxito.
- Un preflightCommitment más bajo puede pasar localmente y aun así fallar en el clúster real.
- El preflight es un filtro, no una garantía de consenso.
Inspeccionar el sobre de error sin procesar de sendTransaction con curl
Antes de escribir lógica de reintento, ayuda ver el sobre de error JSON-RPC exacto que devuelve tu endpoint. El siguiente comando curl envía una transacción firmada codificada en base64 con el preflight habilitado e imprime la respuesta sin procesar. Reemplaza RPC_URL con tu endpoint y BASE64_TX con la transacción serializada.
Ejecútalo contra una transacción que falle deliberadamente (por ejemplo, una con lamports insuficientes) para observar la forma de -32002, incluido el campo data que contiene los logs de simulación. El mismo comando con una transacción válida muestra la forma de éxito: un campo result que contiene la cadena de firma. Este es el sobre que tu código cliente debe analizar.
- Usa curl para capturar el sobre de error sin procesar antes de programar los reintentos.
- Una transacción fallida revela el payload de datos de -32002 con los logs.
- Una transacción válida devuelve una cadena de resultado que contiene la firma.
- Canaliza a través de jq para inspeccionar campos anidados como data e InstructionError.
curl -s -X POST "$RPC_URL" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
"BASE64_TX",
{
"encoding": "base64",
"skipPreflight": false,
"preflightCommitment": "confirmed",
"maxRetries": 0
}
]
}' | jq .Contrato de parámetros de producción: encoding, skipPreflight, preflightCommitment, maxRetries
El contrato de parámetros de sendTransaction define cuatro campos que importan en producción. encoding controla cómo se transmite la transacción serializada (base64 es la opción común para transporte seguro para binarios). skipPreflight omite la fase uno por completo. preflightCommitment selecciona el bank de simulación. maxRetries es un contador de mejor esfuerzo del lado del nodo para reenviar la transacción a los líderes.
El punto operativo crítico es que maxRetries está documentado como un reintento del lado del nodo, no como una garantía de entrega. El nodo puede dejar de reintentar por razones fuera de tu control (calendario de líderes, límites de cola internos, reinicio del nodo), y no te informa cuando se rinde. No debes tratar maxRetries como un sustituto de tu propio bucle de confirmación. Establécelo en un valor pequeño o cero y hazte cargo de la lógica de reintento en tu cliente, donde puedes observar el estado.
skipPreflight: true es apropiado solo cuando ya has simulado la transacción tú mismo (por ejemplo, mediante simulateTransaction) y quieres minimizar la latencia, o cuando estás compitiendo deliberadamente con una transacción que sabes que es válida. Omitir el preflight en una transacción no validada significa que el clúster la rechazará después de consumir recursos, y recibirás un error menos estructurado. El artículo hermano Decodificar errores de simulateTransaction en Solana cubre la ruta de diagnóstico cuando eliges simular fuera de banda.
- encoding: base64 es el valor predeterminado seguro para binarios en transacciones serializadas.
- skipPreflight: omite la simulación; úsalo solo después de tu propia simulación.
- preflightCommitment: selecciona el bank para la simulación de la fase uno.
- maxRetries: mejor esfuerzo del lado del nodo, no una garantía de entrega.
Taxonomía de errores en el momento del envío y lógica de ramificación
La referencia de códigos de error de RPC de Solana en solana.com/docs/rpc documenta los códigos en el momento del envío que encontrarás. -32002 (Transaction simulation failed) lleva un payload data que contiene los logs de simulación y un InstructionError anidado; decodifica ese error anidado usando el método descrito en Decodificar errores de simulateTransaction en Solana. -32003 (Transaction signature verification failure) significa que las firmas de la transacción no coinciden con el mensaje, lo que generalmente indica un error de firma o un mensaje mutado. -32005 (node is behind) significa que la vista del nodo sobre el clúster está desactualizada y debes reintentar contra un nodo diferente.
Los errores de parámetros usan el código estándar JSON-RPC -32602 (Invalid params), definido por la Especificación JSON-RPC 2.0. Estos indican una solicitud malformada (encoding incorrecto, campo faltante, base64 inválido) y no deben reintentarse sin corregir la solicitud. El sobre JSON-RPC 2.0 es el mismo para todos estos: un objeto error con code, message y data opcional.
Ramifica según el código, no según la cadena del mensaje. El campo message es legible por humanos y puede variar según la implementación del nodo, pero el code numérico es estable. Cuando data está presente, puede ser una cadena (logs de simulación) o un objeto (error estructurado); maneja ambas formas de manera defensiva.
- -32002: Transaction simulation failed; decodifica el InstructionError anidado desde data.
- -32003: fallo de verificación de firma; corrige la firma, no reintentes a ciegas.
- -32005: el nodo está retrasado; reintenta contra otro nodo.
- -32602: parámetros inválidos; corrige la solicitud, no reintentes.
- Ramifica según el código numérico; trata el mensaje solo como legible por humanos.
Reintento idempotente: la firma como clave de deduplicación
La firma de una transacción de Solana se deriva de su mensaje. Por lo tanto, reenviar la transacción firmada byte a byte idéntica es seguro: el clúster deduplica por firma y rechaza un duplicado como ya conocido en lugar de ejecutarlo dos veces. Esta es la misma propiedad a nivel de transporte descrita en Idempotencia de JSON-RPC y seguridad ante solicitudes duplicadas, aplicada a la ruta de envío de Solana.
La consecuencia práctica es que tu bucle de reintento debe volver a consultar getSignatureStatuses antes de cada reenvío. Si la firma ya tiene un estado terminal (confirmed o finalized), detente. Si todavía está en processed o es desconocida, reenviar los bytes idénticos es seguro. Nunca vuelvas a firmar el mismo mensaje con un blockhash diferente y reenvíes ambas versiones; eso produce dos firmas distintas y arriesga una doble ejecución.
Por eso el bucle de reintento debe mantener constantes los bytes de la transacción serializada entre reenvíos. Cualquier mutación del mensaje (incluido un nuevo blockhash) cambia la firma y rompe la idempotencia.
- La firma se deriva del mensaje; bytes idénticos producen una firma idéntica.
- Los envíos duplicados se rechazan como ya conocidos, no se ejecutan dos veces.
- Vuelve a consultar getSignatureStatuses antes de cada reenvío; detente ante un estado terminal.
- Nunca reenvíes dos firmas diferentes para la misma transferencia lógica.
La vida útil del blockhash como límite de reintentos
El blockhash reciente de una transacción solo es válido durante una ventana limitada. La documentación de Solana sobre confirmación de transacciones y vida útil del blockhash (ver solana.com/docs) explica que una vez que el blockhash expira, la transacción ya no puede incluirse y el clúster la rechazará. Esto limita tu bucle de reintento: no puedes reenviar una transacción obsoleta para siempre.
Cuando el blockhash expira, debes volver a firmar con un blockhash nuevo. Eso produce una nueva firma, por lo que la garantía de idempotencia se reinicia: primero debes confirmar que la firma antigua no aterrizó antes de enviar la nueva. La secuencia segura es consultar getSignatureStatuses para la firma antigua, y solo si está ausente o expirada, construir y firmar una nueva transacción con un blockhash nuevo.
Para flujos de trabajo que no pueden tolerar esta ventana de re-firma, los nonces duraderos proporcionan un blockhash que no expira. Las ventajas y desventajas se cubren en Expiración del blockhash en Solana y nonces duraderos.
- El blockhash solo es válido durante una ventana limitada; la expiración limita los reintentos.
- Al expirar, vuelve a firmar con un blockhash nuevo; la firma cambia.
- Confirma que la firma antigua no aterrizó antes de enviar la nueva.
- Los nonces duraderos eliminan el límite de expiración a costa de una configuración adicional.
Envío ejecutable en Node.js con preflight, análisis de errores y reintento idempotente
El siguiente ejemplo envía con el preflight activado, analiza el sobre de error JSON-RPC, recurre a un simulateTransaction de diagnóstico en -32002, y reintenta de forma idempotente con retroceso exponencial hasta obtener un estado de firma terminal. Usa @solana/web3.js y un fetch simple para la llamada RPC sin procesar, de modo que el sobre de error sea visible.
Reemplaza RPC_URL con tu endpoint. El bucle mantiene constante la transacción serializada entre reenvíos y solo vuelve a firmar cuando el blockhash ha expirado.
import { Connection, Keypair, Transaction, SystemProgram, sendAndConfirmTransaction } from '@solana/web3.js';
const RPC_URL = process.env.RPC_URL; // e.g. your OnFinality Solana endpoint
const connection = new Connection(RPC_URL, 'confirmed');
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) {
const err = new Error(json.error.message);
err.code = json.error.code;
err.data = json.error.data;
throw err;
}
return json.result;
}
async function submitWithRetry(signedTx, maxAttempts = 8) {
const raw = signedTx.serialize().toString('base64');
const signature = signedTx.signatures[0].signature.toString('base64');
let attempt = 0;
let backoff = 500;
while (attempt < maxAttempts) {
attempt++;
// 1. Check terminal status before resending.
const statuses = await rpc('getSignatureStatuses', [[signature], { searchTransactionHistory: true }]);
const status = statuses.value[0];
if (status && (status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized')) {
return { signature, status: status.confirmationStatus };
}
try {
// 2. Submit with preflight on.
const sig = await rpc('sendTransaction', [raw, { encoding: 'base64', skipPreflight: false, preflightCommitment: 'confirmed', maxRetries: 0 }]);
console.log('submitted', sig, 'attempt', attempt);
} catch (e) {
if (e.code === -32002) {
// 3. Diagnostic simulateTransaction fallback.
const sim = await rpc('simulateTransaction', [raw, { encoding: 'base64', commitment: 'confirmed' }]);
console.error('preflight failed; simulation logs:', sim.value.logs);
throw new Error('simulation failed: ' + JSON.stringify(sim.value.err));
}
if (e.code === -32003) throw new Error('signature verification failed; fix signing');
if (e.code === -32005) { /* node behind: fall through to backoff */ }
if (e.code === -32602) throw new Error('invalid params: ' + e.message);
// transport or unknown error: back off and retry
}
await new Promise(r => setTimeout(r, backoff));
backoff = Math.min(backoff * 2, 8000);
}
throw new Error('exhausted retries without terminal status');
}
// Usage: build, sign, then submit.
const payer = Keypair.generate();
const tx = new Transaction().add(SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: payer.publicKey, lamports: 1 }));
tx.recentBlockhash = (await connection.getLatestBlockhash('confirmed')).blockhash;
tx.feePayer = payer.publicKey;
tx.sign(payer);
submitWithRetry(tx).then(console.log).catch(console.error);Tabla de resultados: medir el comportamiento del preflight y los reintentos contra tu endpoint
El comportamiento del proveedor varía, así que mide contra tu propio endpoint en lugar de confiar en un número genérico. Ejecuta el bucle de envío anterior contra una transacción que sepas que es válida y registra los siguientes campos por intento. Rellena la tabla con tus propias observaciones; no trates ninguna cifra publicada como sustituto de tu propia medición.
El objetivo es caracterizar la latencia del preflight de tu endpoint, la distribución de errores y la convergencia de reintentos para que puedas establecer límites de retroceso e intentos que se ajusten a la realidad.
- Número de intento
- Latencia de reloj de pared de sendTransaction (ms)
- Código de error devuelto (si lo hay)
- confirmationStatus de getSignatureStatuses en el momento de la comprobación
- Retroceso aplicado antes del siguiente intento (ms)
- Estado terminal alcanzado (confirmed/finalized) e intentos totales
Modos de fallo y lista de verificación de solución de problemas
La mayoría de los fallos de envío caen en un pequeño conjunto de patrones. Recorre la lista de verificación en orden; cada elemento aísla una capa diferente de la ruta de dos fases.
Si el preflight pasa pero la transacción nunca se confirma, es probable que el blockhash haya expirado antes de que un líder la incluyera. Si el preflight falla con -32002, el InstructionError anidado te dice qué instrucción y qué programa fallaron; decodifícalo antes de cambiar cualquier otra cosa. Si ves -32005, el nodo está retrasado y debes conmutar por error a otro endpoint en lugar de reintentar el mismo.
- El preflight pasa, no hay confirmación: verifica la expiración del blockhash y vuelve a firmar.
- -32002: decodifica el InstructionError anidado; no reintentes sin cambios.
- -32003: verifica que el mensaje no se haya mutado después de firmar.
- -32005: el nodo está retrasado; conmuta por error a un endpoint saludable.
- -32602: corrige el encoding o la forma de los parámetros; no reintentes.
- Firma duplicada rechazada: es lo esperado; consulta el estado en lugar de reenviar.
- El bucle de reintento nunca termina: confirma que la comprobación de estado terminal sea correcta.
Ventajas y limitaciones de los reintentos agresivos y skipPreflight
Los reintentos agresivos amplifican la carga sobre el nodo y el clúster. Debido a que los envíos duplicados se deduplican por firma, no se ejecutan dos veces, pero aún consumen capacidad de RPC y pueden desplazar a otros llamadores. Usa retroceso exponencial con un límite máximo y detente tan pronto como se observe un estado terminal.
skipPreflight: true reduce la latencia pero elimina el filtro del lado del nodo, por lo que las transacciones malformadas o fallidas llegan al clúster y devuelven errores menos estructurados. Resérvalo para transacciones que ya hayas simulado. El comportamiento del lado del proveedor también varía: algunos endpoints aplican sus propios límites de velocidad, colas o valores predeterminados de preflight, por lo que los mismos parámetros pueden comportarse de manera diferente entre proveedores. El comportamiento documentado es la línea base; verifícalo contra tu endpoint.
Finalmente, maxRetries es un mejor esfuerzo del lado del nodo y no debe confiarse para la entrega. Hazte cargo del bucle de reintento en tu cliente, donde puedes observar el estado y aplicar retroceso. Para una estrategia más amplia de tiempos de espera y reintentos, consulta Tiempos de espera y estrategia de reintentos en RPC de Solana.
- Los reintentos amplifican la carga; usa retroceso exponencial con límite.
- skipPreflight elimina el filtro del lado del nodo; úsalo solo después de la simulación.
- Los valores predeterminados y límites de velocidad del proveedor varían; verifícalos contra tu endpoint.
- maxRetries es de mejor esfuerzo; hazte cargo del bucle en tu cliente.
Próximos pasos: endpoints, precios y guías relacionadas
Para llevar esto a producción, elige un endpoint que se ajuste a tus requisitos de confirmación y reintento. La página de la red Solana describe los endpoints RPC de Solana disponibles, y Precios de RPC cubre las diferencias a nivel de plan que afectan el rendimiento y los límites de velocidad. Si estás integrando en la capa de API, la descripción general del servicio de API explica cómo se estructura el acceso gestionado.
Para referencia a nivel de método, la Guía de la API RPC de Solana (RPC Assistant) documenta toda la superficie de métodos. Para profundizar en la ruta de diagnóstico, lee Decodificar errores de simulateTransaction en Solana; para el manejo de la expiración, lee Expiración del blockhash en Solana y nonces duraderos; y para la idempotencia a nivel de transporte, lee Idempotencia de JSON-RPC y seguridad ante solicitudes duplicadas. Explora el centro de aprendizaje de OnFinality para ver la serie completa de solución de problemas.
- Elige un endpoint que se ajuste a tus necesidades de confirmación y reintento.
- Revisa los precios para diferencias de rendimiento y límites de velocidad.
- Usa la guía de RPC Assistant para referencia a nivel de método.
- Continúa con las guías de simulateTransaction, blockhash e idempotencia.