Solana 技术训练营 2026

2026年01月09日更新 196 人订阅
课程介绍
C4: Solana 程序开发入门: 简单链上数据存储程序扩展为可交易的代币程序
C6:Anchor 入门: SPL 与 Token 2022

Pinocchio 入门-账户、指令及错误

什么是Pinocchio

虽然大多数 Solana 开发者依赖 Anchor,但有很多充分的理由选择不使用它编写程序。也许您需要对每个账户字段进行更精细的控制,或者您追求极致的性能,亦或是您只是想避免使用宏。

在没有像 Anchor 这样的框架支持下编写 Solana 程序被称为 原生开发。这更具挑战性,但在本节课程中,您将学习如何使用 Pinocchio 从零开始构建一个 Solana 程序;这是一个轻量级的库,可以让您跳过外部框架,完全掌控代码的每一个字节。

Pinocchio 是一个极简的 Rust 库,它允许您在不引入重量级 solana-program crate 的情况下编写 Solana 程序。它通过将传入的交易负载(账户、指令数据等所有内容)视为单个字节切片,并通过零拷贝技术就地读取。

主要优势

极简设计带来了三大优势:

  • 更少的计算单元。没有额外的反序列化或内存拷贝。
  • 更小的二进制文件。更精简的代码路径意味着更轻量的 .so 链上程序。
  • 零依赖拖累。没有需要更新(或可能破坏)的外部 crate。

该项目由 Febo 在 Anza 发起,并得到了 Solana 生态系统和 Blueshift 团队的核心贡献,项目地址在 这里

除了核心 crate,您还会发现 pinocchio-system 和 pinocchio-token,它们为 Solana 的原生 System 和 SPL-Token 程序提供了零拷贝辅助工具和 CPI 实用程序。

原生开发

原生开发可能听起来令人望而生畏,但这正是本章节存在的原因。在本章节结束时,您将了解跨越程序边界的每一个字节,以及如何保持您的逻辑紧凑、安全和高效。

Anchor 使用 过程宏和派生宏 来简化处理账户、instruction data 和错误处理的样板代码,这些是构建 Solana 程序的核心。

原生开发意味着我们不再享有这种便利,我们需要:

  • 为不同的指令创建我们自己的 Discriminator 和 Entrypoint
  • 创建我们自己的账户、指令和反序列化逻辑
  • 实现所有 Anchor 之前为我们处理的安全检查

注意:目前还没有用于构建 Pinocchio 程序的“框架”。因此,我们将基于我们的经验,介绍我们认为是编写 Pinocchio 程序的最佳方法。

Entrypoint

在 Anchor 中,#[program] 宏隐藏了许多底层逻辑。它在底层为每个指令和账户构建了一个 8 字节的 Discriminator(从 0.31 版本开始支持自定义大小)。

image.png

原生程序通常更加精简。单字节的 Discriminator(值范围为 0x01…0xFF)足以支持最多 255 个指令,这对于大多数用例来说已经足够。如果需要更多,可以切换到双字节变体,扩展到 65,535 种可能的变体。

entrypoint! 宏是程序执行的起点。它提供了三个原始切片:

  • program_id:已部署程序的公钥
  • accounts:指令中传递的所有账户
  • instruction_data:包含 Discriminator 和用户提供数据的不透明字节数组

这意味着在 entrypoint 之后,我们可以创建一个模式,通过适当的处理器执行所有不同的指令,我们将其称为 process_instruction。以下是它的典型样式:

entrypoint!(process_instruction);

fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {    
    match instruction_data.split_first() {
        Some((Instruction1::DISCRIMINATOR, data)) => Instruction1::try_from((data, accounts))?.process(),
        Some((Instruction2::DISCRIMINATOR, _)) => Instruction2::try_from(accounts)?.process(),
        _ => Err(ProgramError::InvalidInstructionData)
    }
}

在幕后,这个处理器:

  1. 使用 split_first() 提取判别字节
  2. 使用 match 确定要实例化的指令结构
  3. 每个指令的 try_from 实现会验证并反序列化其输入
  4. 调用 process() 执行业务逻辑

solana-program 和 pinocchio 的区别

主要的区别和优化在于 entrypoint() 的行为方式。

  • 标准的 Solana 入口点使用传统的序列化模式,运行时会预先反序列化输入数据,在内存中创建拥有的数据结构。这种方法广泛使用 Borsh 序列化,在反序列化过程中复制数据,并为结构化数据类型分配内存。
  • Pinocchio 入口点通过直接从输入字节数组中读取数据而不进行复制,实现零拷贝操作。该框架定义了引用原始数据的零拷贝类型,消除了序列化/反序列化的开销,并通过直接内存访问避免了抽象层。

账户和指令

由于我们没有宏,并且为了保持程序的精简和高效,我们希望避免使用宏,因此每个指令数据字节和账户都必须手动验证。

为了使这个过程更有条理,我们使用了一种模式,该模式提供了类似 Anchor 的易用性,但没有使用宏,从而通过实现 Rust 的 TryFrom trait,使实际的 process() 方法几乎没有样板代码。

TryFrom Trait

TryFrom 是 Rust 标准转换家族的一部分。与 From 假设转换不会失败不同,TryFrom 返回一个 Result,允许您及早暴露错误——非常适合链上验证。

该 trait 定义如下:

pub trait TryFrom<T>: Sized {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

在 Solana 程序中,我们实现 TryFrom 来将原始账户切片(以及在需要时的指令字节)转换为强类型结构,同时强制执行每个约束。

账户验证

我们通常在每个 TryFrom 实现中处理所有不需要双重借用(即在账户验证和可能的处理过程中同时借用)的特定检查。这使得所有指令逻辑发生的 process() 函数尽可能简洁。

我们从实现指令所需的账户结构开始,类似于 Anchor 的 Context

注意:与 Anchor 不同,在这个账户结构中,我们只包括在处理过程中需要使用的账户,并将指令中需要但不会使用的其余账户(例如 SystemProgram)标记为 _

对于类似 Vault 的内容,它看起来会像这样:

pub struct DepositAccounts<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
}

现在我们知道了在指令中需要使用哪些账户,我们可以使用 TryFrom 特性来反序列化并执行所有必要的检查:

impl<'a> TryFrom<&'a [AccountInfo]> for DepositAccounts<'a> {
    type Error = ProgramError;

    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        // 1. Destructure the slice
        let [owner, vault, _] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };

        // 2. Custom checks
        if !owner.is_signer() {
            return Err(ProgramError::InvalidAccountOwner);
        }

        if !vault.is_owned_by(&pinocchio_system::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }

        // 3. Return the validated struct
        Ok(Self { owner, vault })
    }
}

如您所见,在这个指令中,我们将使用 SystemProgram CPI 将 lamport 从所有者转移到金库,但我们不需要在指令本身中使用 SystemProgram。程序只需要包含在指令中,因此我们可以将其作为 _ 传递。

然后我们对账户执行自定义检查,类似于 Anchor 的 Signer 和 SystemAccount 检查,并返回验证后的结构。

指令验证

指令验证遵循与账户验证类似的模式。我们使用 TryFrom 特性来验证和反序列化指令数据为强类型结构,从而使 process() 中的业务逻辑保持简洁和专注。

我们首先定义一个结构体来表示我们的 instruction data:

pub struct DepositInstructionData {
    pub amount: u64,
}

然后我们实现 TryFrom 来验证 instruction data 并将其转换为我们的结构化类型。这包括:

  1. 验证数据长度是否与预期大小匹配
  2. 将字节切片转换为具体类型
  3. 执行任何必要的验证检查

以下是实现的样子:

impl<'a> TryFrom<&'a [u8]> for DepositInstructionData {
    type Error = ProgramError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        // 1. Verify the data length matches a u64 (8 bytes)
        if data.len() != core::mem::size_of::<u64>() {
            return Err(ProgramError::InvalidInstructionData);
        }

        // 2. Convert the byte slice to a u64
        let amount = u64::from_le_bytes(data.try_into().unwrap());

        // 3. Validate the amount (e.g., ensure it's not zero)
        if amount == 0 {
            return Err(ProgramError::InvalidInstructionData);
        }

        Ok(Self { amount })
    }
}

这种模式使我们能够:

  • 在 instruction data 进入业务逻辑之前进行验证
  • 将验证逻辑与核心功能分离
  • 在验证失败时提供清晰的错误信息
  • 在整个程序中保持类型安全性

账户

正如我们看到的,与 Anchor 不同,Pinocchio 的账户验证无法使用自动执行所有者、签名和标识符检查的账户类型。

在原生 Rust 中,我们需要手动执行这些验证。虽然这需要更多的细节关注,但实现起来相对简单:

// SignerAccount type
if !account.is_signer() {
    return Err(PinocchioError::NotSigner.into());
}

或者进行所有者检查:

// SystemAccount type
if !account.is_owned_by(&pinocchio_system::ID) {
    return Err(PinocchioError::InvalidOwner.into());
}

通过将所有验证封装在我们之前提到的 TryFrom 实现中,我们可以轻松识别缺失的检查并确保我们编写的是安全的代码。

然而,为每个指令编写这些检查可能会变得重复。为了解决这个问题,我们创建了一个 helper.rs 文件,该文件定义了类似于 Anchor 的类型,以简化这些验证。

通用接口和特性

对于我们的 helper.rs 文件,我们利用了 Rust 的两个基本概念:通用接口 和 特性

我们选择这种方法而不是基于宏的解决方案(如 Anchor 的)有几个关键原因:

  • 特性和接口提供了清晰、明确的代码,读者无需在脑海中“展开”宏即可理解
  • 编译器可以验证特性实现,从而实现更好的错误检测、类型推断、自动补全和重构工具
  • 特性允许通用实现,可以重复使用而无需代码重复,而过程宏会为每次使用生成重复代码
  • 这些特性可以打包成可重用的 crate,而宏生成的 API 通常仅限于定义它们的 crate

现在您已经了解了我们的设计决策,让我们来探索这些概念的语法和功能。

什么是特性和通用接口?

如果您熟悉其他编程语言,您可能会发现 traits 类似于“接口”;它们定义了一个契约,规定了某个类型必须实现哪些方法。

在 Rust 中,trait 充当一个蓝图,声明“任何实现此 trait 的类型必须提供这些特定的函数”。

以下是一个简单的示例:

// Define a Trait
pub trait AccountCheck {
    fn check(account: &AccountInfo) -> Result<(), ProgramError>;
}

// Define a Type
pub struct SignerAccount;

// Implement the trait for different Types
impl AccountCheck for SignerAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_signer() {
            return Err(PinocchioError::NotSigner.into());
        }
        Ok(())
    }
}

pub struct SystemAccount;

impl AccountCheck for SystemAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&pinocchio_system::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        Ok(())
    }
}

这里的妙处在于,任何实现了 AccountCheck 的账户类型都可以以相同的方式使用;我们可以对它们中的任何一个调用 .check(),并且每种类型都处理适合其自身的验证逻辑。

这就是我们所说的“通用接口”:不同的类型共享相同的方法签名。

现在让我们看看如何将其应用于账户安全检查:

签名者和系统账户

正如我们在之前的示例中看到的,SystemAccount 和 SignerAccount 检查非常简单,不需要任何额外的验证,因此我们将在 helper.rs 中添加以下内容:

pub trait AccountCheck {
    fn check(account: &AccountInfo) -> Result<(), ProgramError>;
}

pub struct SignerAccount;

impl AccountCheck for SignerAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_signer() {
            return Err(PinocchioError::NotSigner.into());
        }
        Ok(())
    }
}

pub struct SystemAccount;

impl AccountCheck for SystemAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&pinocchio_system::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        Ok(())
    }
}

这里我们只是检查账户是否是签名者,或者是否由系统程序拥有。请注意,这两个结构体都提供了相同的检查方法,为我们提供了前面提到的通用接口。

铸币账户和代币账户

现在事情变得更有趣了。我们从常规的 AccountCheck trait 开始,但我们还添加了其他特定的 traits,以提供类似于 Anchor 宏的额外辅助功能,例如 init 和 init_if_needed

pub struct MintAccount;

impl AccountCheck for MintAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&pinocchio_token::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        if account.data_len() != pinocchio_token::state::Mint::LEN {
            return Err(PinocchioError::InvalidAccountData.into());
        }

        Ok(())
    }
}

对于 init 和 init_if_needed 的功能,我们创建了另一个名为 MintInit 的 trait,专门用于此目的,因为它需要独特的字段。然后我们使用 CreateAccount 和 InitializeMint2 CPI 来初始化 Mint 账户:

pub trait MintInit {
    fn init(account: &AccountInfo, payer: &AccountInfo, decimals: u8, mint_authority: &[u8; 32], freeze_authority: Option<&[u8; 32]>) -> ProgramResult;
    fn init_if_needed(account: &AccountInfo, payer: &AccountInfo, decimals: u8, mint_authority: &[u8; 32], freeze_authority: Option<&[u8; 32]>) -> ProgramResult;
}

impl MintInit for MintAccount {
    fn init(account: &AccountInfo, payer: &AccountInfo, decimals: u8, mint_authority: &[u8; 32], freeze_authority: Option<&[u8; 32]>) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::Mint::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::Mint::LEN as u64,
            owner: &pinocchio_token::ID,
        }.invoke()?;

        InitializeMint2 {
            mint: account,
            decimals,
            mint_authority,
            freeze_authority,
        }.invoke()
    }

    fn init_if_needed(account: &AccountInfo, payer: &AccountInfo, decimals: u8, mint_authority: &[u8; 32], freeze_authority: Option<&[u8; 32]>) -> ProgramResult {
        match Self::check(account) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, payer, decimals, mint_authority, freeze_authority),
        }
    }
}

然后我们对 TokenAccount 执行完全相同的操作:

pub struct TokenAccount;

impl AccountCheck for TokenAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&pinocchio_token::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        if account.data_len().ne(&pinocchio_token::state::TokenAccount::LEN) {
            return Err(PinocchioError::InvalidAccountData.into());
        }

        Ok(())
    }
}

pub trait AccountInit {
    fn init(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &[u8; 32]) -> ProgramResult;
    fn init_if_needed(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &[u8; 32]) -> ProgramResult;
}

impl AccountInit for TokenAccount {
    fn init(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &[u8; 32]) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::TokenAccount::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::TokenAccount::LEN as u64,
            owner: &pinocchio_token::ID,
        }.invoke()?;

        // Initialize the Token Account
        InitializeAccount3 {
            account,
            mint,
            owner,
        }.invoke()
    }

    fn init_if_needed(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &[u8; 32]) -> ProgramResult {
        match Self::check(account) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, mint, payer, owner),
        }
    }
}

Token2022

您可能已经注意到,对于传统的 SPL Token Program,我们仅对 Mint 和 TokenAccount 进行了长度检查。这种方法之所以有效,是因为当您只有两种固定大小的账户类型时,可以仅通过它们的长度来区分它们。

对于 Token2022,这种简单的方法不起作用。当直接将 token extensions 添加到 Mint 数据时,其大小可能会增长并可能超过 TokenAccount 的大小。这意味着我们不能仅依赖大小来区分账户类型。

对于 Token2022,我们可以通过两种方式区分 Mint 和 TokenAccount

  • 通过大小:类似于传统的 Token Program(当账户具有标准大小时)
  • 通过 discriminator:一个位于位置 165 的特殊字节(比传统的 TokenAccount 大一个字节,以避免冲突)

这导致了修改后的验证检查:

// TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
pub const TOKEN_2022_PROGRAM_ID: [u8; 32] = [
    0x06, 0xdd, 0xf6, 0xe1, 0xee, 0x75, 0x8f, 0xde, 0x18, 0x42, 0x5d, 0xbc, 0xe4, 0x6c, 0xcd, 0xda,
    0xb6, 0x1a, 0xfc, 0x4d, 0x83, 0xb9, 0x0d, 0x27, 0xfe, 0xbd, 0xf9, 0x28, 0xd8, 0xa1, 0x8b, 0xfc,
];

const TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET: usize = 165;
pub const TOKEN_2022_MINT_DISCRIMINATOR: u8 = 0x01;
pub const TOKEN_2022_TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 0x02;

pub struct Mint2022Account;

impl AccountCheck for Mint2022Account {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        let data = account.try_borrow_data()?;

        if data.len().ne(&pinocchio_token::state::Mint::LEN) {
            if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                return Err(PinocchioError::InvalidAccountData.into());
            }
            if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET].ne(&TOKEN_2022_MINT_DISCRIMINATOR) {
                return Err(PinocchioError::InvalidAccountData.into());
            }
        }

        Ok(())
    }
}

impl MintInit for Mint2022Account {
    fn init(account: &AccountInfo, payer: &AccountInfo, decimals: u8, mint_authority: &[u8; 32], freeze_authority: Option<&[u8; 32]>) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::Mint::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::Mint::LEN as u64,
            owner: &TOKEN_2022_PROGRAM_ID,
        }.invoke()?;

        InitializeMint2 {
            mint: account,
            decimals,
            mint_authority,
            freeze_authority,
        }.invoke()
    }

    fn init_if_needed(account: &AccountInfo, payer: &AccountInfo, decimals: u8, mint_authority: &[u8; 32], freeze_authority: Option<&[u8; 32]>) -> ProgramResult {
        match Self::check(account) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, payer, decimals, mint_authority, freeze_authority),
        }
    }
}
pub struct TokenAccount2022Account;

impl AccountCheck for TokenAccount2022Account {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        let data = account.try_borrow_data()?;

        if data.len().ne(&pinocchio_token::state::TokenAccount::LEN) {
            if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                return Err(PinocchioError::InvalidAccountData.into());
            }
            if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET].ne(&TOKEN_2022_TOKEN_ACCOUNT_DISCRIMINATOR) {
                return Err(PinocchioError::InvalidAccountData.into());
            }
        }

        Ok(())
    }
}

impl AccountInit for TokenAccount2022Account {
    fn init(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &[u8; 32]) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(pinocchio_token::state::TokenAccount::LEN);

        // Fund the account with the required lamports
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: pinocchio_token::state::TokenAccount::LEN as u64,
            owner: &TOKEN_2022_PROGRAM_ID,
        }.invoke()?;

        InitializeAccount3 {
            account,
            mint,
            owner,
        }.invoke()
    }

    fn init_if_needed(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &[u8; 32]) -> ProgramResult {
        match Self::check(account) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, mint, payer, owner),
        }
    }
}

Token 接口

为了让 Token2022 和传统的 Token Programs 更容易一起使用,而无需在它们之间进行区分,我们创建了一个遵循相同基本原则的辅助工具:

pub struct MintInterface;

impl AccountCheck for MintInterface {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            if !account.is_owned_by(&pinocchio_token::ID) {
                return Err(PinocchioError::InvalidOwner.into());
            } else {
                if account.data_len().ne(&pinocchio_token::state::Mint::LEN) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        } else {
            let data = account.try_borrow_data()?;

            if data.len().ne(&pinocchio_token::state::Mint::LEN) {
                if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
                if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET].ne(&TOKEN_2022_MINT_DISCRIMINATOR) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        }

        Ok(())
    }
}

pub struct TokenAccountInterface;

impl AccountCheck for TokenAccountInterface {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&TOKEN_2022_PROGRAM_ID) {
            if !account.is_owned_by(&pinocchio_token::ID) {
                return Err(PinocchioError::InvalidOwner.into());
            } else {
                if account.data_len().ne(&pinocchio_token::state::TokenAccount::LEN) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        } else {
            let data = account.try_borrow_data()?;

            if data.len().ne(&pinocchio_token::state::TokenAccount::LEN) {
                if data.len().le(&TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET) {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
                if data[TOKEN_2022_ACCOUNT_DISCRIMINATOR_OFFSET]
                    .ne(&TOKEN_2022_TOKEN_ACCOUNT_DISCRIMINATOR)
                {
                    return Err(PinocchioError::InvalidAccountData.into());
                }
            }
        }

        Ok(())
    }
}

关联 Token 账户

我们可以为 Associated Token Program 创建一些检查。这些检查与普通的 Token Program 检查非常相似,但它们包括一个额外的派生检查,以确保账户被正确派生。

pub struct AssociatedTokenAccount;

impl AssociatedTokenAccountCheck for AssociatedTokenAccount {
    fn check(
        account: &AccountInfo,
        authority: &AccountInfo,
        mint: &AccountInfo,
        token_program: &AccountInfo,
    ) -> Result<(), ProgramError> {
        TokenAccount::check(account)?;

        if find_program_address(
            &[authority.key(), token_program.key(), mint.key()],
            &pinocchio_associated_token_account::ID,
        )
        .0
        .ne(account.key())
        {
            return Err(PinocchioError::InvalidAddress.into());
        }

        Ok(())
    }
}

impl AssociatedTokenAccountInit for AssociatedTokenAccount {
    fn init(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &AccountInfo, system_program: &AccountInfo, token_program: &AccountInfo) -> ProgramResult {
        Create {
            funding_account: payer,
            account,
            wallet: owner,
            mint,
            system_program,
            token_program,
        }.invoke()
    }

    fn init_if_needed(account: &AccountInfo, mint: &AccountInfo, payer: &AccountInfo, owner: &AccountInfo, system_program: &AccountInfo, token_program: &AccountInfo) -> ProgramResult {
        match Self::check(account, payer, mint) {
            Ok(_) => Ok(()),
            Err(_) => Self::init(account, mint, payer, owner, system_program, token_program),
        }
    }
}

Program Accounts

最后,我们为 program account 实现了检查和辅助功能,包括 init 和 close 功能。

您可能会在我们的 close 实现中注意到一些有趣的地方:我们将账户的大小调整到几乎为零,仅保留第一个字节并将其设置为 255。这是一种安全措施,用于防止重新初始化攻击。

重新初始化攻击是指攻击者试图通过重新初始化已关闭的账户并注入恶意数据来重新利用该账户。通过将第一个字节设置为 255 并将账户缩小到几乎为零的大小,我们可以确保该账户在未来不会被误认为任何有效的账户类型。这是 Solana 程序中常见的安全模式。

pub struct ProgramAccount;

impl AccountCheck for ProgramAccount {
    fn check(account: &AccountInfo) -> Result<(), ProgramError> {
        if !account.is_owned_by(&crate::ID) {
            return Err(PinocchioError::InvalidOwner.into());
        }

        if account.data_len().ne(&crate::state::ProgramAccount::LEN) {
            return Err(PinocchioError::InvalidAccountData.into());
        }

        Ok(())
    }
}

pub trait ProgramAccountInit {
    fn init<'a, T: Sized>(
        payer: &AccountInfo,
        account: &AccountInfo,
        seeds: &[Seed<'a>],
        space: usize,
    ) -> ProgramResult;
}

impl ProgramAccountInit for ProgramAccount {
    fn init<'a, T: Sized>(
        payer: &AccountInfo,
        account: &AccountInfo,
        seeds: &[Seed<'a>],
        space: usize,
    ) -> ProgramResult {
        // Get required lamports for rent
        let lamports = Rent::get()?.minimum_balance(space);

        // Create signer with seeds slice
        let signer = [Signer::from(seeds)];

        // Create the account
        CreateAccount {
            from: payer,
            to: account,
            lamports,
            space: space as u64,
            owner: &crate::ID,
        }
        .invoke_signed(&signer)?;

        Ok(())
    }
}

pub trait AccountClose {
    fn close(account: &AccountInfo, destination: &AccountInfo) -> ProgramResult;
}

impl AccountClose for ProgramAccount {
    fn close(account: &AccountInfo, destination: &AccountInfo) -> ProgramResult {
        {
            let mut data = account.try_borrow_mut_data()?;
            data[0] = 0xff;
        }

        *destination.try_borrow_mut_lamports()? += *account.try_borrow_lamports()?;
        account.realloc(1, true)?;
        account.close()
    }
}

优化账户数据访问

虽然我们可以实现一个通用的 Trait 来从 ProgramAccount 中读取数据,但创建特定的 readers 和 setters 来仅访问所需字段,而不是反序列化整个账户,会更高效。这种方法可以减少计算开销和 gas 成本。

以下是实现此优化的示例:

#[repr(C)]
pub struct AccountExample {
    pub seed: u64,
    pub bump: [u8; 1]
}

impl AccountExample {
    /// The length of the `AccountExample` account data.
    pub const LEN: usize = size_of::<u64>() + size_of::<[u8; 1]>();

    /// Return an `AccountExample` from the given account info.
    ///
    /// This method performs owner and length validation on `AccountInfo`, safe borrowing
    /// the account data.
    #[inline]
    pub fn from_account_info(account_info: &AccountInfo) -> Result<Ref<AccountExample>, ProgramError> {
        if account_info.data_len() != Self::LEN {
            return Err(ProgramError::InvalidAccountData);
        }
        if account_info.owner() != &crate::ID {
            return Err(ProgramError::InvalidAccountOwner);
        }
        Ok(Ref::map(account_info.try_borrow_data()?, |data| unsafe {
            Self::from_bytes(data)
        }))
    }

    /// Return a `AccountExample` from the given account info.
    ///
    /// This method performs owner and length validation on `AccountInfo`, but does not
    /// perform the borrow check.
    ///
    /// # Safety
    ///
    /// The caller must ensure that it is safe to borrow the account data – e.g., there are
    /// no mutable borrows of the account data.
    #[inline]
    pub unsafe fn from_account_info_unchecked(
        account_info: &AccountInfo,
    ) -> Result<&Self, ProgramError> {
        if account_info.data_len() != Self::LEN {
            return Err(ProgramError::InvalidAccountData);
        }
        if account_info.owner() != &crate::ID {
            return Err(ProgramError::InvalidAccountOwner);
        }
        Ok(Self::from_bytes(account_info.borrow_data_unchecked()))
    }

    /// Return a `AccountExample` from the given bytes.
    ///
    /// # Safety
    ///
    /// The caller must ensure that `bytes` contains a valid representation of `AccountExample`.
    #[inline(always)]
    pub unsafe fn from_bytes(bytes: &[u8]) -> &Self {
        &*(bytes.as_ptr() as *const AccountExample)
    }
}

此实现提供了三种访问账户数据的方法:

  1. from_account_info:一种安全的方法,执行完整的验证和借用检查
  2. from_account_info_unchecked:一种不安全的方法,跳过借用检查但仍验证账户属性
  3. from_bytes:一种用于直接字节访问的不安全方法,由其他方法内部使用

我们还可以实现一个 set_inner 辅助工具来更新账户数据:

#[inline(always)]
pub fn set_inner(&mut self, seed: u64, bump: [u8;1]) {
    self.seed = seed;
    self.bump = bump;
}

为了实现更精细的控制和效率,我们可以使用固定偏移量实现特定的 getter 和 setter:

const SEED_OFFSET: usize = 0;

#[inline(always)]
pub fn check_program_id_and_discriminator(
    account_info: &AccountInfo,
) -> Result<(), ProgramError> {
    // Check Program ID
    if unsafe { account_info.owner().ne(&crate::ID) } {
        return Err(ProgramError::IncorrectProgramId);
    }

    // Check length
    if account_info.data_len().ne(Self::LEN) {
        return Err(ProgramError::InvalidAccountData);
    }

    Ok(())
}

#[inline(always)]
pub fn get_seeds(account_info: &AccountInfo) -> Result<u64, ProgramError> {
    Self::check_program_id_and_discriminator(account_info);

    let data = account_info.try_borrow_data()?;
    Ok(u64::from_le_bytes(data[SEED_OFFSET..SEED_OFFSET + size_of::<u64>()].try_into().unwrap()))
}

#[inline(always)]
pub unsafe fn get_seeds_unchecked(account_info: &AccountInfo) -> Result<u64, ProgramError> {
    let data = account_info.try_borrow_data()?;
    Ok(u64::from_le_bytes(data[SEED_OFFSET..SEED_OFFSET + size_of::<u64>()].try_into().unwrap()))
}

#[inline(always)]
pub fn set_seeds(account_info: &AccountInfo, seed: u64) -> Result<(), ProgramError> {
    Self::check_program_id_and_discriminator(account_info);

    let data = account_info.try_borrow_mut_data()?;
    Ok(unsafe {
        *(data.as_mut_ptr() as *mut [u8; 8]) = seed.to_le_bytes();
    })
}

#[inline(always)]
pub fn set_seeds_unchecked(account_info: &AccountInfo, seed: u64) -> Result<(), ProgramError> {
    let data = account_info.try_borrow_mut_data()?;
    Ok(unsafe {
        *(data.as_mut_ptr() as *mut [u8; 8]) = seed.to_le_bytes();
    })
}

此实现提供:

  1. 一个常量 SEED_OFFSET 用于跟踪 seed 数据的位置
  2. 一个验证函数 check_program_id_and_discriminator
  3. 安全和不安全版本的 getter 和 setter
  4. 内联优化以提高性能

不安全版本跳过验证检查,以在您确定账户有效时获得更好的性能,而安全版本在访问数据之前确保进行适当的验证。

指令

正如我们所看到的,使用 TryFrom trait 可以将验证与业务逻辑清晰地分离,从而提高可维护性和安全性。

Instruction Structure

当需要处理逻辑时,我们可以创建如下结构:

pub struct Deposit<'a> {
    pub accounts: DepositAccounts<'a>,
    pub instruction_datas: DepositInstructionData,
}

此结构定义了在逻辑处理期间可访问的数据。然后,我们可以使用 try_from 函数对其进行反序列化,该函数可以在 lib.rs 文件中找到:

impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Deposit<'a> {
    type Error = ProgramError;

    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = DepositAccounts::try_from(accounts)?;
        let instruction_datas = DepositInstructionData::try_from(data)?;

        Ok(Self {
            accounts,
            instruction_datas,
        })
    }
}

此包装器提供了三个关键优势:

  1. 它接受原始输入(字节和账户)
  2. 它将验证委托给各个 TryFrom 实现
  3. 它返回一个完全类型化、完全验证的 Deposit 结构

然后我们可以像这样实现处理逻辑:

impl<'a> Deposit<'a> {
    pub const DISCRIMINATOR: &'a u8 = &0;

    pub fn process(&self) -> ProgramResult {
        // deposit logic
        Ok(())
    }
}
  • DISCRIMINATOR 是我们在入口点中用于模式匹配的字节
  • process() 方法仅包含业务逻辑,因为所有验证检查都已完成

结果是什么?我们获得了 Anchor 风格的易用性,同时具备完全原生的所有优势:明确、可预测且快速。

Cross Program Invocation

如前所述,Pinocchio 提供了像 pinocchio-system 和 pinocchio-token 这样的辅助 crate,简化了对原生程序的跨程序调用(CPI)。

这些辅助结构和方法取代了我们之前使用的 Anchor 的 CpiContext 方法:

Transfer {
    from: self.accounts.owner,
    to: self.accounts.vault,
    lamports: self.instruction_datas.amount,
}
.invoke()?;

Transfer 结构(来自 pinocchio-system)封装了 System Program 所需的所有字段,而 .invoke() 执行了 CPI。无需上下文构建器或额外的样板代码。

当调用者必须是一个程序派生地址(PDA)时,Pinocchio 保持了同样简洁的 API:

let seeds = [
    Seed::from(b"vault"),
    Seed::from(self.accounts.owner.key().as_ref()),
    Seed::from(&[bump]),
];
let signers = [Signer::from(&seeds)];

Transfer {
    from: self.accounts.vault,
    to: self.accounts.owner,
    lamports: self.accounts.vault.lamports(),
}
.invoke_signed(&signers)?;

操作方式如下:

  1. Seeds 创建一个与 PDA 派生相匹配的 Seed 对象数组
  2. Signer 将这些种子封装在一个 Signer 辅助工具中
  3. invoke_signed 执行 CPI,传递签名者数组以授权转账

结果是什么?一个干净的、一流的接口,适用于常规和签名的 CPI:无需宏,也没有隐藏的魔法。

Multiple Instruction Structure

通常,您会希望在多个指令中重用相同的账户结构和验证逻辑,例如在更新不同的配置字段时。

与其为每个指令创建一个单独的区分符,不如使用一种通过数据负载大小区分指令的模式。

操作方式如下:

我们为所有相关的配置更新使用一个单一的指令区分符。具体的指令由传入数据的长度决定。

之后,在您的处理器中,匹配 self.data.len()。每种指令类型都有一个唯一的数据大小,因此您可以相应地分派到正确的处理程序。

它看起来像这样:

pub struct UpdateConfig<'a> {
    pub accounts: UpdateConfigAccounts<'a>,
    pub data: &'a [u8],
}

impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for UpdateConfig<'a> {
    type Error = ProgramError;

    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = UpdateConfigAccounts::try_from(accounts)?;

        // Return the initialized struct
        Ok(Self { accounts, data })
    }
}

impl<'a> UpdateConfig<'a> {
    pub const DISCRIMINATOR: &'a u8 = &4;

    pub fn process(&mut self) -> ProgramResult {
        match self.data.len() {
            len if len == size_of::<UpdateConfigStatusInstructionData>() => {
                self.process_update_status()
            }
            len if len == size_of::<UpdateConfigFeeInstructionData>() => self.process_update_fee(),
            len if len == size_of::<UpdateConfigAuthorityInstructionData>() => {
                self.process_update_authority()
            }
            _ => Err(ProgramError::InvalidInstructionData),
        }
    }

    //..
}

请注意,我们将指令数据的反序列化推迟到知道要调用哪个处理程序之后。这避免了不必要的解析,并保持了入口逻辑的简洁。

然后,每个处理程序可以反序列化其特定的数据类型并执行更新:

pub fn process_update_authority(&mut self) -> ProgramResult {
    let instruction_data = UpdateConfigAuthorityInstructionData::try_from(self.data)?;

    let mut data = self.accounts.config.try_borrow_mut_data()?;
    let config = Config::load_mut_unchecked(&mut data)?;

    unsafe { config.set_authority_unchecked(instruction_data.authority) }?;

    Ok(())
}

pub fn process_update_fee(&mut self) -> ProgramResult {
    let instruction_data = UpdateConfigFeeInstructionData::try_from(self.data)?;

    let mut data = self.accounts.config.try_borrow_mut_data()?;
    let config = Config::load_mut_unchecked(&mut data)?;

    unsafe { config.set_fee_unchecked(instruction_data.fee) }?;

    Ok(())
}

pub fn process_update_status(&mut self) -> ProgramResult {
    let instruction_data = UpdateConfigStatusInstructionData::try_from(self.data)?;

    let mut data = self.accounts.config.try_borrow_mut_data()?;
    let config = Config::load_mut_unchecked(&mut data)?;

    unsafe { config.set_state_unchecked(instruction_data.status) }?;

    Ok(())
}

这种方法允许您共享账户验证,并为多个相关指令使用单一入口点,从而减少样板代码并使代码库更易于维护。

通过对数据大小进行模式匹配,您可以高效地路由到正确的逻辑,而无需额外的区分符或复杂的解析。

错误

清晰且描述性强的错误类型对于使用 Pinocchio 构建的 Solana 程序至关重要。它们可以让调试更容易,并为与您的程序交互的用户和客户端提供有意义的反馈。

PinocchioError 枚举

在 Rust 中定义自定义错误类型时,您有多种选择,例如 thiserroranyhow 和 failure。对于 Pinocchio 程序,thiserror 是首选,因为:

  • 它允许您使用 #[error("...")] 属性为每个错误变体添加可读的消息注释。
  • 它会自动实现 core::error::Error 和 Display 特性,使您的错误易于打印和调试。
  • 所有错误消息和格式在编译时检查,降低了运行时问题的风险。
  • 最重要的是,thiserror 支持在禁用其默认功能时的 no_std 环境,这是 Pinocchio 程序的必要条件。

要在 Pinocchio 程序中使用 thiserror,请将其添加到您的 Cargo.toml 中,如下所示:

[dependencies]
thiserror = { version = "2.0", default-features = false }

以下是为您的 Pinocchio 程序定义自定义错误类型的方法:

use {
    num_derive::FromPrimitive,
    pinocchio::program_error::{ProgramError, ToStr},
    thiserror::Error,
};

#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum PinocchioError {
    // 0
    /// Lamport balance below rent-exempt threshold.
    #[error("Lamport balance below rent-exempt threshold")]
    NotRentExempt,
}

每个变体都带有一条消息,当错误发生时会显示该消息。

要从 Solana 指令中返回您的自定义错误,请为 ProgramError 实现 From<PinocchioError>

impl From<PinocchioError> for ProgramError {
    fn from(e: PinocchioError) -> Self {
        ProgramError::Custom(e as u32)
    }
}

这使您可以使用 ? 操作符并无缝返回您的自定义错误。

从原始值反序列化错误

如果您需要将原始错误代码(例如来自日志或跨程序调用的代码)转换回您的错误枚举,请实现 TryFrom<u32>

impl TryFrom<u32> for PinocchioError {
    type Error = ProgramError;
    fn try_from(error: u32) -> Result<Self, Self::Error> {
        match error {
            0 => Ok(PinocchioError::NotRentExempt),
            _ => Err(ProgramError::InvalidArgument),
        }
    }
}

⚠️ 这是可选的,但对于高级错误处理和测试非常有用。

可读性强的错误信息

为了记录和调试,您可能希望提供错误的字符串表示。实现 ToStr trait 可以实现这一点:

impl ToStr for PinocchioError {
    fn to_str<E>(&self) -> &'static str {
        match self {
            PinocchioError::NotRentExempt => "Error: Lamport balance below rent-exempt threshold",
        }
    }
}

⚠️ 此步骤也是可选的,但它可以使错误报告对用户更加友好。

点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论