本文介绍了Solana程序如何通过指令自省(instruction introspection)读取同一交易中其他指令的内容。
指令内省(Instruction introspection)使 Solana 程序能够在同一笔交易中读取除自身以外的指令。
通常,一个程序只能读取以自身为目标的指令。Solana 运行时将每条指令路由到指令中指定的程序。
一个 Solana 交易可以包含多条指令,每条指令都以不同的程序为目标。例如,程序 A 可能收到指令 Ax,而程序 B 在同一交易中收到指令 Bx。通过内省,程序 B 可以读取指令 Ax 和 Bx 的内容。
例如,假设你希望确保与你的 DeFi 程序的任何交互都必须首先在同一交易中向你的 treasury 转账 0.5 SOL。你可以通过内省指令来执行此规则,如果所需的 0.5 SOL 转账指令未包含在与你的程序交互的指令之前,则拒绝整个交易。
在本文中,我们将学习内省的工作原理以及如何在你的 Solana 程序中实现它。
在我们了解指令内省之前,让我们详细回顾一下交易和指令。
Solana 交易是一个具有两个字段的结构体:一个消息(message)和签署它的签名(signatures)。该消息包含一个按顺序执行的指令(instructions)数组。

下面的代码(直接来自 Solana SDK)显示了一个交易的结构体表示:
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}
交易消息(transaction message)包含指令列表,以及指令将共同访问的所有帐户密钥的并集。它还包含运行时需要的一些附加数据,例如最近的区块哈希(recent block hash)和消息头(message header)。
pub struct Message {
pub instructions: Vec<Instruction>,
pub account_keys: Vec<Address>,
pub recent_blockhash: Hash,
pub header: MessageHeader,
}
以下是每个组件的详细分解:
指令(Instructions):每个指令是对链上程序的一次调用。一个指令包含三个组件:
指令结构体(Instruction struct)
以下是 Instruction 结构体定义,来自 GitHub 上的 Solana 源代码:
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
pub data: Vec<u8>,
}
pub struct AccountMeta {
/// An account's public key.
pub pubkey: Pubkey,
// True if the instruction requires a signature for this pubkey
/// in the transaction's signatures list.
pub is_signer: bool,
/// True if the account data or metadata may be mutated during program execution.
pub is_writable: bool,
}
指令使用的每个帐户都由 AccountMeta 类型表示,它存储帐户的公钥以及签名者和可写标志。
交易和指令之间关系的总结
为了将所有内容放在一起,下图显示了一个交易、一个消息和指令之间的关系。
一个 交易(Transaction) 包含一个签名列表和一个消息。一个 消息(Message) 包含一个头部、帐户密钥列表、一个最近的区块哈希和一个指令列表。一个 指令(Instruction) 包含一个程序 ID,它使用的帐户(这索引到消息结构体中的 帐户密钥(account keys) 列表)和指令数据。

让我们首先检查 Solana Sysvar 帐户,来讨论内省是如何工作的。
一个 sysvar 是一个特殊的只读帐户,它包含由 Solana 运行时维护的动态更新的数据,并将内部网络状态暴露给程序。我们实际上是从这个帐户读取数据——我们没有对一个程序进行 CPI 调用。
我们在本系列的前一篇文章中讨论了不同类型的 Sysvar。要了解更多关于它们的信息,请阅读文章“Solana Sysvars Explained”。
指令内省使用指令 Sysvar 帐户来访问当前交易的序列化指令向量(program_id、帐户和数据)。例如,在一个包含多个指令的交易中,一个程序可以读取和分析任何指令,而不仅仅是当前指令。
这个动画展示了一个指令内省场景,其中,当指令 1 正在执行时,程序可以读取指令 2 和指令 3 的内容。
与 Solana 中的常规帐户不同,指令 Sysvar 帐户不存储数据;它仅在交易的生命周期内填充,并在执行完成后清除。
指令 Sysvar 帐户地址是 Sysvar1nstructions1111111111111111111111111。它包含当前交易中所有指令的序列化列表。每个条目都包含程序 ID、帐户和指令数据,就像我们之前看到的那样。以下是每个反序列化指令的 Rust 结构体,与之前重现的相同:
pub struct Instruction {
/// Pubkey of the program that executes this instruction
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation
pub data: Vec<u8>,
}
Solana Rust SDK 提供了几个辅助函数来访问指令 sysvar 帐户中的序列化指令。但是,SDK 没有提供返回所有指令的单个函数;相反,它只提供反序列化特定索引处的单个指令的函数。
你仍然可以手动读取和反序列化 sysvar 帐户中的指令列表,但是这样做容易出错,因此,应该使用 SDK 反序列化指令。
以下是 Solana Rust SDK 为内省提供的两个关键辅助函数:
load_current_index_checked – 程序可以使用此辅助函数来了解它们在交易列表中的索引,然后通过它们的相对位置查找另一个指令。load_instruction_at_checked – 加载特定索引处的指令,并将其反序列化为一个 Instruction 结构体。一旦你使用 load_current_index_checked 函数获得了当前索引,你就可以使用此函数来内省较早或较晚的指令。我们将在本文后面的章节中看到如何做到这一点。首先,要理解这些辅助函数是如何工作的,让我们看看指令 sysvar 帐户的布局。它被组织成三个区域:
头部指定了交易中的指令数量和指令偏移量(指向指令开始的位置)。下图显示了一个具有 2 个指令的头部,因此有两个偏移量:一个从内存位置 6 开始,另一个从内存位置 20 开始。

指令区域从偏移量指示的字节位置开始(下图中的红色框只是偏移量的视觉标记,而不是实际的内存位置)。从该位置开始,它包含帐户元数据、程序 ID、指令数据的长度,最后是指令数据本身。如果我们有多个指令,则每个指令都会重复此结构。
![一个图表,显示了来自 Solana 指令 Sysvar 帐户的指令区域的示例布局。它使用一个红色框来视觉上指示偏移量指向的位置,标记指令的开始。然后布局显示了帐户信息字段(包含 num_accounts、account.meta 和 account.pubkey),然后是 program_id、data_len 和数据本身...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!