程序派生地址 (PDA)
程序派生地址 (PDAs) 为 Solana 上的开发者提供了两个主要用例:
- 确定性账户地址: PDAs 提供了一种机制,通过组合可选的“种子”(预定义输入)和 特定的程序 ID 来确定性地派生一个地址。
- 启用程序签名: Solana 运行时允许程序为从其程序 ID 派生的 PDAs “签名”。
你可以将 PDAs 视为一种通过预定义的一组输入(例如字符串、数字和其他账户地址)在链 上创建类似哈希映射结构的方法。
这种方法的好处是消除了跟踪确切地址的需要。 相反,你只需记住用于派生地址的特定输 入。
程序派生地址
重要的是要理解,简单地派生一个程序派生地址(PDA)并不会自动在该地址创建一个链上 账户。具有 PDA 作为链上地址的账户必须通过用于派生地址的程序显式创建。 你可以将派 生 PDA 视为在地图上找到一个地址。 仅仅拥有一个地址并不意味着在该位置有构建内任何 内容。
本节将介绍派生 PDAs 的详细信息。 有关程序如何使用 PDAs 进行签名的详细信息将 在跨程序调用(CPIs) 一节中介绍,因为它需要这两个概念的上 下文。
关键点 #
-
PDAs 是使用用户定义的种子、一个 bump 种子和程序 ID 的组合确定性派生的地址。
-
PDAs 是落在 Ed25519 曲线之外的地址,没有对应的私钥。
-
Solana programs can programmatically "sign" on behalf of PDAs that are derived using its program ID.
-
派生 PDA 并不会自动创建链上账户。
-
使用 PDA 作为地址的账户必须通过 Solana 程序中的专用指令显式创建。
什么是 PDA #
PDAs 是确定性派生的地址,看起来像标准的公钥,但没有关联的私钥。 这意味着没有外部 用户可以为该地址生成有效的签名。 然而,Solana 运行时允许程序以编程方式为 PDAs“签 名”而无需私钥。
作为背景,Solana Keypairs 是 Ed25519 曲线(椭圆曲线加密)上的点,具有公钥和对应的私钥。 我们通常使用公钥作 为新链上账户的唯一 ID,并使用私钥进行签名。
在曲线上的地址
PDA 是一个通过预定义的一组输入故意派生到 Ed25519 曲线之外的点。 一个不在 Ed25519 曲线上的点没有有效的对应私钥,不能用于加密操作(签名)。
然后,PDA 可以用作链上账户的地址(唯一标识符),提供一种轻松存储、映射和获取程序 状态的方法。
不在曲线上的地址
如何派生 PDA #
派生 PDA 需要 3 个输入。
- 可选种子:用于派生 PDA 的预定义输入(例如字符串、数字、其他账户地址)。这 些输入被转换为字节缓冲区。 这些输入被转换为字节缓冲区。
- Bump 种子:一个附加输入(值在 255-0 之间),用于保证生成有效的 PDA(曲线 外)。 生成 PDA 时,将 bump 种子(从 255 开始)附加到可选种子,以将点“推离 ”Ed25519 曲线。 bump 种子有时被称为“nonce”。
- 程序 ID:PDA 派生自的程序地址。 这也是可以代表 PDA“签名”的程序。
PDA 派生
下面的示例包括链接到 Solana Playground,你可以在浏览器编辑器中运行这些示例。
FindProgramAddress #
要派生 PDA,我们可以使用
findProgramAddressSync
方法,该方法来自
@solana/web3.js
。其他编程语言
(例如
Rust)
中也有此函数的等价物,但在本节中,我们将通过 Javascript 示例进行讲解。其他编程语
言(例如
Rust)
中也有此函数的等价物,但在本节中,我们将通过 Javascript 示例进行讲解。
使用findProgramAddressSync
方法时,我们传入:
- 转换为字节缓冲区的预定义可选种子,以及
- 用于派生 PDA 的程序 ID(地址)
一旦找到有效的 PDA,findProgramAddressSync
将返回派生 PDA 的地址(PDA)和 bump
种子。
下面的示例在没有提供任何可选种子的情况下派生 PDA。
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const [PDA, bump] = PublicKey.findProgramAddressSync([], programId);
console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);
你可以在 Solana Playground 上 运行此示例。 PDA 和 bump 种子的输出将始终相同:
PDA: Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9
Bump: 255
下面的示例添加了一个可选种子"helloWorld"。
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const [PDA, bump] = PublicKey.findProgramAddressSync(
[Buffer.from(string)],
programId,
);
console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);
你也可以在 Solana Playground 上运行此示例。 PDA 和 bump 种子的输出将始终相同:
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Bump: 254
注意,bump 种子是 254。 注意,bump 种子是 254。这意味着 255 派生了 Ed25519 曲线 上的一个点,并不是一个有效的 PDA。
findProgramAddressSync
返回的 bump 种子是给定可选种子和程序 ID 组合的第一个值
(在 255-0 之间),该值派生了一个有效的 PDA。
这个第一个有效的 bump 种子被称为“规范 bump”。 为了程序安全,建议在使用 PDAs 时 仅使用规范 bump。
CreateProgramAddress #
在底层,findProgramAddressSync
将迭代地将附加的 bump 种子(nonce)附加到种子缓
冲区,并调用
createProgramAddressSync
方法。bump 种子从 255 开始,每次减少 1,直到找到有效的 PDA(曲线外)。 bump 种子
从 255 开始,每次减少 1,直到找到有效的 PDA(曲线外)。
你可以通过使用createProgramAddressSync
并显式传入 254 的 bump 种子来复制前面的
示例。
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const bump = 254;
const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);
console.log(`PDA: ${PDA}`);
规范 Bump 在 Solana Playground 上运行上述示例。给定相同的种子和程序 ID,PDA 输出将与前一个匹配:
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
规范 Bump #
“规范 bump”是指派生有效 PDA 的第一个 bump 种子(从 255 开始,每次减少 1)。 为了 程序安全,建议仅使用从规范 bump 派生的 PDAs。
以之前的示例为参考,下面的示例尝试使用从 255 到 0 的每个 bump 种子派生 PDA。
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
// Loop through all bump seeds for demonstration
for (let bump = 255; bump >= 0; bump--) {
try {
const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);
console.log("bump " + bump + ": " + PDA);
} catch (error) {
console.log("bump " + bump + ": " + error);
}
}
在 Solana Playground 上运行该 示例,你应该会看到以下输出:
bump 255: Error: Invalid seeds, address must fall off the curve
bump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
bump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4y
bump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH
bump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdP
bump 250: Error: Invalid seeds, address must fall off the curve
...
// remaining bump outputs
正如预期的那样,bump seed 255 会抛出错误,第一个导出有效 PDA 的 bump seed 是 254。
但是,请注意 bump seeds 253-251 都会导出具有不同地址的有效 PDA。 这意味着在给定
相同的可选种子和 programId
的情况下,具有不同值的 bump seed 仍然可以导出有效的
PDA。
在构建 Solana 程序时,建议包括安全检查,以验证传递给程序的 PDA 是使用规范的 bump 导出的。如果不这样做,可能会引入漏洞,允许向程序提供意外的账户。
如果不这样做,可能会引入漏洞,允许向程序提供意外的账户。
创建 PDA 账户 #
这个在 Solana Playground 上的示例程序演示了如何使用 PDA 作为新账户的地址来创建账户。 示例程序是使用 Anchor 框架编写的。
在 lib.rs
文件中,你会发现以下程序,其中包括一个使用 PDA 作为账户地址创建新账
户的指令。 新账户存储了 user
的地址和用于导出 PDA 的 bump
seed。
use anchor_lang::prelude::*;
declare_id!("75GJVCJNhaukaa2vCCqhreY31gaphv7XTScBChmr1ueR");
#[program]
pub mod pda_account {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account_data = &mut ctx.accounts.pda_account;
// store the address of the `user`
account_data.user = *ctx.accounts.user.key;
// store the canonical bump
account_data.bump = ctx.bumps.pda_account;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
// set the seeds to derive the PDA
seeds = [b"data", user.key().as_ref()],
// use the canonical bump
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct DataAccount {
pub user: Pubkey,
pub bump: u8,
}
用于导出 PDA 的种子包括硬编码字符串 data
和指令中提供的 user
账户的地址。
Anchor 框架会自动导出规范的 bump
seed。
#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
init
约束指示 Anchor 调用系统程序,使用 PDA 作为地址创建新账户。 在底层,这是
通过 CPI 完成的。
#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,
在上述 Solana Playground 链接中的测试文件 (pda-account.test.ts
) 中,你会找到等
效的 Javascript 代码来导出 PDA。
const [PDA] = PublicKey.findProgramAddressSync(
[Buffer.from("data"), user.publicKey.toBuffer()],
program.programId,
);
然后发送一个交易来调用 initialize
指令,使用 PDA 作为地址创建一个新的链上账
户。 交易发送后,使用 PDA 来获取在该地址创建的链上账户。
it("Is initialized!", async () => {
const transactionSignature = await program.methods
.initialize()
.accounts({
user: user.publicKey,
pdaAccount: PDA,
})
.rpc();
console.log("Transaction Signature:", transactionSignature);
});
it("Fetch Account", async () => {
const pdaAccount = await program.account.dataAccount.fetch(PDA);
console.log(JSON.stringify(pdaAccount, null, 2));
});
请注意,如果使用相同的 user
地址作为种子多次调用 initialize
指令,则交易将失
败。 这是因为在导出的地址上已经存在一个账户。