Solana 版本化交易与 1232 字节上限

为什么一笔 swap 还没发出去就失败了、address lookup table 怎么压缩账户引用,以及什么时候 v0 交易值得那点额外配置。

BoltTx Team··14 min read
solana版本化交易address-lookup-table交易大小swaprpc

一笔多跳 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

★不是永远值得。★ 这套配置要花租金、两笔交易、以及运维上的复杂度。

值得:

不值得:

最后这条对 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 加地址;也可以停用并关闭它来回收租金。已有的条目不能原地替换。

交易体积会影响上链吗? 间接影响。超大的交易根本发不出去,所以那是客户端失败而不是上链失败。一旦能序列化,体积就不会显著改变它的路由方式。

延伸阅读

返回博客列表