一笔多跳 swap 报了交易过大的错误,而你甚至还没提交。这笔交易大到序列化不了。
Solana 把一笔交易的上限定在 1232 字节。而你引用的每一个账户要占掉其中 32 字节 —— 一笔穿过三个池子的路由 swap,会引用非常多账户。
1232 字节花在哪
预算由四部分吃掉:
签名 每个 64 字节
★账户 key★ ★每个 32 字节★
指令数据 不定
recent blockhash 32 字节
★通常撑爆你的是账户 key。★ 一笔经聚合器的两跳 swap 可能引用三十个以上账户:每个池子有自己的金库和 authority,每种代币有自己的账户,再加上被调用的各个程序。按每个 32 字节算,还没放指令数据就已经吃掉大部分预算了。
// 在尝试发送之前先检查。
const serialized = tx.serialize();
console.log(`${serialized.length} / 1232 字节`);
★这是客户端失败。★ 它发生在序列化阶段、在任何东西到达网络之前 —— 这也是它和这个领域里其它失败长得完全不同的原因。
版本化交易改变了什么
Legacy 交易把每个账户内联列出来。版本化交易(v0)可以通过 address lookup table 来引用账户。
ALT 是一个链上账户,里面存着一串地址。你的交易引用"表 + 索引",★这样每个账户只花 1 字节,而不是 32 字节。★
import {
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
const lookup = await connection.getAddressLookupTable(tableAddress);
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message([lookup.value]); // ★ALT 放这里★
const tx = new VersionedTransaction(message);
tx.sign([payer]);
节省很可观。三十个账户内联是 960 字节;走 lookup table 大约只要 30 字节。
创建 lookup table
两步,而且不能和使用它的交易放在同一笔里:
import { AddressLookupTableProgram } from "@solana/web3.js";
// 1. 创建表。
const [createIx, tableAddress] = AddressLookupTableProgram.createLookupTable({
authority: payer.publicKey,
payer: payer.publicKey,
recentSlot: await connection.getSlot(),
});
// 2. 往里面加地址。
const extendIx = AddressLookupTableProgram.extendLookupTable({
payer: payer.publicKey,
authority: payer.publicKey,
lookupTable: tableAddress,
addresses: [pool, vault, tokenProgram, /* ... */],
});
★两个坑:★
表在创建它的那个 slot 里不可用。 它需要先被确认。创建和使用放在一笔交易里会失败。
extend 本身也受大小限制。 你没法在一条指令里加几百个地址 —— 那笔 extend 交易自己也得装进 1232 字节。要分批。
如果你的交易序列化没问题、问题在它不上链,免费领个 BoltTx key 改一行就能测测路由那一侧。
什么时候值得用 ALT
★不是永远值得。★ 这套配置要花租金、两笔交易、以及运维上的复杂度。
值得:
- 经聚合器的路由 swap,引用账户很多
- 交易固定几个交易对的机器人,同样的账户反复出现
- 任何当前因为大小而失败的交易
不值得:
- 简单转账和单池 swap,本来就装得下
- 一次性交易,那些账户你再也不会碰
- ★狙击全新代币★ —— 账户在上新之前根本不存在,没东西可以预填
最后这条对 meme 交易特别重要。★你没法为一个还不存在的代币建 lookup table。★ 上新场景只能用内联账户,而且必须把指令集控制得很精简。
计算成本
一个常被忽略的细节:解析 lookup table 不是免费的。
运行时要读那个表账户、并且逐个索引解引用。★这会在你真正的指令之上额外消耗计算单元★ —— 所以一笔不用 ALT 时计算很宽裕的交易,用了 ALT 反而可能需要更高的 limit。
// 模拟你实际会发的那个版本,把 ALT 算进去。
const sim = await connection.simulateTransaction(versionedTx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
const limit = Math.ceil((sim.value.unitsConsumed ?? 200_000) * 1.2);
你是在拿计算换字节。 接近大小上限时通常划算,不接近时就是白白的额外开销。
Legacy 和 v0 还有什么区别
| Legacy | v0 | |
|---|---|---|
| 账户引用 | 内联,每个 32 字节 | ★经 ALT,1 字节★ |
| 实际最多账户数 | 约 35 | ★250+★ |
| 需要准备 | 无 | ★创建 + extend 表★ |
| 计算开销 | 无 | 表解析 |
| RPC 支持 | 通用 | ★要传 maxSupportedTransactionVersion★ |
★最后一行会造成静默问题。★ 读回交易时,你必须告诉 RPC 你能处理 v0:
// 不加这个,v0 交易会返回 null。
const tx = await connection.getTransaction(sig, {
maxSupportedTransactionVersion: 0,
});
漏了它,一笔完美落块的 v0 交易看起来就像不存在。★这是个常见的假警报 —— 交易没问题,是你的读取写错了。★
不用 ALT 也能减小体积
在上那套机制之前,有三个更便宜的办法:
去掉不必要的账户。 重复的账户 key 会被自动去重,但你传了却从没用到的账户,照样每个占 32 字节。
减少跳数。 经聚合器的三跳路由引用的账户远多于两跳。有时候接受一个稍差的价格,比承担体积开销更划算。
拆成两笔交易。 如果这些操作不需要原子性的话。★但套利恰恰需要原子性★ —— 而那正是你用不了这个办法的场景。
排查体积失败
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToLegacyMessage();
console.log("账户数:", message.accountKeys.length);
console.log("账户估算字节:", message.accountKeys.length * 32);
console.log("指令数:", message.instructions.length);
★如果光账户字节就超过 800 左右,ALT 就是解法。★ 如果大的是指令数据,ALT 帮不上忙 —— 那要减少你让程序做的事情。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★体积失败根本进不了提交环节★ —— 它在你自己的机器上就失败了,在提交之前。这让它成为这个领域里最便宜的失败,也是唯一一种发生时不花你钱的失败。
BoltTx 在这里的位置
我们做提交。版本化交易和 legacy 交易的提交方式完全一样 —— 格式影响的是序列化,不是路由。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。你在本地签名,我们不托管资金、不代签、不修改交易内容。
tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 交易的最大体积是多少? 1232 字节。它涵盖签名、账户 key、指令数据和 blockhash。通常把复杂交易撑爆的是每个 32 字节的账户 key。
Solana 的版本化交易是什么? 一种交易格式(v0),可以通过 address lookup table 引用账户,而不是内联列出。这样一个引用大约只占 1 字节而不是 32 字节 —— 这正是复杂交易能装下的原因。
address lookup table 是什么? 一个链上账户,里面存着一串地址。v0 交易引用"表 + 索引"而不是完整的 32 字节地址,对账户很多的交易来说体积下降极其显著。
我的 Solana 交易为什么太大?
几乎总是账户数量。一笔路由 swap 可能引用三十个以上账户、每个 32 字节。数一下 message.accountKeys.length —— 光账户字节就超过 800 左右的话,ALT 就是解法。
怎么创建 address lookup table?
两条指令:createLookupTable 然后 extendLookupTable。表在创建它的那个 slot 里不可用 —— 必须先被确认,所以创建和使用不能放在同一笔交易。
一个 lookup table 能装多少地址? 最多 256 个。但你没法一次全加进去,因为 extend 那笔交易自己也要装进 1232 字节。要分几笔加。
版本化交易的计算开销更大吗? 是的,略大。运行时要读表账户、逐个索引解引用,这在你的指令之上额外消耗计算。模拟你实际会发的那个版本。
是不是永远该用版本化交易? 不是。对本来就装得下的简单转账和单池 swap 来说,那点配置成本和计算开销买不到任何东西。接近体积上限时才用。
狙击新代币能用 lookup table 吗? 不能。全新代币的账户在上新之前不存在,没东西可以预填。 上新场景只能用内联账户,并且把指令集保持精简。
getTransaction 为什么对我的交易返回 null?
你多半漏了 maxSupportedTransactionVersion: 0。不加它,RPC 不会返回 v0 交易 —— 于是一笔完美落块的交易看起来像不存在。★交易没问题,是读取写错了。★
Legacy 和 v0 交易有什么区别? Legacy 把每个账户内联列出,实际上限约 35 个账户。v0 可以经 lookup table 引用,能支持多得多。v0 需要建表,并且增加计算开销。
不用 lookup table 能减小交易体积吗? 能,三个办法:去掉传了却没用到的账户、减少路由跳数、或者在操作不需要原子性时拆成两笔。套利通常需要原子性。
lookup table 必须我自己建吗? 不用。你可以引用任何已知地址的现有表。聚合器往往维护着覆盖常见账户的公开表,这就完全省掉了建表成本。
怎么判断 ALT 到底有没有用? 拿账户字节和指令数据比。账户 key 占大头,ALT 帮助很大;指令数据占大头,ALT 什么都改变不了 —— 那要减少你让程序做的事。
创建之后还能修改 lookup table 吗? 只要你还持有 authority,就能继续 extend 加地址;也可以停用并关闭它来回收租金。已有的条目不能原地替换。
交易体积会影响上链吗? 间接影响。超大的交易根本发不出去,所以那是客户端失败而不是上链失败。一旦能序列化,体积就不会显著改变它的路由方式。