本文深入解析了 Solana 交易的核心机制:指令(Instructions)与消息(Messages)。文章详细介绍了指令的组成部分(程序 ID、账户元数据、指令数据),并对比了 Legacy 与 V0 消息格式。重点探讨了 V0 版本如何通过地址查找表(ALT)突破 1232 字节的 MTU 限制,实现更复杂的 DeFi 交互。此外,还分析了消息头部的签名逻辑及最近区块哈希在防重放攻击中的作用,揭示了 Solana 并行执行架构下的底层通信协议。
Before now, we explored Solana 的并行架构和账户模型——数据如何存储在链上、谁可以修改它,以及为什么 Sealevel 能够实现真正的并发。
但仅有账户本身还不能改变状态。Solana 上的每一次更新,从转移 token 到创建 PDA,都是通过由 instructions 组成的 transactions 来完成的。
在这篇文章中,我们会拆解从钱包点击到链上执行之间到底发生了什么:
到最后,你将理解 Solana 是如何逐步对 transactions 进行编码、验证和执行的,以及如何直接从代码中构建、模拟并发送它们。
Instructions 是 Solana 上的核心执行单元。你可以把每个 instruction 想成一个由链上 program 暴露出来的函数调用。每个 program 都定义了自己的 instruction set,也就是它能够执行的具体操作。当你与网络交互时,你并不是直接调用 programs;而是把它们的一个或多个 instructions 打包进一个 transaction,签名后提交执行。
一个 instruction 由以下部分组成:
包含 instruction 逻辑的 program 的链上 ID(address)。
每个 instruction 都包含一个 AccountMeta 条目数组,这些元数据描述了它将读取或写入的每一个 account。通过显式列出这些 accounts,Solana 的 runtime 可以判断哪些 instructions 是彼此独立的,并在它们不会修改同一个 account 的前提下安全地并行执行。

is_signer: 如果该 account 必须签署 transaction,则设为 true。is_writable: 如果 instruction 会修改该 account 的数据,则设为 true。pubkey: 该 account 的 public key address。instruction 的 data 字段是一串 bytes,用于告诉 program 要执行哪个具体函数,并包含该调用所需的参数。
{
"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]
}
program_id 这是 System Program,Solana 内置的 program,用于管理原生 SOL 转账、创建账户以及分配所有权。它告诉 runtime:调用 System Program 来执行它的某个函数。
accounts:
6uR7…ZtT: 发送方,必须签名且可被修改(lamports 会被扣除)。3Nq8…zRy: 接收方,可写,因为 lamports 会被增加,但不需要签名。data:
每个 data 变体都编码为 [variant_index:u32][variant_payload]。在本例中:
pub enum SystemInstruction {
CreateAccount { ... },
Assign { ... },
Transfer { lamports: u64 },
...
}
Transfer 变体的索引是 2,它的 payload 是一个 u64(8-bytes)。
[2, 0, 0, 0 | 128, 150, 152, 0, 0, 0, 0, 0] // 4 + 8 = 12 bytes
[0:4) bytes: Type u32, Value 2。在 program source code 中,Transfer 的索引是 2。[4:12) bytes: Type u64, Value 10,000,000 lamports。[2, 0, 0, 0] → discriminant = 2
[128, 150, 152, 0, 0, 0, 0, 0] → 0x00989680 = 10,000,000 lamports (0.01 SOL)
Solana 上的一个 transaction 不仅仅是一组 instructions;它还是一个由一个或多个 accounts 签名的 message。这个 message 精确地定义了将要运行什么、涉及哪些 accounts,以及哪个 recent blockhash 将它锚定到链的当前状态。
Solana 签名的是整个 message,而不是对每个 instruction 单独签名。这种设计让 transactions 更紧凑且更易验证:validator 只需要检查签名者是否授权了网络收到的同一份 message bytes。
一旦 signatures 被验证,runtime 就会按顺序执行这些 instructions,并以原子方式提交所有更改——要么全部成功,要么整个 transaction 回滚。
最初,所有 Solana transactions 都使用单一的 message 格式,现在称为 legacy message。它要求列出其读取或写入的 all accounts,以便进行并行执行。由于每个 account address 都占 32 bytes,总 message size 增长得很快。Solana 强制实施严格的上限(≈ 1232 bytes)以适配网络的 MTU,这将 legacy transactions 限制在大约 35 accounts。
为了解决这个问题,Solana 引入了 versioned transactions(从 v0 messages 开始)和 Address Lookup Tables (ALT)。lookup tables 允许开发者把一组 addresses 存在链上,并通过 1-byte indexes 来引用它们,而不是使用完整的 32-byte keys。
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>,
}
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>,
}
在 runtime 中,validator 会构造一个统一的 resolved account list:
resolved_keys = [
message.account_keys,
looked_up_writable_keys,
looked_up_readonly_keys
]
runtime 会从 ALTs 中取出 pubkeys,并将它们追加到 resolved_keys。然后 instructions 中的 program_id 和 accounts 就会指向这个合并后的列表。
v0 如何被识别: Versioned transactions 使用一个前导 version bit。如果第一个 byte 的最高位被设置,剩余 bits 就编码版本号(v0 = 0)。如果没有被设置,那就是 legacy message。
v0 增加的开销:
节省的空间: 每个 looked-up address 用一个 1-byte index 替代一个 32-byte inline pubkey。
需要记住的规则:
u8。account_keys 中。header 告诉 runtime 有多少 accounts 必须签名,以及哪些 accounts 是只读的。
account_keys 中前面有多少个必须提供 signatures。这个字段用于防止 replay attacks,同时设置一个 liveness requirement。Solana 不使用 nonces;相反,每个 transaction 都必须包含一个来自最近约 150 个 blocks(≈ 2 分钟)内的 recent blockhash。
为什么需要它:
一个 instruction 定义要运行什么(program、accounts 和 data),而一个 message 则把这些 instructions 打包成一个用于签名的原子单元。Legacy messages 适用于大多数场景,但 v0 messages 利用 Address Lookup Tables 通过紧凑的 1-byte indexes 引用更多 accounts。
- 原文链接: andreyobruchkov1996.subs...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码