simulateTransaction 拿当前链上状态跑一遍你的交易,告诉你会发生什么。听起来正是你花钱之前想知道的。
问题出在"当前"两个字。你不会落在当前 slot —— 你会落在之后某个 slot,面对的是已经被其它交易改过的状态。模拟回答的是一个和你的问题相邻、但不相同的问题。
模拟到底返回什么
三样有用的东西,而且分清哪个可靠很重要:
const sim = await connection.simulateTransaction(tx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
sim.value.err; // 按当前状态,会不会失败
sim.value.logs; // 程序日志输出
sim.value.unitsConsumed; // ★实际消耗的计算单元★
sim.value.accounts; // 执行后的账户状态(需要请求)
★unitsConsumed 是最可靠的输出★ —— 因为计算消耗主要由代码路径决定,而不是行情。这就是你该拿来设 compute unit limit 的那个数字。
err 最不可靠,因为交易会不会失败,往往取决于那些"等你落块时早已变了"的状态。
两个最容易踩的选项
sigVerify: false。 跳过签名验证。适合模拟一笔你还没签名的交易 —— 开发阶段、指令还在组装的时候。
replaceRecentBlockhash: true。 在服务端替换成新的 blockhash。不加这个,模拟一笔 blockhash 已过期的交易会因为错误的原因失败,而那和你的指令毫无关系。
// 开发阶段:量计算消耗、检查指令逻辑。
await connection.simulateTransaction(tx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
// 验证一笔已完整签名的交易格式是否正确。
await connection.simulateTransaction(tx, {
sigVerify: true,
replaceRecentBlockhash: false,
});
如果你的模拟已经写对、问题在交易不上链,免费领个 BoltTx key 改一行就能测测路由那一侧。
测量计算单元
这是模拟最有价值的用途,也是最多人跳过的。
const sim = await connection.simulateTransaction(tx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
const measured = sim.value.unitsConsumed ?? 200_000;
// 余量用来覆盖更深的 CPI 链、更大的账户状态,
// 以及这次模拟没走到的分支。
const limit = Math.ceil(measured * 1.2);
★要对真实状态模拟,不要用空夹具。★ 穿过一个 tick 数据很多的池子做 swap,消耗明显高于穿过一个空池子。在 devnet 拿空池子模拟出来的数字,到生产环境会失败。
不设实测的 limit,你就会按一个远高于真实用量的默认值计费 —— 而这恰好浪费在费用最贵的时候。
读日志
sim.value.logs 是失败交易自己解释原因的地方,前提是你知道该看什么。
if (sim.value.err) {
// 最后几行通常包含真正的原因。
console.log(sim.value.logs?.slice(-8).join("\n"));
}
常见模式及其含义:
| 日志片段 | 原因 |
|---|---|
exceeded CUs meter |
★compute limit 太低★ |
insufficient funds |
余额不足,常常包含租金 |
custom program error: 0x1771 |
★滑点超限(Jupiter 一类)★ |
AccountNotFound |
账户还不存在 —— 往往是缺 ATA |
Cross-program invocation ... privilege escalation |
账户没标成可写或签名者 |
★错误码是程序自定义的。★ 0x1771 在一个 swap 程序里是一个意思,在别的程序里是另一个意思。去查那个程序自己的错误枚举,别光搜这个十六进制值。
生产环境为什么跳过 preflight
preflight 是 RPC 在转发之前自动跑的模拟。它默认开启,而对交易路径来说,它的代价大于收益。
它要花一次往返。 RPC 先模拟、再转发。这是加在每次发送上的延迟。
它模拟的是错误的 slot。 你会在之后一个或多个 slot 落块,面对已被其它交易改过的状态。preflight 通过,预测不了你的结果。
它可能因为"本来没问题"的状态而失败。 拥堵时,preflight 那一刻和收录那一刻的池子状态不同 —— preflight 会拒掉一笔本来能落块的交易。
// 生产发送路径。
await connection.sendRawTransaction(raw, {
skipPreflight: true, // ★在开发阶段模拟,不在热路径上★
maxRetries: 0,
});
★分工很清楚:★ 开发阶段模拟,用来量计算消耗、抓指令 bug;生产环境跳过 preflight,结果用 getSignatureStatuses 拿。
模拟告诉不了你什么
值得明说,因为有团队会在错误的层面上调试几周。
它会不会上链。 模拟跑的是执行,不是收录。一笔模拟完美的交易,可能因为费用、路由或 blockhash 过期而根本进不了区块 —— 这些模拟一个都碰不到。
成交价会是多少。 模拟一笔 swap,给的是按当前储备算的价格。等你落块时,别人的交易已经把储备推走了。
你会不会赢下这场竞争。 模拟没有"竞争"这个概念。两个机器人模拟同一笔清算,都会得到成功的结果 —— 但只有一个能落块。
★如果模拟通过、交易却始终没出现在链上,那问题在提交,不在执行。★
调试循环里的模拟
交易失败时,这个顺序最省时间:
① 它到底上链了吗? → getSignatureStatuses
null → 提交侧问题,模拟帮不上忙
② 上链了但报错 → 用模拟复现
③ 读最后几行日志 → 找到真正的原因
④ 修复、再模拟、重新提交
第 1 步是最多人跳过的。★对一笔从未上链的交易做模拟,你研究的是一个你并不存在的问题。★
const { value } = await connection.getSignatureStatuses([sig], {
searchTransactionHistory: true,
});
if (!value[0]) {
// 从未上链。费用、路由、重试或过期 —— 不是执行问题。
} else if (value[0].err) {
// 上链了但失败。现在模拟才是对的工具。
}
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★"落在比模拟时更晚的 slot"是常态,不是边缘情况。★ 正是这个差距,让"模拟通过"成为对指令的检查,而不是对结果的预测。
BoltTx 在这里的位置
我们做提交,而那正是模拟验证不了的那部分。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。你在本地签名,我们不托管资金、不代签、不修改交易内容。
tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 的 simulateTransaction 是做什么的? 它拿当前链上状态执行你的交易但不提交,返回是否会失败、程序日志、以及消耗的计算单元。它预测的是执行,不是收录。
模拟结果准吗? 量计算消耗准,因为那主要由代码路径决定。判断成功失败只对"模拟那一刻的状态"准 —— 你会落在之后的 slot,而滑点这类依赖行情的结果早就变了。
模拟和 preflight 有什么区别? preflight 是 RPC 在转发前自动跑的模拟。是同一个操作,只是发生在一个"会让你付延迟、而且检查的是你不会落进去的那个 slot"的时点上。
该关掉 preflight 吗?
生产发送通常该关。它花一次往返,而且模拟的是错误的 slot。开发阶段模拟才有意义,真实结果用 getSignatureStatuses 拿。
怎么从模拟里拿到计算单元?
读 sim.value.unitsConsumed。把 compute unit limit 设在它之上、留约 20% 余量,用来覆盖更深的 CPI 链和比模拟时更大的账户状态。
replaceRecentBlockhash 有什么用? 它在模拟时于服务端替换一个新的 blockhash。不加的话,blockhash 已过期的交易会因为这个原因失败,而那和你的指令毫无关系。
什么时候该用 sigVerify: false? 模拟一笔还没签名的交易时 —— 开发阶段的常态。只有当你明确想验证"一笔完整签名的交易格式对不对"时,才设成 true。
为什么我的交易模拟没问题、链上却失败了? 因为模拟和落块之间状态变了。别人的交易推动了池子、消耗了供应、或者改了某个账户。在一条"你总是晚于模拟若干 slot 才落块"的链上,这是预期之内的。
为什么我的交易模拟没问题、却根本没出现在链上?
那是提交问题,不是执行问题。模拟对费用、路由、重试行为、blockhash 过期一个字都不说。先查 getSignatureStatuses。
Solana 的模拟日志怎么读?
最后几行通常是真正的原因。找 compute meter 相关的信息、insufficient funds、AccountNotFound、或者 custom program error 的十六进制码,然后去查那个程序的错误枚举。
custom program error 0x1771 是什么意思? 这是程序自定义的。 在好几个 swap 程序里它表示滑点超限,但同一个十六进制值在别的程序里是另一回事。永远去查具体那个程序的错误定义。
能在签名之前模拟交易吗?
能,用 sigVerify: false。这就是正常的开发流程 —— 组装指令、模拟检查逻辑并量计算消耗,满意之后再签名。
模拟要花钱吗? 不花手续费,因为没有东西上链。但它会消耗一次 RPC 调用额度,而且在热发送路径上要付延迟 —— 这正是那里该跳过 preflight 的理由。
生产环境该给每笔交易都做模拟吗? 发送路径上不该。开发阶段模拟,以及调试"上链后失败"的问题时模拟。 每次发送前都模拟,买到的是一个关于错误 slot 的预测,代价是一次往返。
怎么对真实状态测计算消耗? 拿主网账户模拟,别用空的 devnet 夹具。一个状态丰富的池子消耗的计算量明显更多,而这个差别正是让 limit 在生产环境失败的原因。
模拟能告诉我会不会赢下竞争吗? 不能。模拟没有"竞争"的概念。两个机器人模拟同一个机会都会得到成功结果,而只有一个落块。竞争的结果取决于费用、路由和时机。