Cada transacción de Solana incluye un blockhash reciente, una referencia a un estado reciente de la red que demuestra que la transacción se creó "ahora". La red rechaza cualquier transacción con un blockhash anterior a ~150 bloques (~60-90 segundos), previniendo ataques de repetición y envíos obsoletos. Esto funciona perfectamente para pagos en tiempo real. Pero rompe los flujos de trabajo que necesitan un intervalo entre la firma y el envío, tales como:
| Escenario | Por qué fallan las transacciones estándar |
|---|---|
| Operaciones de tesorería | El CFO en Tokio firma, el controlador en NYC aprueba: 90 segundos no son suficientes |
| Flujos de cumplimiento | Las transacciones necesitan revisión legal/de cumplimiento antes de la ejecución |
| Firma en almacenamiento frío | Las máquinas aisladas requieren transferencia manual de transacciones firmadas |
| Preparación por lotes | Preparar nómina o desembolsos en horario laboral, ejecutar durante la noche |
| Coordinación multi-firma | Múltiples aprobadores en diferentes zonas horarias |
| Pagos programados | Programar pagos para ejecutarse en una fecha futura |
En las finanzas tradicionales, un cheque firmado no expira en 90 segundos. Ciertas operaciones de blockchain tampoco deberían hacerlo. Los nonces duraderos resuelven esto reemplazando el blockhash reciente con un valor almacenado y persistente que solo avanza cuando lo usas, dándote transacciones que permanecen válidas hasta que estés listo para enviarlas.
Cómo funciona
En lugar de un blockhash reciente (válido ~150 bloques), usas una cuenta nonce, una cuenta especial que almacena un valor único que puede usarse en lugar de un blockhash. Cada transacción que use este nonce debe "avanzarlo" como primera instrucción. Cada valor de nonce solo puede usarse para una transacción.
La cuenta nonce cuesta ~0.0015 SOL para exención de rent. Una cuenta nonce = una transacción pendiente a la vez. Para flujos de trabajo paralelos, crea múltiples cuentas nonce.
Crear una cuenta nonce
Crear una cuenta nonce requiere dos instrucciones en una sola transacción:
- Crear la cuenta usando
getCreateAccountInstructiondel System Program - Inicializarla como nonce usando
getInitializeNonceAccountInstruction
Generar keypair
Genera un nuevo keypair para usar como dirección de la cuenta nonce y calcula el espacio requerido y el rent.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();
Instrucción de crear cuenta
Crea la cuenta propiedad del System Program con suficientes lamports para la exención de rent.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();const createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});
Instrucción de inicializar nonce
Inicializa la cuenta como cuenta nonce, estableciendo la autoridad que puede avanzarla.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();const createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});const initNonceIx = getInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: sender.address});
Construir transacción
Construye una transacción con ambas instrucciones.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();const createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});const initNonceIx = getInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: sender.address});const { value: blockhash } = await rpc.getLatestBlockhash().send();const createNonceTx = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),(tx) =>appendTransactionMessageInstructions([createNonceAccountIx, initNonceIx],tx));
Firmar y enviar
Firma y envía la transacción para crear e inicializar la cuenta nonce.
Construir una transacción diferida
En lugar de un blockhash reciente, usa el blockhash de la cuenta nonce como
tiempo de vida de la transacción.
Obtener el nonce
Obtén los datos de la cuenta nonce. Usa el blockhash de la cuenta nonce como
tiempo de vida de la transacción.
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
Crear instrucción de transferencia
Crea la instrucción para tu pago. Este ejemplo muestra una transferencia de tokens.
Construir transacción con nonce duradero
Usa setTransactionMessageLifetimeUsingDurableNonce que establece el nonce como
blockhash y antepone automáticamente la instrucción de avance del nonce.
Firmar transacción
Firma la transacción. Ahora usa el nonce duradero en lugar de un blockhash estándar.
Almacenar o enviar transacción
Después de firmar, codifica la transacción para almacenarla. Cuando esté lista, envíala a la red.
Codificar para almacenamiento
Codifica la transacción firmada a base64. Almacena este valor en tu base de datos.
Enviar transacción
Envía la transacción firmada cuando esté lista. La transacción permanece válida hasta que el nonce sea avanzado.
Demo
// Generate keypairs for sender and recipientconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();console.log("Sender Address:", sender.address);console.log("Recipient Address:", recipient.address);// Demo Setup: Create RPC connection, mint, and token accountsconst { rpc, rpcSubscriptions, mint } = await demoSetup(sender, recipient);// =============================================================================// Step 1: Create a Nonce Account// =============================================================================const nonceKeypair = await generateKeyPairSigner();console.log("\nNonce Account Address:", nonceKeypair.address);const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();// Instruction to create new account for the nonceconst createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});// Instruction to initialize the nonce accountconst initNonceIx = getInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: sender.address});// Build and send nonce account creation transactionconst { value: blockhash } = await rpc.getLatestBlockhash().send();const createNonceTx = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),(tx) =>appendTransactionMessageInstructions([createNonceAccountIx, initNonceIx],tx));const signedCreateNonceTx =await signTransactionMessageWithSigners(createNonceTx);assertIsTransactionWithBlockhashLifetime(signedCreateNonceTx);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedCreateNonceTx,{ commitment: "confirmed" });console.log("Nonce Account created.");// =============================================================================// Step 2: Token Payment with Durable Nonce// =============================================================================// Fetch current nonce value from the nonce accountconst { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);console.log("Nonce Account data:", nonceData);const [senderAta] = await findAssociatedTokenPda({mint: mint.address,owner: sender.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});const [recipientAta] = await findAssociatedTokenPda({mint: mint.address,owner: recipient.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});console.log("\nMint Address:", mint.address);console.log("Sender Token Account:", senderAta);console.log("Recipient Token Account:", recipientAta);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n // 0.25 tokens});// Create transaction message using durable nonce lifetime// setTransactionMessageLifetimeUsingDurableNonce automatically prepends// the AdvanceNonceAccount instructionconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) =>setTransactionMessageLifetimeUsingDurableNonce({nonce: nonceData.blockhash as string as Nonce,nonceAccountAddress: nonceKeypair.address,nonceAuthorityAddress: nonceData.authority},tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithDurableNonceLifetime(signedTransaction);const transactionSignature = getSignatureFromTransaction(signedTransaction);// Encode the transaction to base64, optionally save and send at a later timeconst base64EncodedTransaction =getBase64EncodedWireTransaction(signedTransaction);console.log("\nBase64 Encoded Transaction:", base64EncodedTransaction);// Send the encoded transaction, blockhash does not expireawait rpc.sendTransaction(base64EncodedTransaction, {encoding: "base64",skipPreflight: true}).send();console.log("\n=== Token Payment with Durable Nonce Complete ===");console.log("Transaction Signature:", transactionSignature);// =============================================================================// Demo Setup Helper Function// =============================================================================
Invalidar una transacción pendiente
Cada cuenta nonce blockhash solo puede usarse una vez. Para invalidar una
transacción pendiente o preparar la cuenta nonce para reutilizarla, avánzala
manualmente:
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
Esto genera un nuevo valor de nonce, haciendo que cualquier transacción firmada con el valor antiguo sea permanentemente inválida.
Flujo de trabajo de aprobación multipartita
Deserializa la transacción para agregar firmas adicionales, luego serializa nuevamente para almacenarla o enviarla:
import {getBase64Decoder,getTransactionDecoder,getBase64EncodedWireTransaction,partiallySignTransaction} from "@solana/kit";// Deserialize the stored transactionconst txBytes = getBase64Decoder().decode(serializedString);const partiallySignedTx = getTransactionDecoder().decode(txBytes);// Each approver adds their signatureconst fullySignedTx = await partiallySignTransaction([newSigner],partiallySignedTx);// Serialize again for storage or submissionconst serialized = getBase64EncodedWireTransaction(fullySignedTx);
La transacción puede ser serializada, almacenada y pasada entre aprobadores. Una vez que se recopilen todas las firmas requeridas, envíala a la red.
Consideraciones de producción
Gestión de cuentas nonce:
- Crear un grupo de cuentas nonce para la preparación paralela de transacciones
- Rastrear qué nonces están "en uso" (tienen transacciones firmadas pendientes)
- Implementar el reciclaje de nonces después de que las transacciones se envíen o abandonen
Seguridad:
- La autoridad del nonce controla si las transacciones pueden invalidarse. Considera separar la autoridad del nonce de los firmantes de transacciones para un control adicional y separación de funciones
- Cualquiera con los bytes de transacción serializados puede enviarla a la red
Recursos relacionados
Is this page helpful?