Chaque transaction Solana inclut un blockhash récent — une référence à un état réseau récent qui prouve que la transaction a été créée "maintenant". Le réseau rejette toute transaction avec un blockhash plus ancien que ~150 blocs (~60-90 secondes), empêchant les attaques par rejeu et les soumissions obsolètes. Cela fonctionne parfaitement pour les paiements en temps réel. Mais cela casse les flux de travail qui nécessitent un délai entre la signature et la soumission, tels que :
| Scénario | Pourquoi les transactions standard échouent |
|---|---|
| Opérations de trésorerie | Le directeur financier à Tokyo signe, le contrôleur à New York approuve — 90 secondes ne suffisent pas |
| Flux de conformité | Les transactions nécessitent un examen juridique/de conformité avant l'exécution |
| Signature en stockage à froid | Les machines isolées nécessitent un transfert manuel des transactions signées |
| Préparation par lots | Préparer la paie ou les décaissements pendant les heures de bureau, exécuter la nuit |
| Coordination multi-signatures | Plusieurs approbateurs dans différents fuseaux horaires |
| Paiements programmés | Programmer des paiements à exécuter à une date future |
Dans la finance traditionnelle, un chèque signé n'expire pas en 90 secondes. Certaines opérations blockchain ne devraient pas non plus. Les nonces durables résolvent ce problème en remplaçant le blockhash récent par une valeur stockée et persistante qui n'avance que lorsque vous l'utilisez — vous donnant des transactions qui restent valides jusqu'à ce que vous soyez prêt à les soumettre.
Comment ça fonctionne
Au lieu d'un blockhash récent (valide ~150 blocs), vous utilisez un compte nonce, un compte spécial qui stocke une valeur unique pouvant être utilisée à la place d'un blockhash. Chaque transaction utilisant ce nonce doit l'« avancer » comme première instruction. Chaque valeur nonce ne peut être utilisée que pour une seule transaction.
Le compte nonce coûte ~0,0015 SOL pour l'exemption de loyer. Un compte nonce = une transaction en attente à la fois. Pour les flux de travail parallèles, créez plusieurs comptes nonce.
Créer un compte nonce
La création d'un compte nonce nécessite deux instructions dans une seule transaction :
- Créer le compte en utilisant
getCreateAccountInstructiondu System Program - L'initialiser comme nonce en utilisant
getInitializeNonceAccountInstruction
Générer une paire de clés
Générez une nouvelle paire de clés à utiliser comme adresse du compte nonce et calculez l'espace requis et le loyer.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();
Instruction de création de compte
Créez le compte détenu par le System Program avec suffisamment de lamports pour l'exemption de loyer.
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});
Instruction d'initialisation du nonce
Initialisez le compte comme compte nonce, en définissant l'autorité qui peut l'avancer.
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});
Construire la transaction
Construisez une transaction avec les deux 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));
Signer et envoyer
Signez et envoyez la transaction pour créer et initialiser le compte nonce.
Construire une transaction différée
Au lieu d'un blockhash récent, utilisez le blockhash du compte nonce comme
durée de vie de la transaction.
Récupérer le nonce
Récupérez les données du compte nonce. Utilisez le blockhash du compte nonce
comme durée de vie de la transaction.
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
Créer l'instruction de transfert
Créez l'instruction pour votre paiement. Cet exemple montre un transfert de token.
Construire la transaction avec un nonce durable
Utilisez setTransactionMessageLifetimeUsingDurableNonce qui définit le nonce
comme blockhash et ajoute automatiquement l'instruction d'avancement du nonce au
début.
Signer la transaction
Signez la transaction. Elle utilise maintenant le nonce durable au lieu d'un blockhash standard.
Stocker ou envoyer la transaction
Après signature, encodez la transaction pour le stockage. Lorsque vous êtes prêt, envoyez-la au réseau.
Encoder pour le stockage
Encodez la transaction signée en base64. Stockez cette valeur dans votre base de données.
Envoyer la transaction
Envoyez la transaction signée lorsque vous êtes prêt. La transaction reste valide jusqu'à ce que le nonce soit avancé.
Démo
// 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// =============================================================================
Invalider une transaction en attente
Chaque compte nonce blockhash ne peut être utilisé qu'une seule fois. Pour
invalider une transaction en attente ou préparer le compte nonce pour une
réutilisation, avancez-le manuellement :
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
Cela génère une nouvelle valeur de nonce, rendant toute transaction signée avec l'ancienne valeur définitivement invalide.
Flux de travail d'approbation multi-parties
Désérialisez la transaction pour ajouter des signatures supplémentaires, puis sérialisez à nouveau pour le stockage ou la soumission :
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 transaction peut être sérialisée, stockée et transmise entre les approbateurs. Une fois toutes les signatures requises collectées, soumettez-la au réseau.
Considérations pour la production
Gestion des comptes nonce :
- Créer un pool de comptes nonce pour la préparation parallèle des transactions
- Suivre quels nonces sont « en cours d'utilisation » (ont des transactions signées en attente)
- Implémenter le recyclage des nonces après la soumission ou l'abandon des transactions
Sécurité :
- L'autorité du nonce contrôle si les transactions peuvent être invalidées. Envisagez de séparer l'autorité du nonce des signataires de transactions pour un contrôle supplémentaire et une séparation des responsabilités
- N'importe qui disposant des octets de transaction sérialisés peut la soumettre au réseau
Ressources associées
Is this page helpful?