Every Solana transaction includes a recent blockhash—a reference to a recent network state that proves the transaction was created "now." The network rejects any transaction with a blockhash older than ~150 blocks (~60-90 seconds), preventing replay attacks and stale submissions. This works perfectly for real-time payments. But it breaks workflows that need a gap between signing and submission, such as:
| Scenario | Why Standard Transactions Fail |
|---|---|
| Treasury operations | CFO in Tokyo signs, Controller in NYC approves—90 seconds isn't enough |
| Compliance workflows | Transactions need legal/compliance review before execution |
| Cold storage signing | Air-gapped machines require manual transfer of signed transactions |
| Batch preparation | Prepare payroll or disbursements during business hours, execute overnight |
| Multi-sig coordination | Multiple approvers across time zones |
| Scheduled payments | Schedule payments to be executed at a future date |
In traditional finance, a signed check doesn't expire in 90 seconds. Certain blockchain operations shouldn't either. Durable nonces solve this by replacing the recent blockhash with a stored, persistent value that only advances when you use it—giving you transactions that stay valid until you're ready to submit.
How It Works
Instead of a recent blockhash (valid ~150 blocks), you use a nonce account, a special account that stores a unique value which can be used in place of a blockhash. Each transaction using this nonce must "advance" it as the first instruction. Each nonce value can only be used for one transaction.
The nonce account costs ~0.0015 SOL for rent exemption. One nonce account = one pending transaction at a time. For parallel workflows, create multiple nonce accounts.
Create a Nonce Account
Creating a nonce account requires two instructions in a single transaction:
- Create the account using
getCreateAccountInstructionfrom the System Program - Initialize it as a nonce using
getInitializeNonceAccountInstruction
Generate Keypair
Generate a new keypair to use as the nonce account address and calculate the required space and rent.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();
Create Account Instruction
Create the account owned by the System Program with enough lamports for rent exemption.
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});
Initialize Nonce Instruction
Initialize the account as a nonce account, setting the authority that can advance it.
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});
Build Transaction
Build a transaction with both instructions.
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));
Sign and Send
Sign and send the transaction to create and initialize the nonce account.
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));const signedCreateNonceTx =await signTransactionMessageWithSigners(createNonceTx);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedCreateNonceTx,{ commitment: "confirmed" });
Build a Deferred Transaction
Instead of a recent blockhash, use the nonce account's blockhash as the
transaction's lifetime.
Fetch the Nonce
Fetch the data from the nonce account. Use the blockhash from the nonce
account as the transaction's lifetime.
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);
Create Transfer Instruction
Create the instruction for your payment. This example shows a token transfer.
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n});
Build Transaction with Durable Nonce
Use setTransactionMessageLifetimeUsingDurableNonce which sets the nonce as the
blockhash and automatically prepends the advance nonce instruction.
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n});const transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) =>setTransactionMessageLifetimeUsingDurableNonce({nonce: nonceData.blockhash as Nonce,nonceAccountAddress: nonceKeypair.address,nonceAuthorityAddress: nonceData.authority},tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));
Sign Transaction
Sign the transaction. It now uses the durable nonce instead of a standard blockhash.
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n});const transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) =>setTransactionMessageLifetimeUsingDurableNonce({nonce: nonceData.blockhash as Nonce,nonceAccountAddress: nonceKeypair.address,nonceAuthorityAddress: nonceData.authority},tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);
Store or Send Transaction
After signing, encode the transaction for storage. When ready, send it to the network.
Encode for Storage
Encode the signed transaction to base64. Store this value in your database.
const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);const base64EncodedTransaction =getBase64EncodedWireTransaction(signedTransaction);// Store base64EncodedTransaction in your database
Send Transaction
Send the signed transaction when ready. The transaction remains valid until the nonce is advanced.
const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);const base64EncodedTransaction =getBase64EncodedWireTransaction(signedTransaction);// When ready to execute (could be days later):await rpc.sendTransaction(base64EncodedTransaction, { encoding: "base64" }).send();
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// =============================================================================
Invalidate a Pending Transaction
Each nonce account blockhash can only be used once. To invalidate a pending
transaction or prepare the nonce account for reuse, advance it manually:
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
This generates a new nonce value, making any transaction signed with the old value permanently invalid.
Multi-Party Approval Workflow
Deserialize the transaction to add additional signatures, then serialize again for storage or submission:
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);
The transaction can be serialized, stored, and passed between approvers. Once all required signatures are collected, submit to the network.
Production Considerations
Nonce account management:
- Create a pool of nonce accounts for parallel transaction preparation
- Track which nonces are "in use" (have pending signed transactions)
- Implement nonce recycling after transactions are submitted or abandoned
Security:
- The nonce authority controls whether transactions can be invalidated. Consider separating nonce authority from transaction signers for additional control and separation of duties
- Anyone with the serialized transaction bytes can submit it to the network
Related Resources
Is this page helpful?