文章围绕 Solana 交易的两层核心结构展开:Instruction 与 Message。

指令是程序可执行的最小逻辑单元。它由三个主要部分组成:
true。true。指令的 data 字段是一个字节序列,用于告诉程序要执行哪个具体函数,并包含该调用所需的参数。
{
"program_id": "11111111111111111111111111111111",
"accounts": [
{
"pubkey": "6uR7N6oDgE3vJXvM6Eh4xVHw2g7o7YhA7FJxC4pXcZtT",
"is_signer": true,
"is_writable": true
},
{
"pubkey": "3Nq8yVbGz7KpYw9sT6rF2LmHc4Qz5XvA1uJd8BvCzRy",
"is_signer": false,
"is_writable": true
}
],
"data": [2, 0, 0, 0, 128, 150, 152, 0, 0, 0, 0, 0]
}
这是 System Program,Solana 内置的程序,用于管理原生 SOL 转账、创建账户和分配所有权。每个验证节点默认都包含该程序。这告诉运行时:调用 System Program 来执行其某个功能。
6uR7…ZtT:发送方,必须签名且可被修改(将扣除 lamports)。3Nq8…zRy:接收方,由于将增加 lamports,因此需要可写,但不需要签名。每种数据变体都编码为 [variant_index:u32][variant_payload]。在本例中:
pub enum SystemInstruction {
CreateAccount { ... },
Assign { ... },
Transfer { lamports: u64 },
...
}
Transfer 变体位于索引 2,其负载是一个 u64(8 字节)。
[2, 0, 0, 0, 128, 150, 152, 0, 0, 0, 0, 0]
即:[02 00 00 00 | 80 96 98 00 00 00 00 00] // 4 + 8 = 12 字节
[0:4) 字节:类型为 u32,值为 2。在程序源代码中可以看到,Transfer 在 SystemInstruction 枚举中位于索引 2。[4:12) 字节:类型为 u64,值为 10,000,000 lamports。[2, 0, 0, 0] → discriminant = 2
[128, 150, 152, 0, 0, 0, 0, 0] → 字节转十六进制
[80 96 98 00 00 00 00 00] → 十六进制表示
[0x00000000989680] → 0x00989680 = 10,000,000 lamports (0.01 SOL)
Solana 上的 交易 (transaction) 不仅仅是一组指令;它还是一个由一个或多个账户签名的 消息 (message)。消息精确定义了将要执行的内容、涉及哪些账户,以及哪个最近的 Blockhash 将其锚定到链的当前状态。
Solana 签名的是消息,而不是单独对每条指令签名。这种设计使交易更加紧凑且易于验证:验证节点只需检查签名者是否授权了网络接收到的同一份消息字节。
一旦签名验证通过,运行时就会按顺序执行这些指令,并以原子方式提交所有更改——要么全部成功,要么整笔交易回滚。
最初,所有 Solana 交易都使用单一的消息格式,现在称为 Legacy 消息。它对于简单转账和小型程序来说效果很好,但为了支持并行执行,每笔交易都必须列出其将要读取或写入的 所有 账户。
由于每个账户地址占 32 字节,消息总大小会很快增长。Solana 为了适配网络的 MTU,强制设定了严格的上限(序列化交易约为 1232 字节)。这使 Legacy 交易最多只能容纳大约 35 个账户,对于复杂、可组合的 DeFi 协议而言就成了问题。
为了解决这一点,Solana 引入了 版本化交易 (versioned transactions),从 v0 消息 开始,并配套引入了一个新的链上程序:地址查找表 (Address Lookup Table, ALT) 程序。查找表允许开发者将一组地址存储在链上,之后通过较小的 1 字节索引来引用,而不必使用完整的 32 字节 Key。
pub struct Message {
pub header: MessageHeader,
pub account_keys: Vec<Address>,
pub recent_blockhash: Hash,
pub instructions: Vec<CompiledInstruction>,
}
pub struct MessageHeader {
pub num_required_signatures: u8,
pub num_readonly_signed_accounts: u8,
pub num_readonly_unsigned_accounts: u8,
}
pub struct CompiledInstruction {
pub program_id_index: u8,
pub accounts: Vec<u8>,
pub data: Vec<u8>,
}
注意:
#[serde(with = "short_vec")]属性对提升序列化效率非常重要。
Legacy 消息大约可以容纳 35 个账户(35 * 32 字节 = 1120 字节,占总 1232 字节中的一部分)。它包含固定的 Header、账户、Blockhash 和指令。
pub enum VersionedMessage {
Legacy(LegacyMessage),
V0(v0::Message),
}
pub struct Message {
pub header: MessageHeader,
pub account_keys: Vec<Pubkey>,
pub recent_blockhash: Hash,
pub instructions: Vec<CompiledInstruction>,
/// 用于加载额外账户的地址表查找列表
#[serde(with = "short_vec")]
pub address_table_lookups: Vec<MessageAddressTableLookup>,
}
pub struct MessageAddressTableLookup {
pub account_key: Pubkey,
#[serde(with = "short_vec")]
pub writable_indexes: Vec<u8>,
#[serde(with = "short_vec")]
pub readonly_indexes: Vec<u8>,
}
V0 引入了 地址查找表 (ALTs),使消息可以通过 1 字节索引 来引用大量账户,而不是内联 32 字节的 pubkey。
在运行时,验证节点会构建一个统一的 已解析账户列表 (resolved account list),指令会通过索引访问该列表:
resolved_keys = [
message.account_keys,
looked_up_writable_keys (from all ALTs, in order),
looked_up_readonly_keys (from all ALTs, in order)
]
每个 MessageAddressTableLookup 都指向一个特定的链上 ALT(account_key)。writable_indexes 和 readonly_indexes 是该 ALT 所存储地址中的 u8 索引。运行时会取出这些 pubkey,并将它们追加到 resolved_keys 中。
版本化交易在消息编码中使用 前导版本位:
v0 会增加少量额外开销,但在需要许多账户时,能显著减少内联 key 所占的字节数。
每笔交易增加的开销:
address_table_lookups 的长度节省的空间:每个通过查找获得的地址,都用 1 字节 索引替换了原本 32 字节 的内联 pubkey。
u8(0–255)。u8。account_keys 中,以便高效验证。account_keys 和 ALT 查找结果中被重复加载。每个 ALT 最多 256 个条目:writable_indexes 或 readonly_indexes 向量中的每个元素都是单字节(范围 0–255)。如果 writable_indexes = [0, 2, 4, 6],运行时就会从该表中加载第 0、2、4 和第 6 个地址。
short_vec 格式:Solana 对变长数组使用 short_vec 格式。其序列化形式为 [长度 (1-9 字节)][元素...]。对于较小的向量(长度 < 128),长度只占 1 个字节。
每个查找表 34 字节:一个 MessageAddressTableLookup 需要 32 字节用于 account_key,外加至少 2 字节用于两个索引向量的长度。
Header 会告诉运行时有多少账户必须签名,以及哪些账户是只读的。
account_keys 开头有多少个账户必须提供签名。如果该值为 2,则前两个 key 是签名者。该字段用于防止 重放攻击 (replay attacks),并设置 活性要求。Solana 不使用 nonce;相反,每笔交易都必须包含最近约 150 个区块(约 2 分钟)内的一个 Blockhash。
指令 (instruction) 定义了要执行什么(程序、账户和数据),而 消息 (message) 会将这些指令打包成一个用于签名和验证的原子单元。Legacy 消息适用于大多数场景,而 v0 消息 通过 地址查找表 (Address Lookup Tables),可以用紧凑的 1 字节索引来引用更多账户。
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码