🛣 Anchor中的PDA(程序派生地址)
你做得很好!让我们继续深入探讨。
本课程中,我们将深入探讨如何使用#[account(...)]
属性,并深入了解以下限制条件:
seeds
和bump
- 初始化和验证PDA
realloc
- 重新分配账户空间close
- 关闭账户
🛣 Anchor里的PDAs
我们再次回顾一下,PDA是通过一系列可选的种子、一个bump seed
和一个 programId
来衍生的。Anchor
提供了一种方便的方式来验证带有seeds
和bump
限制的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>,
}
在此示例中,通过seed
和bump
约束验证pda_account
的地址是否是预期的PDA
。
推导PDA
的 seeds
包括:
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
约束与seeds
和bump
约束组合,以使用PDA
初始化账户。
init
约束必须与以下内容结合使用:
payer
- 指定用于支付初始化费用的账户space
- 新账户所分配的空间大小system_program
- 在账户验证结构中必须存在的system_program
默认情况下,init
会将创建账户的所有者设置为当前正在执行的程序。
- 当使用
init
与seeds
和bump
初始化PDA
账户时,所有者必须是正在执行的程序 - 这是因为创建账户需要签名,只有执行程序的
PDA
才能提供 - 如果用于派生
PDA
的programId
与正在执行的程序的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
这个限制是很有用的。