版本化交易

摘要

Solana 有三种交易格式:legacy、v0 和 v1。v0 新增了地址查找表(ALT),可通过 1 字节索引引用账户。v1 将大小限制提升至 4,096 字节,将资源限制移入消息本身,并移除了 ALT。

Solana 支持三种交易格式:legacy、v0 和 v1。以下将分别从三个方面介绍每种格式:字节在网络上的布局方式、账户的引用方式,以及资源限制的来源。

v1 激活状态

v1 格式已在 mainnet、devnet 和 testnet 上启用。读取交易属于破坏性变更:未选择启用的客户端在处理 v1 交易时会失败。现有应用应查看为 v1 做准备。

格式对比

限制legacyv0v1
最大交易大小1,232 字节1,232 字节4,096 字节
账户地址~32,受大小限制64,通过查找表64,内联(启用 SIMD-0596 后为 96)
地址查找表不支持支持不支持
资源限制ComputeBudget 指令ComputeBudget 指令消息配置

跳转至:Legacy · v0 · v1

Legacy 格式

这是原始格式,目前仍是大多数工具的默认格式。它没有任何版本前缀:交易的第一个字节是签名数组的 compact-u16 计数,消息的第一个字节是 num_required_signatures,其高位始终未置位。

Legacy 线格式布局

字段大小描述
num_signaturescompact-u16签名数量
signaturesnum_signatures x 64 字节Ed25519 签名
header3 字节MessageHeader — 第一个字节的版本位未置位
num_account_keyscompact-u16账户密钥数量
account_keysnum_account_keys x 32 字节公钥,全部内联
recent_blockhash32 字节生命周期指定符
num_instructionscompact-u16指令数量
instructions可变长度每条指令连续序列化

每个可变长度数组都以 compact-u16 长度为前缀:值 0–127 使用 1 字节,较大值使用 2–3 字节。关于单条指令的布局及大小计算示例,请参阅交易二进制格式。

Legacy 中的账户

每个账户在 account_keys 中以完整的 32 字节公钥写出,指令通过 1 字节索引引用该数组中的账户。无法引用未在交易中明确列出的账户,这也是 legacy 交易在耗尽 1,232 字节前大约只能容纳 32 个账户的原因。

Legacy 中的资源限制

计算单元限制、已加载账户数据大小限制、堆大小和优先费均通过在交易中包含 ComputeBudget 程序指令来请求设置。每条指令占用一个指令 slot 和 150 个计算单元。省略这些指令是安全的:运行时将回退到默认值——每条指令 200,000 CU(上限 1.4M)、64 MiB 数据大小限制、32 KiB 堆,以及零优先费。

V0 格式

v0 是在 legacy 消息基础上新增了两项内容:一个 0x80 版本前缀字节,以及附加在指令之后的 address_table_lookups 数组。此前的所有内容与 legacy 格式字节完全相同。

V0 线格式布局

字段大小描述
num_signaturescompact-u16签名数量
signaturesnum_signatures x 64 字节Ed25519 签名
0x801 字节版本前缀字节 — 消息的第一个字节
header3 字节消息头(与传统相同)
num_account_keyscompact-u16静态账户密钥数量
static_account_keysnum_account_keys x 32 字节交易中直接出现的密钥
recent_blockhash32 字节生命周期指定符
num_instructionscompact-u16指令数量
instructions可变长度与传统格式相同
address_table_lookupscompact-u16 + 可变长度ALT 引用(见下文)

每个地址表查找项包含:

字段大小描述
account_key32 字节ALT 账户的公钥
writable_indexescompact-u16 + N × 1 字节ALT 中可写账户的索引
readonly_indexescompact-u16 + N × 1 字节ALT 中只读账户的索引

地址查找表

ALT 是一个链上账户,最多可存储 256 个公钥。通过引用 ALT,交易可以使用 1 字节索引而非 32 字节公钥来包含额外账户,从而显著减少每个账户的开销。

在运行时,执行开始前,validator 会将所有 ALT 引用解析为完整公钥。解析后的地址会追加到静态账户密钥后,形成完整的账户密钥列表。ALT 解析账户的顺序与静态账户一致:可写查找项在只读查找项之前。

地址查找表只影响账户在链上传输时的引用方式。在执行时,运行时会将所有索引解析为完整账户地址。ALT 解析账户只能是可写或只读(非签名者);不能作为签名者。

创建并使用地址查找表

此示例将创建一个查找表,向其中添加一个地址,并使用该表编译并发送一笔 v0 交易。

只有非签名者账户才能从查找表中加载。手续费支付方和所有必需的签名者仍保留在消息的静态账户列表中。此外,地址在添加它的同一个 slot 中无法被查找,因此该示例会等待下一个已确认的 slot,然后再编译 v0 消息。

v0 中的资源限制

与 legacy 相同:使用 ComputeBudget 指令,省略时采用相同的默认值。

V1 格式

v1 将大小限制提升至 4,096 字节,并围绕交易配置对消息结构进行了重构:资源限制从 ComputeBudget 指令中移出,转移到消息本身的固定位置字段中。这使得网络可以通过单次固定偏移读取来按优先费对交易进行排序,而无需扫描并反序列化指令列表。

V1 线格式布局

字段大小描述
0x811 字节版本前缀字节 — 交易的第一个字节
header3 字节消息头(与传统相同)
config_mask4 字节u32 小端位掩码,标记哪些配置值存在
recent_blockhash32 字节生命周期指定符
num_instructions1 字节固定宽度计数,最大 64
num_addresses1 字节固定宽度计数,最大 64(启用 SIMD-0596 后为 96)
addressesN x 32 字节账户地址,全部内联 — 无查找表引用
config_values0–20 字节每个已置位的掩码位对应一个值,按位顺序排列(见下文)
instruction_headersN x 4 字节每条指令:program_id_index(u8)、num_accounts(u8)、data_len(u16 小端)
instruction_payloads可变长度每条指令:账户索引,然后是 instruction data
signaturesN x 64 字节位于末尾,无长度前缀 — 数量来自 header

编写解码器时,有两处与 legacy 和 v0 的结构差异值得注意。计数使用固定宽度的 u8 字段而非 compact-u16,且指令被拆分为两段:先是所有固定大小的 header,然后是所有可变长度的 payload,而非每条指令连续存放。

v1 中的账户:无地址查找表

v1 有意移除了对 ALT 的支持。64 个原始地址为 2,048 字节,而 SIMD-0596 之后允许的 96 个地址为 3,072 字节,二者都轻松处于 4,096 字节的限制之内,因此每个地址都像在 legacy 中一样以内联方式存放。如果你的应用依赖查找表,迁移到 v1 就意味着需要将这些地址内联。

v1 中的资源限制:交易配置

配置由一个 u32 位掩码加上每个已置位字段的固定宽度值组成:

位字段宽度备注
0–1优先费u64lamport 总量 — 两位同时置位
2计算单元限制u32
3已加载账户数据大小限制u32
4请求的堆大小u32

未知位将被拒绝。由于消息已签名,无法识别的配置字段不能被静默丢弃。

优先费是 lamport 总量,而非单价

在 legacy 和 v0 中,优先费通过 SetComputeUnitPrice 以每计算单元的 micro-lamport 数设置,再乘以计算单元限制。在 v1 中,优先费是以 lamport 为单位的绝对总量 — 无需乘法,无需取整。请勿将按 CU 计算的算术逻辑迁移过来。总费用公式其余部分不变:(signatures × lamports_per_signature) + priority_fee。

ComputeBudget 指令在 v1 中为空操作

v1 交易不会拒绝 ComputeBudget 指令 — 它会忽略这些指令的配置作用。这些指令仍会作为成功的空操作执行,消耗 150 个计算单元和 64 个指令 slot 中的一个,但对预算没有任何影响。构建 v1 交易时请将其去除,读取 v1 交易时也无需扫描这些指令:相关值已存放在消息配置中。

配置字段必须显式设置

对于发送方而言,最重要的行为变化是:与 legacy 和 v0 不同,v1 交易必须显式设置计算单元限制和已加载账户数据大小限制,否则交易将失败。

未设置字段legacy / v0v1省略时的后果
计算单元限制每条指令 200k,最大 1.4M0 CU立即失败,计算单元耗尽
已加载账户数据大小64 MiB0 字节第一个加载的账户触发 MaxLoadedAccountsDataSizeExceeded
优先费00—
堆大小32 KiB32 KiB—

推荐做法是:先以两个限制均设为最大值进行一次模拟,然后将返回的 unitsConsumed 和 loadedAccountsDataSize 写回配置中,并将数据大小向上取整到下一个 32 KiB 页边界以留出余量(区块费用模型按 32 KiB 页计费,因此低于下一个页边界的余量是免费的)。

构建 v1 交易

v1 消息的组装流程与 legacy 或 v0 消息相同。区别在于 setTransactionMessageConfig:资源限制是消息本身的属性,因此指令列表中只包含程序指令。

Kit
import {
appendTransactionMessageInstruction,
assertIsTransactionWithBlockhashLifetime,
createClient,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageConfig,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";
import { airdropPayer, payer } from "@solana/kit-plugin-signer";
import { getTransferSolInstruction } from "@solana-program/system";
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const client = await createClient()
.use(payer(sender))
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900",
}),
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
// A v1 message is assembled through the same pipeline as a legacy or v0 one.
// The difference is the config: a v1 transaction carries its resource limits in
// the message itself, so no ComputeBudget instructions are compiled in.
const transactionMessage = pipe(
createTransactionMessage({ version: 1 }),
(m) => setTransactionMessageFeePayerSigner(sender, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
(m) =>
appendTransactionMessageInstruction(
getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: lamports(10_000_000n),
}),
m,
),
// The compute unit limit and loaded accounts data size limit both default to
// zero on v1, so a transaction that leaves them unset always fails. The
// priority fee is an absolute total in lamports, not a price per compute
// unit.
(m) =>
setTransactionMessageConfig(
{
computeUnitLimit: 20_000,
loadedAccountsDataSizeLimit: 64 * 1024,
priorityFeeLamports: 5_000n,
},
m,
),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
assertIsTransactionWithBlockhashLifetime(signedTransaction);
await sendAndConfirmTransactionFactory({
rpc: client.rpc,
rpcSubscriptions: client.rpcSubscriptions,
})(signedTransaction, { commitment: "confirmed" });
const transactionSignature = getSignatureFromTransaction(signedTransaction);
console.log("Transaction Signature:", transactionSignature);
Console
Click to execute the code.

Kit 还为每个配置字段提供了单独的设置函数 — setTransactionMessagePriorityFeeLamports、setTransactionMessageComputeUnitLimit、setTransactionMessageLoadedAccountsDataSizeLimit 和 setTransactionMessageHeapSize — 用于逐项设置。

通过模拟确定限制大小

与其猜测这两个必需的限制,不如实际测量。模拟运行时会将两个限制都提高到运行时允许的最大值,因此不会因为所测量的资源不足而失败。返回的数值是单次模拟运行的精确消耗,因此在依赖该数值之前请预留一定余量。

Kit
import {
appendTransactionMessageInstruction,
assertIsTransactionWithBlockhashLifetime,
createClient,
createTransactionMessage,
estimateAndSetResourceLimitsFactory,
estimateResourceLimitsFactory,
fillTransactionMessageProvisoryResourceLimits,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
setTransactionMessagePriorityFeeLamports,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";
import { airdropPayer, payer } from "@solana/kit-plugin-signer";
import { getTransferSolInstruction } from "@solana-program/system";
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const client = await createClient()
.use(payer(sender))
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900",
}),
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
// The priority fee is a pricing decision that simulation cannot measure, so it
// is set directly. The two resource limits are measured below instead of
// guessed. The provisory fill writes placeholder limits so the message
// simulates at the same size it will be sent at.
const draftMessage = pipe(
createTransactionMessage({ version: 1 }),
(m) => setTransactionMessageFeePayerSigner(sender, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
(m) =>
appendTransactionMessageInstruction(
getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: lamports(10_000_000n),
}),
m,
),
(m) => setTransactionMessagePriorityFeeLamports(5_000n, m),
fillTransactionMessageProvisoryResourceLimits,
);
// Simulation runs with both limits raised to the runtime maximum, so it cannot
// fail for want of the resources it is measuring.
const estimateAndSetResourceLimits = estimateAndSetResourceLimitsFactory(
estimateResourceLimitsFactory({ rpc: client.rpc }),
);
const transactionMessage = await estimateAndSetResourceLimits(draftMessage, {
commitment: "confirmed",
});
console.log("Compute unit limit:", transactionMessage.config?.computeUnitLimit);
console.log(
"Loaded accounts data size limit:",
transactionMessage.config?.loadedAccountsDataSizeLimit,
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
assertIsTransactionWithBlockhashLifetime(signedTransaction);
await sendAndConfirmTransactionFactory({
rpc: client.rpc,
rpcSubscriptions: client.rpcSubscriptions,
})(signedTransaction, { commitment: "confirmed" });
// The estimate is the exact cost of one simulated run, with nothing to spare.
// Add a margin before relying on it in production.
console.log(
"Transaction Signature:",
getSignatureFromTransaction(signedTransaction),
);
Console
Click to execute the code.

为 v1 做准备

v1 改变的是交易的读取方式,而不仅仅是发送方式。任何在调用 getTransaction 或 getBlock 时未选择启用的客户端,在处理 v1 交易时都会失败:

  • 向 getTransaction 和 getBlock 传入 maxSupportedTransactionVersion: 1 — JSON 整数 1,而非字符串 "1"。传入 0 与省略该参数的效果相同,同样会在 v1 交易上失败,因此在 v0 推出期间已更新的代码库仍需修改此值。
  • getTransaction 在处理 v1 交易时将返回错误 -32015,且一笔 v1 交易将导致整个 getBlock 响应失败并返回相同错误 — 不会有部分结果。
  • blockSubscribe 将发出 block: null 并停止推进,因此将其解读为空区块的消费者将从第一个 v1 slot 起静默落后。
  • getSignaturesForAddress 从不检查交易体,因此 v1 签名可正常列出。
  • 对于 v1 交易,已选择启用的响应会在消息中包含一个 transactionConfig 对象(legacy 和 v0 交易则完全不包含该对象)。通过扫描 ComputeBudget 指令来推导优先费用或计算限制的数据管道,对每一笔 v1 交易都会静默地报告为零。
  • 在客户端解码交易时请使用 encoding: 'base64',对于超过 1,232 字节的交易在使用 sendTransaction/simulateTransaction 时也应如此 — base58 编码仍受旧大小限制约束。
  • 客户端库支持需要较新版本:@solana/kit 8.0+、Agave 4.2.x 世代的 Rust crate,或 web3.js v3。web3.js v1 从 1.99.0 起支持读取 v1,但无法构建或发送 v1 交易。

读取 v1 交易

选择启用需要传入 JSON 整数 1。对返回的字节进行解码即可得到版本和配置,资源限制就存放在配置中 — 扫描指令列表查找 ComputeBudget 指令将一无所获。

Kit
import {
decompileTransactionMessage,
getBase64Encoder,
getCompiledTransactionMessageDecoder,
getTransactionDecoder,
} from "@solana/kit";
// Reading a v1 transaction requires opting in. The integer 1 is mandatory:
// omitting the parameter, or passing 0, fails on a v1 transaction rather than
// returning it in a degraded form.
const fetched = await client.rpc
.getTransaction(transactionSignature, {
commitment: "confirmed",
encoding: "base64",
maxSupportedTransactionVersion: 1,
})
.send();
if (fetched === null) {
throw new Error("the transaction just sent was not found");
}
// The compiled message stores the config as a bitmask plus a positional list,
// so it is decompiled to address the fields by name. Decompiling fetches no
// accounts, because v1 does not support address lookup tables.
const wireTransaction = getBase64Encoder().encode(fetched.transaction[0]);
const transaction = getTransactionDecoder().decode(wireTransaction);
const compiledMessage = getCompiledTransactionMessageDecoder().decode(
transaction.messageBytes,
);
const message = decompileTransactionMessage(compiledMessage);
console.log("Version:", message.version);
if ("config" in message) {
// Scanning the instruction list for ComputeBudget instructions finds nothing
// here — on v1 the limits live in the config.
console.log("Compute unit limit:", message.config?.computeUnitLimit);
console.log("Priority fee (lamports):", message.config?.priorityFeeLamports);
}
Console
Click to execute the code.

如需完整的迁移指南 — 包括客户端库支持、流式传输(Geyser/gRPC)版本检测以及模拟行为 — 请参阅交易格式 v1 升级页面。TypeScript、Rust、Python 和 Go 的可运行示例位于solana-foundation/transaction-v1-examples。

Is this page helpful?

©️ 2026 Solana 基金会版权所有