当 Solana 交易预检模拟失败时,JSON-RPC 错误消息“Transaction simulation failed”包含一个 logs 数组,该数组精确定位了失败点。关键在于读取“Program <PROGRAM_ID> failed: custom program error: 0x<N>”或“Error processing Instruction N: Program failed to complete”这一行,然后将指令索引映射到交易的编译指令,并将数字代码映射到程序的错误枚举。本文解释了该机制,提供了一个可复现的 Node.js 脚本来解析错误,并给出了修复常见原因的检查清单。
直接回答:如何阅读 Solana 模拟错误
当您在 Solana RPC 端点上调用 simulateTransaction(或启用预检的 sendTransaction)时,节点会根据当前银行状态运行交易。如果失败,JSON-RPC 响应将是一个错误对象,其中包含 message: "Transaction simulation failed" 和 data.logs 数组。最后一条有意义的日志行通常会准确告诉您出了什么问题。对于程序编码错误,您会看到 Program <PROGRAM_ID> failed: custom program error: 0x<N>。对于运行时级别的失败(例如,指令调用的程序无法完成),您会看到 Error processing Instruction N: Program failed to complete。指令索引 N 指的是交易编译指令中的位置(对于版本化交易,在地址查找表查找之后)。十六进制代码 0x<N> 是失败程序自定义错误枚举中错误变体的索引,从第一个变体的 0x0 开始。要修复该问题,您必须将该索引映射回程序的源代码,并检查所涉及账户的状态。
本文是 OnFinality Learn 中心 的一部分,专注于诊断失败的交易,补充了我们的指南:Solana 版本化交易与解析 和 Solana RPC 超时与重试。如果您是 Solana RPC 端点的新手,请参阅我们的 Solana JSON-RPC 方法(RPC 助手) 和 Solana 网络概述。
机制:预检模拟期间会发生什么
Solana 运行时按确定性顺序处理交易。首先,它检查费用支付者的余额和区块哈希。然后,它加载交易引用的每个账户。对于每条指令,它使用给定的账户和指令数据调用指定的程序。如果任何步骤失败,运行时中止并返回错误。RPC 节点捕获此过程中发出的日志,并将其附加到 JSON-RPC 错误响应中。
simulateTransaction RPC 方法(在 Solana RPC 文档 中有文档说明)接受 sigVerify 标志和可选的 accounts 配置。默认情况下,除非您设置 replaceRecentBlockhash,否则它不需要最近的区块哈希。响应包含一个 value 对象,其中包含 err、logs 和 accounts。当 err 非空时,交易失败。logs 数组包含来自运行时和程序本身的消息。最终的错误行通常有两种形式之一:
Program <PROGRAM_ID> failed: custom program error: 0x<N>– 程序返回了自定义错误。十六进制数字是程序错误枚举中错误变体的索引。例如,如果程序定义了enum MyError { InsufficientFunds, InvalidOwner },那么0x0表示InsufficientFunds,0x1表示InvalidOwner。
Error processing Instruction N: Program failed to complete– 程序未返回干净的错误(例如,它发生 panic、耗尽计算单元或遇到意外的系统调用失败)。指令索引N告诉您哪条指令导致了问题。
其他常见的运行时错误包括 BlockhashNotFound(区块哈希过期或无效)和 AccountInUse(与另一笔交易冲突)。这些错误并非特定于程序,通常表示客户端问题而非逻辑错误。
- 预检模拟是可选的:
sendTransaction接受skipPreflight: true来绕过它,但如果交易无效,提交时仍会失败。 logs数组可能包含错误行之前的部分日志;这些日志可以帮助您了解交易进行到了哪一步。- 对于版本化交易(v0),错误中的指令索引指的是编译指令数组中的顺序,该数组可能包含地址查找表。SDK 的
Transaction对象以相同顺序公开instructions。
解码指令索引和程序 ID
Error processing Instruction N 中的指令索引 N 是从零开始的。要找到对应的指令,请查看交易的 instructions 数组(对于传统交易)或 message.compiledInstructions(对于版本化交易)。每条指令都有一个 programIdIndex,它指向账户键数组。程序 ID 是该索引处的账户键。如果您使用 @solana/web3.js,Transaction 对象有一个 compileMessage() 方法,返回编译后的指令。对于版本化交易,TransactionMessage 类可以将消息反编译回 TransactionInstruction 对象。
一旦有了程序 ID,您就可以识别哪个程序失败了。如果它是众所周知的程序(例如,系统程序、代币程序或关联代币账户程序),错误代码在 Solana 源代码中有文档说明。对于自定义程序,您需要查看程序的源代码来映射错误代码。约定是错误枚举的变体索引与十六进制代码匹配。例如,如果日志显示 custom program error: 0x2,则程序返回了其错误枚举的第三个变体(索引 2)。
区分程序错误和运行时错误很重要。程序错误由程序本身通过 ProgramError::Custom(u32) 返回。运行时错误(如 ProgramFailedToComplete)意味着程序执行因意外情况(如 panic 或超出计算预算)而中止。在这种情况下,日志可能会显示 Program log: Panicked 行或计算单元耗尽消息。
- 检查指令的
programIdIndex以从账户键中获取程序 ID。 - 对于版本化交易,请记住编译后的指令可能引用地址查找表中的账户;当您使用
TransactionMessage.decompile()时,SDK 会透明地处理这一点。 - 如果错误是
Program failed to complete,请查找前面的Program log: Panicked或Program log: Error:行以获取更多详细信息。
可复现示例:使用 Node.js 解析模拟错误
以下脚本使用 @solana/web3.js 构建一个简单的交易,针对用户提供的 RPC URL 进行模拟,并打印原始 JSON-RPC 错误以及解析后的细分。将 YOUR_RPC_URL 替换为您的端点(例如,来自 OnFinality 的 Solana RPC)。该脚本故意创建一笔会失败的交易(例如,从没有余额的账户转移 lamports)以演示错误格式。
使用 Node.js 18+ 和 npm install @solana/web3.js 运行它。该脚本打印原始错误对象,然后使用简单的正则表达式从日志中提取指令索引、程序 ID 和错误代码。
- 预期输出:模拟结果将包含
err,消息类似于"Transaction simulation failed: Error processing Instruction 0: custom program error: 0x0",日志显示Program 11111111111111111111111111111111 failed: custom program error: 0x0(系统程序的InsufficientFunds错误)。 - 该脚本打印原始 JSON-RPC 错误和解析后的字段。将其用作您自己交易的模板。
const { Connection, Keypair, SystemProgram, Transaction, LAMPORTS_PER_SOL } = require('@solana/web3.js');
const RPC_URL = process.env.RPC_URL || 'YOUR_RPC_URL';
const connection = new Connection(RPC_URL, 'confirmed');
async function main() {
// Create a fee payer with no lamports (will cause simulation to fail)
const feePayer = Keypair.generate();
const recipient = Keypair.generate();
const tx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: feePayer.publicKey,
toPubkey: recipient.publicKey,
lamports: LAMPORTS_PER_SOL,
})
);
tx.feePayer = feePayer.publicKey;
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
try {
const result = await connection.simulateTransaction(tx);
console.log('Simulation result:', JSON.stringify(result, null, 2));
if (result.value.err) {
const logs = result.value.logs || [];
const errorLine = logs.find(l => l.includes('failed:') || l.includes('Error processing'));
console.log('\nParsed error line:', errorLine);
// Extract instruction index and program ID
const match = errorLine.match(/Error processing Instruction (\d+): Program (\S+) failed/);
if (match) {
console.log('Instruction index:', match[1]);
console.log('Program ID:', match[2]);
}
const customMatch = errorLine.match(/custom program error: (0x[0-9a-fA-F]+)/);
if (customMatch) {
console.log('Custom error code:', customMatch[1]);
}
}
} catch (e) {
console.error('RPC error:', e.message);
if (e.data && e.data.logs) {
console.log('Logs:', e.data.logs);
}
}
}
main().catch(console.error);结果表:为您的交易填写
当您针对自己失败的交易运行脚本时,请记录以下字段。此表可帮助您系统地诊断问题。
字段 值(填写) 解释 RPC URL 使用的端点 交易类型 传统 / 版本化 影响指令索引 错误消息 例如,'Transaction simulation failed' 指令索引 哪条指令失败(从 0 开始) 程序 ID 返回错误的程序 自定义错误代码 十六进制代码,如 0x0 错误枚举变体 将代码映射到程序的错误枚举 费用支付者余额 检查是否足够 区块哈希 检查是否最新 错误前的日志 用于上下文的部分日志
失败与修复检查清单
使用此检查清单来解决常见的模拟失败。第一步始终是确定错误是来自运行时(例如,费用支付者余额不足)还是来自程序(自定义错误)。
费用支付者问题:如果错误是来自系统程序的 0x0,通常意味着费用支付者没有足够的 lamports 来支付交易费用或转账金额。使用 getBalance 检查费用支付者的余额。同时确保区块哈希是最新的;过时的区块哈希会导致 BlockhashNotFound。
账户未初始化:如果程序期望账户已初始化(例如,代币账户),模拟可能会失败并出现自定义错误,如 UninitializedAccount。验证所有必需的账户是否存在且由正确的程序拥有。
程序自定义错误:将十六进制代码映射到程序的错误枚举。例如,如果程序有 enum MyError { InvalidOwner, InsufficientFunds },那么 0x0 是 InvalidOwner,0x1 是 InsufficientFunds。查看程序的源代码或 ABI 以了解条件。
指令顺序:对于版本化交易,确保使用正确的指令索引。SDK 的 Transaction 对象会处理这一点,但如果您手动构造消息,请仔细检查编译指令的顺序。
计算单元耗尽:如果日志显示 Program failed to complete 和计算单元消息,请通过添加 ComputeBudgetProgram.setComputeUnitLimit 指令来增加计算预算。
提供商特定的预检设置:某些 RPC 提供商可能具有不同的预检行为(例如,它们可能默认跳过预检或使用不同的承诺级别)。请查看您的提供商的文档。OnFinality 的 RPC 定价 和 API 服务 页面描述了我们的标准设置,但请始终使用您的端点进行验证。
局限性与权衡
模拟并不能保证交易在提交时会成功。状态可能在模拟和确认之间发生变化,导致不同的结果。此外,如果您使用较低的承诺级别,模拟不会针对最新状态执行交易;使用 commitment: 'confirmed' 或 'finalized' 以获得更准确的结果。
对于非常大的交易或程序记录过多日志时,logs 数组可能会被截断。在这种情况下,错误行可能会丢失。您可以通过设置 encoding 参数来增加日志限制,但这并非总是受支持。
对于版本化交易,错误中的指令索引指的是编译指令的顺序,如果涉及地址查找表,这可能与您构造的顺序不同。始终反编译消息以进行验证。
提供商特定的预检设置可能会影响您是否能看到错误。某些提供商可能会返回没有日志的通用错误。如果遇到这种情况,请尝试使用不同的 RPC 端点或直接使用 simulateTransaction 方法。
后续步骤与进一步阅读
既然您已经能够解码模拟错误,您可以将此知识应用于调试您的 Solana 应用程序。有关更高级的主题,请探索我们的其他指南:
- Solana 版本化交易与解析 – 了解如何解析版本化交易,这对于正确的指令索引至关重要。
- Solana RPC 超时与重试 – 处理发送交易时的网络问题。
- Solana 速率限制与 429 错误 – 避免在高吞吐量操作期间达到速率限制。
- 通过 RPC 查询 Solana 历史数据 – 检索过去的交易数据以进行分析。
- Solana JSON-RPC 方法(RPC 助手) – 所有 Solana RPC 方法的参考。
有关官方协议详细信息,请参阅 Solana 关于 simulateTransaction 的文档 和 Solana 错误代码参考。Solana Cookbook 也有社区贡献的错误解释。