每笔 Solana 交易必须引用一个最近的区块哈希,该哈希在大约 150 个区块(约 75 秒)后过期。如果您离线签名或您的流程较慢,交易可能会被拒绝并显示“BlockhashNotFound”。持久 Nonce 用一个存储的值替换最近的区块哈希,该值仅在您明确允许时才会前进,从而使交易可以提前很长时间安全签名。本文解释了该机制,展示了如何实现这两种路径,并提供了故障排除清单。
直接回答:为什么您的 Solana 交易会过期以及如何修复
如果您曾经构建过 Solana 交易,等待几分钟,然后提交却看到 BlockhashNotFound,那么您遇到了协议内置的过期机制。每笔 Solana 交易必须包含一个最近的区块哈希——最近一个区块的哈希——并且该哈希仅在有限的时间窗口内有效。RPC 方法 getLatestBlockhash 返回一个区块哈希和一个 lastValidBlockHeight。一旦网络达到该高度,交易即被视为过期并被拒绝。对于大多数用例,此窗口约为 150 个区块,在 Solana 的 400 毫秒时隙时间下,这大约相当于 60-75 秒,但确切数字不是固定的协议常量——它是节点的最近区块哈希窗口。
在线签名的稳健模式是:获取新的区块哈希,立即签名,然后提交。如果区块哈希在提交前过期,您必须重新获取并重新签名——您不能简单地重试相同的已签名交易。然而,对于离线或缓慢的流程,重新签名是不可能的。这就是持久 Nonce 发挥作用的地方。持久 Nonce 是一个特殊的账户,存储一个仅在您明确推进时才会更改的值。通过使用持久 Nonce 代替最近的区块哈希,您的交易不再依赖于墙钟时间的新鲜度。它保持有效,直到其他人推进该 Nonce,而只有您(或您的授权人)才能做到这一点。这使得持久 Nonce 成为 Solana 上安全离线签名的标准解决方案。
- Solana 交易需要最近的区块哈希以防止重放并确保新鲜度。
- 区块哈希仅在
lastValidBlockHeight之前有效;之后,交易将被拒绝。 - 持久 Nonce 将交易有效性与时间解耦,从而实现离线签名。
- 在线流程使用
getLatestBlockhash;离线或延迟提交使用持久 Nonce。
机制:最近的区块哈希和 lastValidBlockHeight
Solana 的交易格式包含一个 recent_blockhash 字段。此哈希用于去重交易并确保交易不会无限期有效。当您调用 getLatestBlockhash 时,RPC 返回一个区块哈希和 lastValidBlockHeight——该区块哈希将不再被接受的区块高度。节点维护一个最近区块哈希的窗口(大约最近 150 个区块)。如果您提交的交易使用的区块哈希早于该窗口,节点将返回错误:BlockhashNotFound。
官方 Solana 交易确认文档 解释说,交易仅在其区块哈希位于节点的最近区块哈希窗口内时才有效。sendTransaction RPC 方法将拒绝具有过期区块哈希的交易。getLatestBlockhash 方法是获取新区块哈希及其有效高度的推荐方式。较旧的 getRecentBlockhash 方法已弃用,但仍可使用;它仅返回区块哈希,而不返回 lastValidBlockHeight,因此您无法确切知道它何时过期。
关键要点:区块哈希不是时间戳。它是最近一个区块的哈希,其有效性与链的进度相关。如果您的节点落后于链尖,它返回的区块哈希可能比网络实际链尖更旧,从而使您的交易比预期更早过期。这就是为什么使用健康、最新的 RPC 端点至关重要——请参阅 检测 RPC 节点落后于链尖 了解如何检查。
getLatestBlockhash返回blockhash和lastValidBlockHeight。- 最近区块哈希窗口大约是最后 150 个区块,但它不是固定的协议常量。
- 如果您在
lastValidBlockHeight之后提交,您将收到BlockhashNotFound。 - 滞后的 RPC 可能返回过时的区块哈希,增加过期风险。
稳健的在线模式:获取、签名、发送和处理过期
对于大多数应用程序,正确的流程很简单:获取新的区块哈希,构建并签名交易,然后立即发送。如果交易未在有效窗口内发送,您必须重新获取新的区块哈希并重新签名。您不能简单地重新提交相同的已签名交易,因为签名是针对旧区块哈希的。
当您发送交易并收到 BlockhashNotFound 错误时,不要重试相同的交易。相反,使用新的区块哈希重新构建它。这对于支付或代币转账尤其重要,因为如果您意外重新提交了实际上已确认但您未看到确认的交易,重复可能会导致双重支付。
以下 JavaScript 示例使用 @solana/web3.js 演示了该模式。它获取一个区块哈希,签名一个简单的转账,然后发送。如果交易过期,它会捕获错误并使用新的区块哈希重新构建。
// Online signing with blockhash expiry handling
import { Connection, SystemProgram, Transaction, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
const connection = new Connection('https://api.mainnet-beta.solana.com');
const from = Keypair.generate(); // replace with your keypair
const to = new PublicKey('...');
async function sendWithRetry() {
let blockhashInfo = await connection.getLatestBlockhash();
const transaction = new Transaction();
transaction.add(SystemProgram.transfer({
fromPubkey: from.publicKey,
toPubkey: to,
lamports: 0.01 * LAMPORTS_PER_SOL,
}));
transaction.recentBlockhash = blockhashInfo.blockhash;
transaction.feePayer = from.publicKey;
transaction.sign(from);
try {
const signature = await connection.sendTransaction(transaction);
console.log('Transaction sent:', signature);
} catch (error) {
if (error.message.includes('BlockhashNotFound')) {
console.log('Blockhash expired, retrying with fresh blockhash');
return sendWithRetry();
}
throw error;
}
}
sendWithRetry();持久 Nonce:离线签名解决方案
持久 Nonce 是 Solana 的一项功能,允许交易在没有最近区块哈希的情况下进行签名。相反,交易使用持久 Nonce——存储在特殊账户(NonceAccount)中的值。该账户由程序创建并拥有,并且有一个可以推进 Nonce 的授权人。当您在交易中包含持久 Nonce 时,您还必须包含一个 SystemProgram.advanceNonceAccount 指令,该指令将存储的 Nonce 更新为新值。此指令必须由 Nonce 授权人签名,并且还需要支付费用。
关键属性是 Nonce 仅在授权人明确签名 advanceNonceAccount 指令时才会前进。因此,使用持久 Nonce 的交易不会基于时间过期;它只有在另一个交易已经推进了 Nonce 时才会变得无效。这使得离线签名交易、存储它并稍后提交成为可能——甚至几天或几周后——只要 Nonce 账户保持资金充足且未被使用。
官方 Solana Cookbook 关于持久 Nonce 提供了创建和使用 Nonce 账户的配方。该过程包括:创建 Nonce 账户,使用授权人初始化它,然后使用 Nonce 代替最近的区块哈希。@solana/web3.js 库提供了诸如 createNonceAccount 和 NonceAccount 之类的辅助函数来简化此过程。
- 持久 Nonce 存储在
NonceAccount中,只能由其授权人推进。 - 使用持久 Nonce 的交易包含一个
advanceNonceAccount指令。 - Nonce 不会基于时间过期,只有在被另一个交易消耗时才会过期。
- 这为气隙或计划交易实现了安全的离线签名。
分步指南:创建和使用持久 Nonce
以下是创建 Nonce 账户并将其用于离线交易的完整流程。此示例使用 @solana/web3.js,并假设您有一个资金充足的付款人账户。步骤是:创建 Nonce 账户,初始化它,然后构建一个使用 Nonce 并包含推进指令的交易。
首先,创建一个 Nonce 账户。这需要为账户提供足够的 lamports 以使其免租。createNonceAccount 辅助函数会为您完成此操作。然后,您可以使用 getNonce 检索 Nonce 值。
当您准备离线签名时,您从账户(或从先前存储的值)获取当前的 Nonce 值,使用该 Nonce 作为 recentBlockhash 构建您的交易,并添加 advanceNonceAccount 指令。使用费用付款人和 Nonce 授权人签名。然后可以存储交易并稍后提交。
// Creating and using a durable nonce
import { Connection, SystemProgram, Transaction, Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
const connection = new Connection('https://api.mainnet-beta.solana.com');
const payer = Keypair.generate(); // fund this account
const nonceAuthority = Keypair.generate(); // authority for the nonce
// Create a nonce account
const nonceAccount = Keypair.generate();
const tx = new Transaction().add(
SystemProgram.createNonceAccount({
fromPubkey: payer.publicKey,
noncePubkey: nonceAccount.publicKey,
authority: nonceAuthority.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(80), // NonceAccount size
})
);
tx.feePayer = payer.publicKey;
await connection.sendTransaction(tx, [payer, nonceAccount]);
// Fetch the nonce value
const nonceInfo = await connection.getNonce(nonceAccount.publicKey);
const nonce = nonceInfo.nonce;
// Build an offline transaction (e.g., a transfer)
const transfer = SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: someDestination,
lamports: 0.1 * LAMPORTS_PER_SOL,
});
const offlineTx = new Transaction().add(
SystemProgram.advanceNonceAccount({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: nonceAuthority.publicKey,
}),
transfer
);
offlineTx.recentBlockhash = nonce;
offlineTx.feePayer = payer.publicKey;
// Sign offline (e.g., in an air-gapped environment)
offlineTx.sign(payer, nonceAuthority);
// Later, submit the signed transaction
const signature = await connection.sendTransaction(offlineTx);
console.log('Offline transaction sent:', signature);预期输出和要填写的结果表
当您运行在线示例时,您应该会看到一个签名被打印出来,并且交易应在几秒钟内确认。如果您故意延迟提交超过 lastValidBlockHeight,您将看到包含 BlockhashNotFound 的错误。对于持久 Nonce 示例,即使您等待几分钟再提交,只要 Nonce 未被使用,您也应该看到签名被打印出来。
要自己验证行为,您可以运行以下测试并记录结果。此表将帮助您记录您观察到的过期时间和错误消息。
- 运行在线示例,并记录获取区块哈希和发送交易之间的时间。
- 故意等待 2 分钟再发送以触发
BlockhashNotFound。 - 运行持久 Nonce 示例,并等待 5 分钟再发送——它应该成功。
- 尝试发送相同的持久 Nonce 交易两次——第二次应该失败并显示
DurableNonceMismatch。
// Fill in your observations
| Scenario | Wait time | Result (signature/error) |
|----------|-----------|--------------------------|
| Online, immediate send | 0s | |
| Online, 2 min delay | 120s | |
| Durable nonce, 5 min delay | 300s | |
| Durable nonce, double send | - | |失败和修复清单
以下是您可能遇到的常见错误以及如何修复它们。此清单基于文档化行为和社区报告。
- BlockhashNotFound:区块哈希已过期。重新获取新的区块哈希并重新签名。不要重试相同的交易。
- DurableNonceMismatch:您交易中的 Nonce 值与当前存储的 Nonce 不匹配。这通常意味着 Nonce 已被另一个交易推进。获取当前 Nonce 并重新签名。
- Attempt to advance durable nonce:当
advanceNonceAccount指令未正确签名或授权人不正确时,会发生此错误。确保 Nonce 授权人签署交易。 - Nonce account not initialized:您必须在使用 Nonce 账户之前使用
initializeNonceAccount初始化它。 - Insufficient lamports for rent:Nonce 账户必须是免租的。使用
getMinimumBalanceForRentExemption为其提供足够的 lamports。 - RPC lag:如果您的 RPC 端点滞后,它返回的区块哈希可能已过时。使用健康的端点并检查滞后——请参阅 检测 RPC 节点落后于链尖。
持久 Nonce 的限制和权衡
虽然持久 Nonce 解决了离线签名问题,但它们也有权衡。首先,Nonce 账户必须资金充足且免租,这会锁定少量 SOL。其次,Nonce 只能使用一次——在它被推进后,您需要为下一笔交易获取新的 Nonce。这意味着您必须有一个管理 Nonce 账户的流程,特别是如果您离线签署许多交易。
在安全性方面,Nonce 授权人是一个关键密钥。如果它被泄露,攻击者可以推进 Nonce 并使您的待处理交易失效。因此,授权人应与您的主签名密钥一样安全。此外,请注意,持久 Nonce 交易仍然需要费用付款人,并且 advanceNonceAccount 指令本身会产生费用。
最后,持久 Nonce 不能防止所有类型的重放。它们只确保在 Nonce 被推进后,交易不能被重放。如果您需要防止不同上下文中的重放,您仍然需要仔细设计您的交易。
后续步骤和进一步阅读
既然您了解了区块哈希过期和持久 Nonce,您可以构建更可靠的 Solana 应用程序。要深入了解 Solana 的交易机制,请参阅 Solana 文档 和 Solana Cookbook。
如果您使用 RPC 提供商,请确保使用可靠的端点。OnFinality 提供高可用性的 Solana RPC 端点。有关处理 RPC 问题的更多信息,请阅读 Solana RPC 超时和重试 和 解码 Solana 交易模拟错误。
要了解版本化交易及其与区块哈希的交互,请参阅 Solana 版本化交易和解析。如果您正在 Solana 上构建,请浏览 OnFinality Learn 中心 获取更多指南,或查看我们的 API 服务 和 RPC 定价 以获取生产级基础设施。