新消息:Helius 收购 Light Protocol
Solana 交易版本:Legacy、v0 与 v1
博客/基础知识

Solana 交易版本:Legacy、v0 与 v1

研究员X 上的 Lostin
阅读需 6 分钟

Solana 交易格式简史

Solana 有三种交易格式:Legacy、v0 和 v1。总体而言,它们的用途相同:将签名、账户和指令打包,供验证者针对链上程序执行。

随着时间推移,变化的是线路格式,也就是这些信息如何序列化并通过网络传输。

随着 Solana 应用变得更加复杂,各种限制逐渐显现,因此引入了新的格式。同时,新格式保留了向后兼容性,让旧格式可以继续运行。

这三种格式都共存于网络中,开发者可以选择最适合当前交易的格式。

Legacy 交易

最初的格式称为 Legacy,因为它早于交易版本机制,不包含版本标识符。

其 1,232 字节的大小上限源于 Solana 最初的网络设计:交易大小需要适配 IPv6 的 1,280 字节最小 MTU,扣除网络开销后剩余 1,232 字节。

这对小型交易很有效,但随着应用增长,空间限制变得越来越严苛。直接包含在交易中的每个账户地址占用 32 字节,每个 Ed25519 签名还会占用 64 字节。

因此在实践中,账户较多的交易往往还没达到运行时的账户锁定上限,就已经耗尽字节额度。

v0 交易

首次缓解这一压力的尝试是 v0 交易,它于 2022 年 10 月在 mainnet-beta 上线。v0 没有提高 1,232 字节的上限,而是引入了地址查找表(ALT)。

ALT 在链上存储账户地址,使 v0 交易可以用单字节索引引用这些地址,而不必重复写入每个 32 字节公钥。这让交易能够在现有数据包大小限制内达到运行时的 64 个账户上限。

v0 是对 Legacy 格式的渐进式扩展:它增加了 0x80 版本前缀和地址查找部分,同时保留了相同的基本消息和指令编码。

版本机制的引入还建立了一套框架,使新的交易格式能够与 Legacy 交易共存,而非取代它们。

ALT 解决了迫在眉睫的账户大小问题,但也带来了新的复杂性。应用需要在链上创建和维护查找表,验证者必须在执行前解析其中的条目,RPC 服务和索引器则需要使用解析后的地址重建完整交易。

如果原始交易引用的查找表后来已被关闭,历史数据解码也会困难得多。因此,v0 虽然有效压缩了账户列表,却也在交易的线路表示中引入了外部链上依赖。

ALT 已被 Solana 开发者广泛采用。在引入 v1 交易前不久开展的研究发现,大约 62% 的 v0 交易至少引用了一个 ALT。

后加的优先费机制

Solana 也在 2022 年引入了优先费,时间早于 v0 交易在主网上启用,不过当时 v0 格式本身已经完成设计。Solana 没有重新设计任何一种交易格式,而是通过现有的 Compute Budget Program 添加费用配置。交易可以包含 SetComputeUnitLimit 和 SetComputeUnitPrice 等指令,让用户能够请求计算预算,并附加额外费用来提高调度优先级。这套方式同样适用于 Legacy 和 v0 交易,也避免了为每种格式单独设计费用机制。

这是一个务实且向后兼容的解决方案,但并不特别优雅。优先费和计算限制本质上属于交易级元数据,但 Legacy 和 v0 没有对应的专用字段,因此只能将它们作为程序调用后加到指令流中。这意味着交易必须引用 Compute Budget Program,配置会占用指令字节,验证者还必须在接收交易时检查这些指令,才能提取费用和资源设置。

因此,v0 并不是对 Solana 交易格式的全面重构。它的主要目标是通过地址查找表解决账户地址瓶颈,同时尽量保留 Legacy 消息结构。由于 Compute Budget 指令已经提供了兼容方式,可让两种格式都支持优先费,因此没有太多理由进一步扩大 v0 的范围。

更大的交易容量

随着时间推移,Solana 将交易接收机制从 UDP 迁移到 QUIC,后者的数据流不再局限于单个 MTU 大小的载荷。因此,最初的 1,232 字节上限不再是传输层要求。2025 年提出了两项 SIMD,以引入新的 v1 格式。SIMD-0296 将 v1 交易的最大大小提高到 4,096 字节,约为此前上限的 3.3 倍;SIMD-0385 则定义了一种全新的线路格式,围绕新增空间和更高效的验证者接收流程进行设计。

新增空间让 v1 可以彻底移除 ALT。64 个 32 字节地址最多需要 2,048 字节,因此完整账户列表可以直接内联到交易中。

Solana Foundation 的分析表明,将大多数现有 v0 交易升级并内联所有地址时,大小增幅较为温和:50% 的交易增加不到 420 字节,90% 增加不到 1,400 字节。即使交易大量使用 ALT,在 v1 的 4,096 字节上限内仍有充足余量。

更重要的是,v1 并不只是容量更大的 v0 交易。它大幅重组了线路格式:签名移到末尾,资源配置从 Compute Budget 指令移到交易配置部分,可变长度的指令载荷与固定宽度的头部相互分离。这些变化让验证者能够以更低成本、更简单地解析交易并确定优先级。

更大的 4,096 字节上限还支持了仅靠地址压缩无法实现的工作负载。零知识证明、嵌套多重签名、未截断的 Winternitz 签名、BLS 签名和其他密码学操作,可能需要数百乃至数千字节的指令数据。例如,Token-2022 机密余额现在可以构建为单笔原子化 v1 交易,而不必拆分为多笔交易。

值得注意的是,初始 v1 格式并未提高 Solana 的计算、签名、指令数量或 64 个账户的限制;它主要消除了大小瓶颈,并重新设计了交易在线路层面的表示方式。

由 Anza 提出的 SIMD-0596计划将 v1 的账户锁定上限从 64 提高到 96,涵盖交易引用的所有账户,包括签名者、程序 ID、可写账户和只读账户。截至撰写本文时,该提案仍在审核中。Legacy 和 v0 交易的上限将继续保持为 64。

格式规范与对比

限制Legacyv0v1
最大大小1,232 字节1,232 字节4,096 字节
账户地址约 32 个,受大小限制64 个,通过查找表64 个,内联
地址查找表不支持支持不支持
重复地址允许允许拒绝
前缀无0x800x81
优先费SetComputeUnitPrice 指令,每 CU 以 micro-lamports 计SetComputeUnitPrice 指令,每 CU 以 micro-lamports 计config.priorityFeeLamports,lamports 总额
计算单元限制SetComputeUnitLimit 指令SetComputeUnitLimit 指令config.computeUnitLimit
堆大小RequestHeapFrame 指令RequestHeapFrame 指令config.heapSize
已加载账户上限SetLoadedAccountsDataSizeLimit 指令SetLoadedAccountsDataSizeLimit 指令config.loadedAccountsDataSizeLimit

Legacy 交易规范

序列化后的 Legacy 交易以 compact-u16 格式的签名数量开头,后接 64 字节的 Ed25519 签名。

随后是消息,其中包含三字节头部、带紧凑长度前缀的账户列表、32 字节的近期区块哈希,以及带紧凑长度前缀的已编译指令数组。

Legacy 交易没有版本前缀。

代码
NumSignatures (compact-u16)

Signatures [[u8; 64]]
  -- Length = NumSignatures

MessageHeader (u8, u8, u8)
  -- (NumRequiredSignatures,
      NumReadonlySignedAccounts,
      NumReadonlyUnsignedAccounts)

NumAccountKeys (compact-u16)

AccountKeys [[u8; 32]]
  -- Length = NumAccountKeys

RecentBlockhash [u8; 32]

NumInstructions (compact-u16)

Instructions [CompiledInstruction]
  -- Length = NumInstructions

CompiledInstruction:
  ProgramIdIndex (u8)

  NumInstructionAccounts (compact-u16)

  InstructionAccountIndexes [u8]
    -- Length = NumInstructionAccounts

  InstructionDataLength (compact-u16)

  InstructionData [u8]
    -- Length = InstructionDataLength

一个关键特征是,每条 CompiledInstruction 都会完整序列化,然后才开始下一条指令。

账户索引数组和数据数组的长度可变,其大小由 compact-u16 前缀定义。

v0 交易规范

v0 交易保留了几乎全部 Legacy 线路格式。交易仍以签名数量和签名开头,但消息以版本前缀 0x80 开始。

随后,消息采用与 Legacy 相同的三字节头部、静态账户列表、区块哈希和已编译指令格式,并在末尾附加地址查找表部分。

这让 v0 交易能够通过 ALT 加载更多账户,同时仍符合 1,232 字节的交易上限。

代码
NumSignatures (compact-u16)

Signatures [[u8; 64]]
  -- Length = NumSignatures

VersionByte (u8)
  -- 0x80

MessageHeader (u8, u8, u8)

NumStaticAccountKeys (compact-u16)

StaticAccountKeys [[u8; 32]]
  -- Length = NumStaticAccountKeys

RecentBlockhash [u8; 32]

NumInstructions (compact-u16)

Instructions [CompiledInstruction]
  -- Same encoding as legacy

NumAddressTableLookups (compact-u16)

AddressTableLookups [MessageAddressTableLookup]
  -- Length = NumAddressTableLookups

MessageAddressTableLookup:
  AccountKey [u8; 32]
    -- Address of the lookup table account

  NumWritableIndexes (compact-u16)

  WritableIndexes [u8]
    -- Indices into the lookup table

  NumReadonlyIndexes (compact-u16)

  ReadonlyIndexes [u8]
    -- Indices into the lookup table

区分静态账户密钥和已加载地址十分重要。只有静态密钥会直接序列化到消息中;通过 ALT 引用的地址会在运行时解析,并附加到实际账户列表中。

对于使用 ALT 的 v0 交易,验证者必须先从 Bank 和 AccountsDB 加载每个被引用的查找表,验证该账户是有效的地址查找表,对其状态进行反序列化,将请求的索引解析为完整的 32 字节地址,然后把这些地址与交易的静态账户密钥合并。只有完成这一步解析后,验证者才能获得确定读写锁以及与其他交易冲突情况所需的完整账户列表。

v1 交易规范

序列化后的 v1 交易以版本字节 0x81 开头。随后依次是三字节的 Legacy 风格消息头、32 位交易配置掩码、32 字节生命周期说明符、指令和地址数量、完整的 32 字节地址数组、配置值、固定大小的指令头、连续的指令载荷,最后是签名。

代码
VersionByte (u8)
  -- 0x81

LegacyHeader (u8, u8, u8)

TransactionConfigMask (u32)
  -- Bitmask describing which configuration values are present

LifetimeSpecifier [u8; 32]

NumInstructions (u8)

NumAddresses (u8)

Addresses [[u8; 32]]
  -- Length = NumAddresses

ConfigValues [[u8; 4]]
  -- Length = popcount(TransactionConfigMask)
  -- Multi-word values, such as priority fee, consume multiple entries

InstructionHeaders [(u8, u8, u16)]
  -- Length = NumInstructions
  -- (ProgramAccountIndex,
      NumInstructionAccounts,
      NumInstructionDataBytes)

InstructionPayloads [InstructionPayload]
  -- Length = NumInstructions

InstructionPayload:
  InstructionAccountIndexes [u8]
    -- Length = NumInstructionAccounts

  InstructionData [u8]
    -- Length = NumInstructionDataBytes

Signatures [[u8; 64]]
  -- Length = LegacyHeader.NumRequiredSignatures

因此,v1 在多个根本方面不同于早期格式。版本标识符位于第 0 字节,签名移到末尾且不再需要自己的长度前缀,账户和指令数量改为固定宽度的 u8 值,资源配置直接编码在交易中,固定大小的指令头与可变长度载荷相互分离。与 Legacy 和 v0 不同,v1 不使用地址查找表。

v1 交易使用交易头中的字段取代 Compute Budget 程序配置指令。初始 TransactionConfigMask 可以声明交易的:

  • 以 lamports 计的优先费总额
  • 计算单元限制
  • 已加载账户数据大小限制
  • 请求的堆大小

每个置位的比特都标识一个随附的四字节配置值,其中 64 位优先费占用两个掩码位置。该掩码旨在支持未来交易版本的扩展。

交易示例

为了对比 Solana 的各种交易格式,我们来看一个最简单且实用的示例:将 SOL 从一个地址转移到另一个地址。

示例交易只有一个签名者(即发送方),并调用一次 System Program,将 1,000 lamports 转给接收方。它引用三个地址:发送方、接收方和 System Program。

Legacy 和 v0 交易

Legacy 和 v0 交易使用几乎相同的顶层布局。两者都以签名数量开头,紧接着是签名本身,之后才是交易消息。对于只有一个签名者的转账,第一个字节是 01,表示一个签名,后接发送方的 64 字节 Ed25519 签名。

消息包含三字节头部、账户地址、近期区块哈希(生命周期说明符)和已编译指令。

发送方是地址 0,接收方是地址 1,System Program 是地址 2。转账指令引用索引 2 处的程序,并将账户索引 00 01 传给该程序。

Legacy 和 v0 格式仅有细微差异:

  • Legacy 消息直接以三字节消息头开头,而 v0 消息则以版本字节 0x80 开头,后接消息头。
  • v0 还会在指令之后附加地址查找表部分。这个简单示例没有使用查找表,因此该部分只有一个 00 字节,表示查找次数为零。

因此,示例 SOL 转账使用 Legacy 交易格式时为 215 字节,使用 v0 时为 217 字节。v0 为版本前缀增加了 1 字节,并为空查找表数组增加了 1 字节。交易的其余部分完全相同。

发送方只对消息签名。在 Legacy 示例中,这意味着签名覆盖第 65–214 字节;在 v0 中,则覆盖第 65–216 字节,并从 0x80 消息版本前缀开始。

下表详细列出了两个地址之间 SOL 转账示例采用 v0 交易格式时的字节布局。

偏移量大小字段示例值说明
01 B签名数量01一个签名;compact-u16
1–6464 B签名 0<Ed25519 signature>发送方签名
651 B版本前缀80版本化消息,版本 0
66–683 B消息头01 00 011 个必要签名者、0 个只读已签名账户、1 个只读未签名账户
691 B静态账户数量03三个内联账户
70–10132 B账户 0<sender pubkey>发送方/费用支付方;可写签名者
102–13332 B账户 1<recipient pubkey>接收方;可写且未签名
134–16532 B账户 211111111111111111111111111111111System Program;只读且未签名
166–19732 B近期区块哈希<recent blockhash>交易生命周期
1981 B指令数量01一条指令
1991 B程序 ID 索引02System Program
2001 B指令账户数量02两个指令账户
201–2022 B账户索引00 01发送方、接收方
2031 B数据长度0C12 字节
204–2074 B转账判别值02 00 00 00SystemInstruction::Transfer
208–2158 BLamportsE8 03 00 00 00 00 00 001,000 lamports
2161 B地址表查找数量00不进行 ALT 查找
总计217 B

资源配置

在 Legacy 和 v0 交易中,资源配置以发送给 Compute Budget Program 的指令表示。常见指令包括 SetComputeUnitLimit 和 SetComputeUnitPrice。需要时也会使用 SetLoadedAccountsDataSizeLimit 和 RequestHeapFrame。

在这个最小交易示例中,这些指令并不存在,而是由运行时提供默认值。

未设置计算单元价格意味着没有优先费,而已加载账户数据限制默认为运行时上限。

验证者在接收交易时必须扫描指令列表以查找 Compute Budget 指令,这种低效正是更新 v1 格式的动机之一。在 v1 中,这些信息改为移至 TransactionConfigMask 和 ConfigValues。

首先,将交易级配置表示为指令本身就会产生固有开销。Legacy 和 v0 没有用于请求计算资源或设置优先费的专用字段,因此这些设置通过 Compute Budget Program 后加进去:交易必须引用其程序 ID,每项设置都会占用指令列表空间,而且程序调用本身不执行任何应用工作,却仍会消耗计算资源。

v1 改为在线路格式中为这些值提供原生位置,避免额外的指令开销,也让验证者能够以更低成本、更轻松地检查资源配置。

v1 交易

v1 交易不再以签名数量和签名开头,而是直接从版本字节 0x81 开始。

接下来是三字节头部,然后是新的四字节 TransactionConfigMask、生命周期说明符(通常为区块哈希,也可以是 nonce)、指令和地址数量、账户地址、配置值、指令,最后是签名。

偏移量大小字段示例值说明
01 B版本字节81v1 交易
1–33 BLegacy 头部01 00 011 个必要签名、0 个只读已签名账户、1 个只读未签名账户
4–74 B交易配置掩码0C 00 00 000x0000000C:第 2 位和第 3 位置位,指定两个 4 字节配置值
8–3932 B生命周期说明符<recent blockhash>近期区块哈希(或 nonce)
401 B指令数量011 条 System Program 指令
411 B地址数量03发送方、接收方、System Program
42–7332 B地址 0<sender pubkey>发送方、费用支付方、可写签名者
74–10532 B地址 1<recipient pubkey>接收方、可写未签名账户
106–13732 B地址 211111111111111111111111111111111System Program、只读、未签名
138–1414 B配置值:计算单元限制10 27 00 0010,000 CU,以小端序 u32 表示(对应掩码第 2 位)
142–1454 B配置值:已加载账户数据大小限制00 00 01 0065,536 字节/64 KiB,以小端序 u32 表示(对应掩码第 3 位)
146–1494 B指令:头部02 02 0C 00程序索引 2、2 个账户、12 字节数据
150–1512 B指令:账户索引00 01地址 0 = 发送方,地址 1 = 接收方
152–1554 B指令:转账判别值02 00 00 00SystemInstruction::Transfer
156–1638 B指令:Lamports例如 E8 03 00 00 00 00 00 001,000 lamports,以小端序 u64 表示
164–22764 B签名 0<Ed25519 signature>发送方对第 0–155 字节的签名
总计228 B

签名验证

签名验证是交易到达验证者后最先执行的操作之一。签名无效的交易需要在到达调度器之前被过滤并丢弃。将签名放在可变长度消息之后,看起来似乎会增加访问难度,但实际上并不会。

在执行成本高昂的 Ed25519 验证前,验证者只需确定交易各部分的起止位置。v1 格式旨在让这一过程成本更低且结果确定。

第一个字节 0x81 会立即将交易标识为 v1。随后的头部包含 num_required_signatures,因此验证者从一开始就知道应该找到多少个 64 字节签名。

随后,Agave 的 TransactionView 解析器会向前遍历交易并记录偏移量。它读取固定大小的字段,并使用 NumAddresses 跳过地址数组。

解析器使用配置掩码确定配置部分的大小,并读取每个 4 字节指令头,以了解对应指令载荷的大小。

解析器越过最后一个指令载荷后,会将当前位置记录为签名偏移量。从该位置开始,Agave 预期恰好存在 num_required_signatures × 64 字节。

该实现会存储一个 SignatureFrame,其中包含签名数量以及数据包中第一个签名的字节偏移量。交易的已签名消息,是从 v1 消息开头到所记录签名偏移量之间的字节范围。因此,验证者无需执行交易或加载账户,即可进行成本高昂的 Ed25519 验证。

v1 还禁止在签名数组后出现尾随字节。解析器到达签名后,剩余字节的大小可以根据签名数量明确确定。SIMD-0385要求每个必要签名者都必须恰好对应一个 64 字节签名,并且最终签名后不能有任何数据。

改进的指令解析

v1 交易的布局让指令更容易解析。在 Legacy 和 v0 交易中,如果要定位后面的指令,解析器必须遍历之前的指令,解码其长度,并跳过每个可变长度载荷。

**v1 将固定宽度的指令头与可变长度载荷分离。**每条指令首先提供一个四字节头,其中包含程序账户索引、指令账户数量和指令数据长度。这些头部集中排列在账户索引和数据载荷之前,从一开始就为每条指令提供紧凑描述。这降低了划分和跳过指令部分的成本,也有助于在不解析指令数据的情况下确定尾部签名数组的偏移量。

交易配置掩码

v1 格式的另一项重要新增内容是四字节 TransactionConfigMask。

Legacy 和 v0 交易通过发送给 Compute Budget Program 的指令配置资源。例如,一笔交易可能包含 SetComputeUnitLimit、SetComputeUnitPrice 或 SetLoadedAccountsDataSizeLimit 指令。若验证者需要了解交易的资源需求或优先级,就必须定位并解释这些指令。v1 交易将这些信息移出指令流,直接放入交易格式本身。

该掩码是一个 32 位小端序位域。每个置位的比特都对应交易后续 ConfigValues 部分中的一个四字节字:

  • 第 0 位和第 1 位:以 lamports 计的优先费总额(编码为 8 字节小端序 u64)
  • 第 2 位:计算单元限制(4 字节 u32)
  • 第 3 位:已加载账户数据大小限制(4 字节 u32)
  • 第 4 位:请求的堆大小(4 字节 u32)

注意:初始 v1 规范仅为第 0–4 位赋予了含义。其余位目前尚未分配,为未来的交易级配置字段预留空间。

该掩码相当于一种紧凑模式,既告诉解析器存在哪些字段,也说明应该读取多少字节。例如,这笔简单的 SOL 转账需要计算单元限制和已加载账户数据大小限制,但不需要优先费或自定义堆大小。

由于这两个比特已置位,交易的地址数组后会紧跟恰好两个 4 字节配置值。在本例中,它们请求 20,000 CU 的限制和 64 KiB 的已加载账户数据限制。

掩码告诉验证者,第一个 4 字节配置属于第 2 位(计算单元限制),第二个属于第 3 位(已加载账户数据限制)。

在 Legacy 和 v0 交易中,如果省略计算单元限制指令,交易会获得隐式计算预算。v1 不使用相同的默认值。未设置的计算单元限制会解析为零,未设置的已加载账户数据大小限制同样为零;只有堆保留 32 KiB 的默认值。因此,v1 交易必须明确请求所需资源。

将这些设置移入定义明确的配置区域后,验证者可以在接收交易时直接访问所需信息,不必搜索指令。这也意味着 Compute Budget Program 指令不再用于配置 v1 交易。如果包含这些指令,它们会被忽略并作为空操作指令处理,但仍会消耗计算单元。

这还会带来额外的空间优势。在 Legacy/v0 交易中,添加 Compute Budget 指令通常意味着账户列表中还需要包含 32 字节的 Compute Budget Program 地址,以及序列化后的指令本身。

总结

Solana 的三种交易格式反映了网络的演进。Legacy 奠定了最初的模型;v0 通过 ALT 对其进行扩展,以便在 1,232 字节上限内支持更大的账户集合;v1 则围绕更大的交易容量、更简单的解析、内联地址和原生资源配置,对线路格式进行了更根本的重新设计。

这三种格式目前都仍然有效,但每一种都代表了 Solana 发展的不同阶段。它们共同展现了交易格式如何随着网络应用、费用市场和验证者需求的演变而不断适应。

延伸阅读

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新

放大图片