Skip to main content

🛣 Anchor中的PDA(程序派生地址)

你做得很好!让我们继续深入探讨。

本课程中,我们将深入探讨如何使用#[account(...)]属性,并深入了解以下限制条件:

  • seedsbump - 初始化和验证PDA
  • realloc - 重新分配账户空间
  • close - 关闭账户

🛣 Anchor里的PDAs

我们再次回顾一下,PDA是通过一系列可选的种子、一个bump seed和一个 programId来衍生的。Anchor提供了一种方便的方式来验证带有seedsbump限制的PDA

#[account(seeds = [], bump)]
pub pda_account: Account<'info, AccountType>,

在账户验证过程中,Anchor会使用seeds约束中指定的种子生成一个PDA,并确认传入指令的账户是否与找到的PDA匹配。

当包含bump约束,但未指定具体的bump时,Anchor将默认使用规范bump(即找到有效PDA的第一个bump)。

#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct Example<'info> {
#[account(seeds = [b"example-seed", user.key().as_ref(), instruction_data.as_ref()]
pub pad_account: Account<'info, AccountType>,
#[account(mut)]
pub user: Signer<'info>,
}

在此示例中,通过seedbump约束验证pda_account的地址是否是预期的PDA

推导PDAseeds包括:

  • example_seed - 一个硬编码的字符串值
  • user.key() - 传入账户的公钥 user
  • instruction_data - 传入指令的数据
    • 你可以通过#[instruction(...)]属性来访问这些数据
pub fn example_instruction(
ctx: Context<Example>,
input_one: String,
input_two: String,
input_three: String,
) -> Result<()> {
// ....
Ok(()
}

#[derive(Accounts)]
#[instruction(input_one: String, input_two: String)]
pub struct Example<'info> {
// ...
}
  • 使用#[instruction(...)]属性时,指令数据必须按照传入指令的顺序排列
  • 你可以忽略不需要的最后一个参数及其之后的所有参数
#[derive(Accounts)]
#[instruction(input_one: String, input_two: String)]
pub struct Example<'info> {
// ...
}

如果输入顺序错误,将会导致错误

#[derive(Accounts)]
pub struct InitializedPda<'info> {
#[account(
init,
seeds = [b"example_seed", user.key().as_ref()]
bump,
payer = user,
space = 8 + 8
)]
pub pda_account: Account<'info, AccountType>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[account]
pub struct AccountType {
pub data: u64
}

你可以将init约束与seedsbump约束组合,以使用PDA初始化账户。

init约束必须与以下内容结合使用:

  • payer - 指定用于支付初始化费用的账户
  • space - 新账户所分配的空间大小
  • system_program - 在账户验证结构中必须存在的system_program

默认情况下,init会将创建账户的所有者设置为当前正在执行的程序。

  • 当使用initseedsbump初始化PDA账户时,所有者必须是正在执行的程序
  • 这是因为创建账户需要签名,只有执行程序的PDA才能提供
  • 如果用于派生PDAprogramId与正在执行的程序的programId不匹配,则PDA账户初始化的签名验证将失败
  • 因为init使用find_program_address来推导PDA,所以不需要指定bump
  • 这意味着PDA将使用规范的bump进行推导
  • 在为执行Anchor程序所初始化和拥有的账户分配space时,请记住前8个字节是保留给唯一账户discriminator的,Anchor程序使用该discriminator来识别程序账户类型

🧮 重新分配

在许多情况下,你可能需要更新现有账户而不是创建新账户。Anchor提供了出色的realloc约束,为现有账户重新分配空间提供了一种简便的方法。

#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct ReallocExampl<'info> {
#[account(
mut,
seeds = [b"example_seed", user.key().as_ref()]
bump,
realloc = 8 + 4 + instruction_data.len(),
realloc::payer = user,
realloc::zero = false,
)]
pub pda_account: Account<'info, AccountType>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[account]
pub struct AccountType {
pub data: u64
}

realloc约束必须与以下内容结合使用:

  • mut - 账户必须设置为可变
  • realloc::payer - 账户空间的增加或减少将相应增加或减少账户的lamports
  • realloc::zero - 一个布尔值,用于指定是否应将新内存初始化为零
  • system_program - realloc约束要求在账户验证结构中存在system_program

例如,重新分配用于存储String类型字段的账户的空间。

  • 使用String类型时,除了String本身所需的空间外,还需要额外的4个字节来存储String的长度
  • 如果账户数据长度是增加的,为了保持租金豁免,Lamport将从realloc::payer转移到程序账户
  • 如果长度减少,Lamport将从程序账户转回realloc::payer
  • 需要realloc::zero约束来确定重新分配后是否应对新内存进行零初始化
  • 在之前减小过空间的账户上增加空间时,应将此约束设置为true

close 关闭操作

当你用完一个账户并不再需要它时会发生什么呢?你可以将它关闭!

通过这样做,你可以腾出空间,并收回用于支付租金的SOL

执行关闭操作是通过使用 close 约束来完成的:

pub fn close(ctx: Context<Close>) -> Result<()> {
Ok(())
}

#[derive(Accounts)]
pub struct Close<'info> {
#[account(mut, close = receiver)]
pub data_account: Account<'info, AccountType>,
#[account(mut)]
pub receiver: Signer<'info>,
}
  • close 约束会在指令执行结束时将账户标记为已关闭,并通过将其discriminator设置为 CLOSED_ACCOUNT_DISCRIMINATOR,同时将其 lamports 发送到特定的账户。
  • discriminator设置为特定的变量,以阻止账户复活攻击(例如,后续指令重新添加租金豁免的lamports)。
  • 我们将关闭名为 data_account 的账户,并将用于租金的lamports发送到名为 receiver 的账户。
  • 然而,目前任何人都可以调用关闭指令并关闭 data_account
pub fn close(ctx: Context<Close>) -> Result<()> {
Ok(())
}

#[derive(Accounts)]
pub struct Close<'info> {
#[account(mut, close = receiver, has_one = receiver)]
pub data_account: Account<'info, AccountType>,
#[account(mut)]
pub receiver: Signer<'info>,
}

#[account]
pub struct AccountType {
pub data: String,
pub receiver: PubKey,
}
  • has_one 约束可以用来核实传入指令的账户是否与存储在 data 账户字段中的账户匹配。
  • 你必须在所使用的账户的 data 字段上应用特定的命名规则,以便进行 has_one 约束检查。
  • 使用 has_one = receiver时:
    • 账户的 data 需要有一个名为 receiver 的字段与之匹配。
    • #[derive(Accounts)] 结构中,账户名称也必须称为 receiver
  • 请注意,虽然使用 close 约束只是一个例子,但 has_one 约束可以有更广泛的用途。
info

这里需要知道的是 has_one 这个限制是很有用的。