Skip to main content

🐟 Anchor 框架

当进行本地构建时,我们会将程序分解为多个文件,每个文件负责特定的任务。但由于 Anchor 大大精简了代码量,我们现在可以学习如何将程序组织到单一文件的不同部分中 😎。

我们可以将所有内容整合到一个文件中,这得益于 Anchor 使用宏来抽象各种重复任务。只需在文件中放置一个宏,让 Anchor 替我们处理,无需编写大量的代码。这也意味着我们能将指令逻辑与账户验证和安全检查分开。

在我们继续之前,先快速回顾一下过去必须编写许多样板代码的那些无趣部分:

  • 账户验证
  • 安全检查
  • 序列化/反序列化

Anchor 使用一些 Rust 的巧妙技巧来解决所有这些问题✨。它被设计为处理许多常见的安全问题,使你能够构建更安全的程序!

🍱 Anchor Program 的结构

让我们一起深入了解 Anchor program 的结构。

// use this import to gain access to common anchor features
use anchor_lang::prelude::*;

// Program on-chain address
declare_id!("6bujjNgtKQtgWEu4XMAtoJgkCn5RoqxLobuA7ptZrL6y");

#[program]
pub mod program_module_name {
use super::*;

pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
ctx.accounts.account_name.data = instruction_data;
Ok(())
}
}

// validate incoming account for instructions
#[derive(Accounts)]
pub struct InitializeAccounts<'info> {
#[account(init, payer = user, space = 8 + 8]
pub account_name: Account<'info, AccountStruct>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}

// Define custom program account type
#[account]
pub struct AccountStruct {
data: u64,
}

这是一个相当简洁的程序 — 它初始化一个新账户,并使用从指令中传入的数据更新账户的数据字段。

你会注意到每个部分都是以宏或属性开始的,这些都有助于扩展你所编写的代码。

我们有四个主要部分:

  • declare_id! - 程序的链上地址(这取代了 entrypoint!
  • #[program] - 程序的指令逻辑
  • #[derive(Accounts)] - list、验证和反序列化传入指令的账户
  • #[account] - 为程序定义自定义账户类型

🗿 declare_id!

让我们先来了解 declare_id! 宏,因为它非常简单:

// Program on-chain address
declare_id!("6bujjNgtKQtgWEu4XMAtoJgkCn5RoqxLobuA7ptZrL6y");

这用于指定程序的链上地址(即 PROGRAM_ID)。当你第一次构建 Anchor 程序时,会生成一个新的密钥对(可以使用 anchor keys list 获取)。这个密钥对会作为部署程序的默认密钥对(除非你另外指定)。该密钥对的公钥被用作 PROGRAM_ID 并在 declare_id! 宏中定义。

👑 #[program]

#[program]
pub mod program_module_name {
use super::*;

pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
ctx.accounts.account_name.data = instruction_data;
Ok(())
}
}

#[program] 属性定义了包含所有程序指令的模块(因此是 mod)。这就是你将实现程序中每个指令逻辑的地方。你将为程序支持的每个指令创建一个公共函数。账户验证和安全检查与程序逻辑分离,因此不会出现在此处!

每个指令都需要两个参数:一个 Context 和指令数据。Anchor 会自动反序列化指令数据,所以我们无需为此担心!

在我们深入了解这些宏的其他部分之前,我们需要先了解指令逻辑中这个新的 Context 是什么。我们将深入探讨三个层次:Native 层、Rust 层和 Anchor 层,请紧随我来!

📝 Context

回想我们在本地处理指令时的工作流程。我们在 process_instruction 函数中传入了 program_idaccountsinstruction_data。除了指令数据外,其余部分可以统称为指令的“Context”。由于程序无状态,因此必须知道指令的上下文。在Anchor中,处理指令只需要Context和数据两部分。

Context是一个结构体,承载着当前事务的所有信息。它传递到每个指令处理程序中,并包含以下字段:

pub struct Context<'a, 'b, 'c, 'info, T> {
/// 当前正在执行的程序ID
pub program_id: &'a Pubkey,
/// 反序列化的账户
pub accounts: &'b mut T,
/// 剩下的账户信息,但未被反序列化或验证
/// 直接使用时需小心。
pub remaining_accounts: &'c [AccountInfo<'info>],
/// 在约束验证期间找到的Bump种子
/// 提供此项便利,以便处理程序
/// 不必重新计算Bump种子或
/// 将它们作为参数传入。
pub bumps: BTreeMap<String, u8>
}

第二层:Rust。

我们之前没在Rust中谈论过“生命周期”,这在参数'a, 'b, 'c, 'info' 符号中体现。生命周期是Rust用来追踪引用有效期的机制。每个带生命周期标记的属性都与Context的生命周期关联。简而言之,它的意思是,在Context的其他属性消失前,不要释放或解引用它,以免出现悬挂引用。但现阶段我们无需过多深究,因为这对我们即将要做的事情影响不大。

pub accounts: &'b mut T,

重要的是 T,这是一个通用占位符,代表一种类型。这意味着Context将包含一个类型,并且该类型可以在运行时确定。

简单来说,我们告诉Rust:“嘿,我现在还不知道accounts的确切类型,我会在实际使用时告诉你。”

第三层:Anchor

在运行时,accounts的类型变为我们在InstructionAccounts中定义的类型。这意味着我们的instruction_one函数现在能够访问在InstructionAccounts中声明的账户。

  • 执行程序的PROGRAM_IDctx.program_id
  • 传递到指令中的账户(ctx.accounts
  • 剩余的账户(ctx.remaining_accounts),包括所有传入指令但未在Accounts结构中声明的账户。这是不常用的。
  • 任何PDA账户的Bumpctx.bumps)。把它们放在这里,就不必在指令处理程序内重新计算。

#[derive(Accounts)]

让我们回归主题,探讨与#[derive(Accounts)]部分有关的Context类型。

这是我们定义传入指令的账户的地方。#[derive(Accounts)]宏让Anchor创建了解析和验证这些账户所需的实现。

例如,instruction_one需要一个类型为InstructionAccountsContext参数。#[derive(Accounts)]宏实现了InstructionAccounts结构,其中包括三个账户:

  • account_name
  • user
  • system_program

instruction_one被调用时,程序会:

  • 核对传入指令的账户是否与InstructionAccounts结构中规定的账户类型匹配。
  • 检查账户是否满足指定的附加约束(这是#[account]行的作用)。

最后,我想强调一下。在一行代码中,我们就执行了一个CPI到系统程序来创建一个账户!是不是有点疯狂?我们无需编写任何创建账户的代码,只需声明要创建的账户,Anchor就会完成剩下的工作!

最后,对于用户账户,有一个“mut”属性,它标记了账户为可变。由于用户将为此付费(余额会有所变化),因此它必须是可变的。

🔎 在Anchor中的账户类型

或许你还记得上周我们使用的AccountInfo类型,当在编写native程序时。每当需要处理账户时,我们都会使用这个类型 - 处理指令、创建交易、进行CPI等。这个类型覆盖了我们可能会用到的各种账户类型,比如PDA、用户账户,甚至系统程序。想想看,使用同一个类型来描述如此多样的参数,的确有些奇特。

Anchor将原生类型包裹起来,提供了一系列新类型,每个都带有不同的验证。我们不再需要在指令中检查是否拥有一个账户,因为我们可以声明它为特定类型,Anchor会为我们进行验证!

下面让我们了解一下几种常见的类型,首先是Account

你会注意到account_nameAccount类型的,它基本上是对AccountInfo的扩展,我们在原生开发中已经用过了。那么它在这里的作用是什么呢?

对于account_name账户,Account包装器会:

  • AccountStruct的格式反序列化data
  • 检查账户的程序所有者是否与指定的账户类型匹配。
  • 当在Accounts包装器中指定的账户类型是使用#[account]宏在同一个crate中定义的时候,程序的所有权检查是针对declare_id!宏中定义的programId进行的。

简直省了不少力气!

🖖 Signer类型

接下来是Signer类型。

这个类型用来确认账户是否已经签署了交易。

例如,我们可以要求user账户必须是指令的签署者。我们不检查其他任何内容 - 我们不关心账户的类型或签署者是否拥有该账户。

如果他们没有签署交易,指令就会失败!

💻 Program 输入类型

#[program]
pub mod program_module_name {
use super::*;

pub fn initialize_one(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
ctx.accounts.account_name.data = instruction_data;
Ok(())
}
}

最后,Program类型确保传入的账户符合我们的预期,并且确实是一个程序(可执行文件)。

你可能已经开始注意到Anchor是如何让事情变得简单的。这段代码不仅更简洁,还更易于理解!因为每个元素都有自己的类型,所以你能够更快地理解程序的功能。只需掌握几个额外的“规则”就行了 :)

🤔 额外的限制条件

到现在为止,我们唯一还没有涉及的是#[account]位,无论是在InstructionAccounts结构体内还是外部。

让我们先看看#[account]结构体内部:

// validate incoming account for instructions
#[derive(Accounts)]
pub struct InitializeAccounts<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub account_name: Account<'info, AccountStruct>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}

这就是我们为账户指定额外限制条件的地方。Anchor在基本验证方面做得很好,但它还能帮我们检查一些其他特定的东西!

对于account_name属性,它通过#[account(..)]指定了:

  • init - 通过CPI创建和初始化账户,将其设置为账户的discriminator
  • payer - 指定payer为结构中定义的user账户的初始化值。
  • space - 指定为账户分配的space的大小为8 + 8字节。
    • 8个字节是一个discriminatorAnchor会自动添加以识别账户类型。
    • 接下来的8个字节为账户中存储的数据分配空间,其定义在AccountStruct类型中。
    • 更多细节请参考:Space Reference

再来复习一遍。我们在一行代码中执行一个CPI到系统程序来创建一个账户!想想看,这有多简便?我们不需要手动编写代码来创建账户,我们只需指定要创建的账户,Anchor就会完成剩下的工作!

最后,对于用户账户,有一个mut属性,表示账户是可变的。因为用户会为此付费,余额可能会变化,所以它必须是可变的。

#[account]

再多陪我一会儿,我们已经到了最后的部分!

#[account]属性用于表现Solana账户的数据结构,并且实现了以下几个Trait

  • AccountSerialize
  • AccountDeserialize
  • AnchorSerialize
  • AnchorDeserialize
  • Clone
  • Discriminator
  • Owner

简单来说,#[account]属性实现了序列化和反序列化功能,并为账户实现了discriminatorOwner trait

  • discriminator是一个8字节的唯一标识符,代表账户类型,并由账户结构名称的SHA256的前8字节派生。
  • 任何对AccountDeserializetry_deserialize的调用都会检查这个discriminator
  • 如果不匹配,那么账户就会被视为无效,并且账户反序列化会以错误退出。

#[account]属性还实现了Owner Trait

  • 使用programIddeclareId声明的crate#[account]的使用。
  • 使用程序中定义的#[account]属性初始化的帐户归程序所有

就是这样,Anchor程序的构建结构就介绍完了。虽然有些复杂,但这些都是我们后续使用Anchor的必要知识。休息一下吧,很快就回来,是时候开始构建了!

‼ 赶快回来!

这真的非常重要——你现在可能无法完全理解其中的全部内容。

没关系,我也是。我花了整整两天的时间来写这一页。一旦你用Anchor构建了一个程序,再回来重新阅读一遍。你会发现更容易理解,一切都会变得更有意义。

学习并不是一个线性的过程,它会有高潮和低谷。你不能仅仅通过一次阅读就掌握宇宙中最困难的主题。不断学习,不断建立,你会学得更精,建得更好。