Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 66 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 指令自省 Solana 中的 Ed25519 签名验证 Solana - Switchboard 预言机使用 原生Solana:程序入口与执行 原生 Solana :读取账户数据 原生 Solana :Borsh 序列化 原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用 原生 Solana :创建存储账户 (一) 原生 Solana:创建存储账户 二 原生 Solana: 函数分发 原生 Solana:关键安全检查 Rust 程序到 SBF 编译 sBPF 虚拟机和指令集介绍 跟踪 sBPF 指令执行和计算成本 Solana 程序执行与输入序列化 指令处理器和运行时设置 sBPF 内存布局和寄存器约定 使用 sBPF 汇编读取 Solana 指令输入 Solana 系统调用:sBPF 汇编中的日志记录

原生 Solana: 函数分发

本文详细介绍了Solana程序中的函数分发机制,解释了其在Native Rust程序中的重要性,并对比了与以太坊和Anchor框架的区别。文章深入阐述了Anchor如何通过8字节鉴别器实现函数分发,并提供了三种在原生Rust程序中实现分发的方法。最后,通过一个完整的Native Rust程序示例和TypeScript客户端,演示了如何使用简单字节方法进行函数分发。

原生 Solana: 函数分发

Solana 中的函数分发是根据指令数据中编码的特定标识符,将传入指令路由到适当的处理函数的过程。

在我们之前的原生 Rust Solana 教程中,我们将所有程序逻辑都放在 process_instruction 函数中。这对于只有一个指令的简单程序是可行的。然而,当一个程序支持多个指令时,入口点会充满解析逻辑、条件检查和处理代码。一个更清晰的方法是将逻辑移动到单独的函数中,并将每个指令路由到正确的处理程序。

与 Ethereum 不同,EVM 通过内置选择器将调用路由到正确的函数,Solana 程序必须自己路由指令。Anchor 通过生成 8 字节的 discriminator 并将其添加到指令数据的前面来解决这个问题。程序读取这个值并分发到正确的处理程序。我们将在下一节详细解释这一点。

在本教程中,我们将演示如何在原生 Rust Solana 程序中处理函数分发。

Anchor 如何处理函数分发

当你在 Anchor 中编写如下函数时:

#[program]
pub mod my_program {
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        // Initialize logic
        Ok(())
    }

    pub fn update_counter(ctx: Context<UpdateCounter>, new_value: u64) -> Result<()> {
        // Update logic
        Ok(())
    }

    pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
        // Close logic
        Ok(())
    }
}

Anchor 生成必要的代码,根据指令数据将指令路由到适当的函数。在底层,它执行三个主要步骤:

  1. 步骤 1 — Anchor 在每个指令前添加一个 8 字节的 discriminator。 这个 discriminator 是通过获取“global:”加上函数名的 SHA-256 哈希的前 8 字节创建的 (sha256("global:" + function_name))。这个唯一的标识符有助于将每个指令路由到正确的处理函数,这类似于 Ethereum,其中函数选择器是函数签名的 Keccak-256 哈希的前 4 字节。
let mutdiscriminator = [0u8; 8];
let preimage = format!("global:{}", ix_name);   // ix_name = 函数名
let hash = sha2::Sha256::digest(preimage.as_bytes());
discriminator.copy_from_slice(&hash[..8]);
  1. 步骤 2 — Anchor 在编译时生成一个 dispatcher。 它在 8 字节标记处分割指令数据,并使用 discriminator 通过 Rust 的 match 语句将其路由到适当的处理程序,就在指令处理器内部。
// Anchor 生成的概念性表示
pub fn process_instruction(
       program_id: &Pubkey,
       accounts: &[AccountInfo],
       instruction_data: &[u8],
) -> ProgramResult {
       // 分割出 8 字节的 discriminator
       let (discriminator, remaining_data) = instruction_data.split_at(8);

       // 匹配已知的指令 discriminator
       match discriminator {
           // 每个指令的 discriminator 都在编译时计算
           INITIALIZE_DISCRIMINATOR => initialize(program_id, accounts, remaining_data),
           UPDATE_COUNTER_DISCRIMINATOR => update_counter(program_id, accounts, remaining_data),
           _ => return Err(ProgramError::InvalidInstructionData),
       }
}
  1. 步骤 3 — Anchor 反序列化指令参数。 对于带有参数的指令,Anchor 将剩余的指令数据反序列化为预期的 Rust 类型。这意味着你的函数接收到它期望的参数,而不是原始字节。

有了这三个步骤,我们编写 Rust 函数,Anchor 为我们处理指令路由和所有指令数据解析。但由于我们使用的是原生 Rust,我们必须自己完成这些步骤。

在原生 Rust 程序中实现函数分发

我们将使用 Rust 构建一个原生 Solana 程序,它有三个函数:process_instruction 函数和两个指令处理程序。在 process_instruction 中,我们将检查指令数据的第一个字节,并使用 match 语句分发到适当的处理程序。第一个函数将简单地记录一条消息,以演示路由机制正确识别并调用了预期的处理程序。第二个函数将遍历 account 并解析指令数据字节。

一旦我们了解了路由的工作原理,我们之前在原生 Rust Solana 教程中介绍的所有其他概念,即 account 创建、Borsh serialization、cross-program invocation,都可以在你的处理函数内部以相同的方式应用。

我们可以通过多种方式实现函数分发。虽然没有单一的强制约定(程序可以自由选择自己的方法),但简单字节方法和 Borsh 序列化枚举在原生 Rust 程序中最常见,而 Anchor 使用其哈希方法。以下是主要方法:

  1. 简单字节方法: 我们为程序中的每个函数分配一个唯一的常量,例如 const INITIALIZE: u8 = 0const UPDATE: u8 = 1。当客户端调用我们的程序时,它将这些值中的一个放在指令数据的开头(字节位置 0)。然后我们的程序读取第一个字节,并将其与定义的常量进行比较,以确定客户端打算调用哪个函数,并相应地路由执行。
  2. Borsh 序列化枚举: 这种方法使用 Borsh 进行序列化。我们定义一个 Rust enum,其中包含每个指令的变体,派生 Borsh 序列化 trait,并让客户端发送序列化的 enum。在程序端,我们将指令数据反序列化回我们的 enum,并匹配变体。
  3. Anchor 风格哈希: 这种方法模仿了 Anchor 的内部工作方式。我们可以通过获取“global:”加上每个指令名的 SHA-256 哈希的前 8 字节来创建唯一的标识符。客户端以相同的方式计算此哈希并将其添加到指令数据的前面,然后我们的程序匹配这些预先计算的哈希常量

我们将使用简单字节方法作为我们的示例。

我们的程序将有三个主要功能:

  1. process_instruction: 指令处理器,接收所有指令并根据指令数据将其路由到适当的处理程序。
  2. say_hello: 一个简单的处理函数,用于记录问候消息。
  3. inspect_accounts: 此函数将从我们将构建的客户端接收字符串消息,并将其与程序 ID 和该客户端提供的 account 信息一起记录。

项目设置

首先,让我们设置项目结构来演示函数分发:

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论