每笔 Solana 交易都包含一个最近的区块哈希——这是对网络最新状态的引用,用于证明该交易是在“当前”创建的。网络会拒绝任何区块哈希早于约 150 个区块(约 60-90 秒)的交易,从而防止重放攻击和过期提交。这种机制非常适合实时支付。但对于签名和提交之间需要间隔的流程,则会遇到问题,例如:
| 场景 | 标准交易为何失效 |
|---|---|
| 资金管理操作 | 东京的 CFO 签名,纽约的 Controller 审批——90 秒远远不够 |
| 合规流程 | 交易需在执行前经过法律/合规审核 |
| 冷存储签名 | 隔离设备需手动转移已签名交易 |
| 批量准备 | 在工作时间准备工资或付款,夜间执行 |
| 多签协调 | 多位审批人分布在不同时区 |
| 定时支付 | 计划在未来某一日期执行支付 |
在传统金融中,签过字的支票不会在 90 秒内过期。某些区块链操作也不应如此。持久随机数(Durable nonce) 通过用一个存储的、持久的值替代最近区块哈希来解决这个问题,该值只有在你使用时才会更新——让你的交易在你准备好提交前始终有效。
工作原理
你可以使用 nonce 账户 来替代最近区块哈希(有效期约为 150 个区块)。nonce 账户是一种特殊账户,存储一个 唯一 的值,可用来替代区块哈希。每笔使用该 nonce 的交易,必须在第一条指令中“推进”该 nonce。每个 nonce 值只能用于一笔交易。
nonce 账户的租金豁免费用约为 0.0015 SOL。一个 nonce 账户 = 一次只能有一笔待处理交易。若需并行处理,请创建多个 nonce 账户。
创建 Nonce 账户
创建 nonce 账户需要在一笔交易中包含两条指令:
- 使用 System Program 的
getCreateAccountInstruction创建账户 - 使用
getInitializeNonceAccountInstruction初始化为 nonce
生成 Keypair
生成一个新的 keypair 作为 nonce 账户地址,并计算所需空间和租金。
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();
创建账户指令
使用 System Program 创建账户,并为租金豁免准备足够的 lamports。
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});
初始化 Nonce 指令
将账户初始化为 nonce 账户,并设置可推进 nonce 的权限方。
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 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));
签名并发送
签名并发送交易以创建并初始化 nonce 账户。
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" });
构建延迟交易
用 nonce 账户的 blockhash 替代最近区块哈希,作为交易的有效期。
获取 Nonce
从 nonce 账户中获取数据。使用 nonce 账户的 blockhash 作为交易的有效期。
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);
创建转账指令
为你的支付创建指令。本示例展示了一个代币转账。
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n});
使用持久 Nonce 构建交易
使用
setTransactionMessageLifetimeUsingDurableNonce,将 nonce 设置为 blockhash,并自动在前面添加 advance
nonce 指令。
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));
签名交易
签名交易。现在它使用持久 nonce 替代了标准 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);
存储或发送交易
签名后,将交易编码以便存储。准备好后,将其发送到网络。
编码以便存储
将已签名的交易编码为 base64。将该值存入数据库。
发送交易
准备好后发送已签名的交易。只要 nonce 未被推进,该交易始终有效。
演示
// 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// =============================================================================
使待处理交易失效
每个 nonce 账户 blockhash
只能使用一次。要使待处理交易失效或让 nonce 账户可复用,请手动推进:
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
这会生成一个新的 nonce 值,使所有用旧值签名的交易永久失效。
多方审批流程
反序列化交易以添加额外签名,然后再次序列化以便存储或提交:
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);
交易可以被序列化、存储,并在审批人之间传递。收集到所有必需签名后,提交到网络。
生产环境注意事项
Nonce 账户管理:
- 创建一组 Nonce 账户,用于并行准备交易
- 跟踪哪些 Nonce 正在“使用中”(即有待处理的已签名交易)
- 在交易提交或放弃后,实现 Nonce 的回收利用
安全性:
- Nonce 权限方控制交易是否可以作废。建议将 Nonce 权限与交易签名人分离,以实现更高的控制和职责分离
- 任何人 只要拥有序列化后的交易字节,都可以将其提交到网络
相关资源
Is this page helpful?