理解 Solana - 第4部分:指令与消息

本文深入解析了 Solana 交易的核心机制:指令(Instructions)与消息(Messages)。文章详细介绍了指令的组成部分(程序 ID、账户元数据、指令数据),并对比了 Legacy 与 V0 消息格式。重点探讨了 V0 版本如何通过地址查找表(ALT)突破 1232 字节的 MTU 限制,实现更复杂的 DeFi 交互。此外,还分析了消息头部的签名逻辑及最近区块哈希在防重放攻击中的作用,揭示了 Solana 并行执行架构下的底层通信协议。

Before now, we explored Solana 的并行架构和账户模型——数据如何存储在链上、谁可以修改它,以及为什么 Sealevel 能够实现真正的并发。

但仅有账户本身还不能改变状态。Solana 上的每一次更新,从转移 token 到创建 PDA,都是通过由 instructions 组成的 transactions 来完成的。

在这篇文章中,我们会拆解从钱包点击到链上执行之间到底发生了什么:

  1. transaction 在底层是如何构成的。
  2. runtime 如何将 instructions 路由到 programs。
  3. accounts 如何被锁定以实现安全的并行执行。
  4. “signing”、“message” 和 “recent blockhash” 到底是什么意思。

到最后,你将理解 Solana 是如何逐步对 transactions 进行编码、验证和执行的,以及如何直接从代码中构建、模拟并发送它们。


Instructions

Instructions 是 Solana 上的核心执行单元。你可以把每个 instruction 想成一个由链上 program 暴露出来的函数调用。每个 program 都定义了自己的 instruction set,也就是它能够执行的具体操作。当你与网络交互时,你并不是直接调用 programs;而是把它们的一个或多个 instructions 打包进一个 transaction,签名后提交执行。

一个 instruction 由以下部分组成:

  • ProgramId
  • Accounts
  • Instruction specific data

ProgramId

包含 instruction 逻辑的 program 的链上 ID(address)。

Accounts

每个 instruction 都包含一个 AccountMeta 条目数组,这些元数据描述了它将读取或写入的每一个 account。通过显式列出这些 accounts,Solana 的 runtime 可以判断哪些 instructions 是彼此独立的,并在它们不会修改同一个 account 的前提下安全地并行执行。

  • is_signer: 如果该 account 必须签署 transaction,则设为 true
  • is_writable: 如果 instruction 会修改该 account 的数据,则设为 true
  • pubkey: 该 account 的 public key address。

Data

instruction 的 data 字段是一串 bytes,用于告诉 program 要执行哪个具体函数,并包含该调用所需的参数。

示例:简单的 0.01 SOL 转账

{
  "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)

Messages

Solana 上的一个 transaction 不仅仅是一组 instructions;它还是一个由一个或多个 accounts 签名的 message。这个 message 精确地定义了将要运行什么、涉及哪些 accounts,以及哪个 recent blockhash 将它锚定到链的当前状态。

Solana 签名的是整个 message,而不是对每个 instruction 单独签名。这种设计让 transactions 更紧凑且更易验证:validator 只需要检查签名者是否授权了网络收到的同一份 message bytes。

一旦 signatures 被验证,runtime 就会按顺序执行这些 instructions,并以原子方式提交所有更改——要么全部成功,要么整个 transaction 回滚。

  • message 是“应该发生什么”的规范记录。
  • signature 证明“谁批准了它”。

Legacy 和 Versioned Messages

最初,所有 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。

Legacy Message Structure

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>,
}

Versioned Messages (v0)

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>,
}

Address Lookup 的工作方式

在 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_idaccounts 就会指向这个合并后的列表。

v0 如何被识别: Versioned transactions 使用一个前导 version bit。如果第一个 byte 的最高位被设置,剩余 bits 就编码版本号(v0 = 0)。如果没有被设置,那就是 legacy message。

大小影响和限制

v0 增加的开销:

  • +1 byte: version tag
  • +1 byte: address_table_lookups length
  • 每个 lookup table +34 bytes(32 用于 table pubkey + 2 用于 lengths)
  • 每个 looked-up index +1 byte

节省的空间: 每个 looked-up address 用一个 1-byte index 替代一个 32-byte inline pubkey。

需要记住的规则:

  • 每个 ALT 最多 256 个 entries:Indexes 是 u8
  • 总共最多可以加载 256 个 unique accounts
  • Signers 不能来自 ALTs:所有 signer keys 都必须出现在 account_keys 中。
  • 不能重复:一个 account 不能被加载多次。
  • ALT 可用性:新的 entries 在一个 slot 之后才可使用(warm-up)。

MessageHeader

header 告诉 runtime 有多少 accounts 必须签名,以及哪些 accounts 是只读的。

  • num_required_signatures (u8)account_keys 中前面有多少个必须提供 signatures。
  • num_readonly_signed_accounts (u8):在 signers 中,有多少个是只读的。这可以防止意外修改 signer accounts。
  • num_readonly_unsigned_accounts (u8):在 non-signers 中,有多少个是只读的。

Recent Blockhash

这个字段用于防止 replay attacks,同时设置一个 liveness requirement。Solana 不使用 nonces;相反,每个 transaction 都必须包含一个来自最近约 150 个 blocks(≈ 2 分钟)内的 recent blockhash。

为什么需要它:

  • Anti-replay:旧的 transactions 会被拒绝。
  • Transaction expiration:确保 transactions 是“fresh”的。
  • Fork commitment:通过把 transaction 锚定到链历史中的特定位置,减少歧义。

Summary

一个 instruction 定义要运行什么(program、accounts 和 data),而一个 message 则把这些 instructions 打包成一个用于签名的原子单元。Legacy messages 适用于大多数场景,但 v0 messages 利用 Address Lookup Tables 通过紧凑的 1-byte indexes 引用更多 accounts。

  • 原文链接: andreyobruchkov1996.subs...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
andreyobruchkov1996
andreyobruchkov1996
江湖只有他的大名,没有他的介绍。