Program Derived Address (PDA)
Program Derived Address (PDA) 为 Solana 开发者提供了两个主要用例:
- 确定性账户地址:PDA 提供了一种机制,可以使用可选的 "seed"(预定义输入)和特定的程序 ID 的组合来确定性地创建地址。
- 支持程序签名:Solana 运行时允许程序为从程序地址推导出的 PDA "签名"。
您可以将 PDA 理解为一种在链上从预定义输入(例如字符串、数字和其他账户地址)创建类似哈希表结构的方式。
这种方法的好处在于,它消除了需要跟踪确切地址的需求。相反,您只需记住用于推导地址的特定输入即可。
Program Derived Address
需要注意的是,仅仅推导出一个 Program Derived Address (PDA) 并不会自动在该地址创建链上账户。使用 PDA 作为链上地址的账户必须通过用于推导该地址的程序显式创建。您可以将推导 PDA 理解为在地图上找到一个地址。仅仅拥有一个地址并不意味着该位置已经建有任何东西。
本节介绍了 PDA 推导的详细信息。Cross Program Invocations (CPI) 部分解释了程序如何使用 PDA 进行签名。
关键点
- PDA 是通过预定义的 seed、bump seed 和程序 ID 的组合确定性推导出的地址。
- PDA 是位于 Ed25519 曲线之外的地址,没有对应的私钥。
- Solana 程序可以代表从其程序 ID 推导出的 PDA 进行签名。
- 推导 PDA 并不会自动创建链上账户。
- 使用 PDA 作为地址的账户必须通过 Solana 程序中的指令创建。
什么是 PDA
PDA 是一种地址,它以确定性的方式生成,看起来像公钥,但没有私钥。这意味着无法为该地址生成有效的签名。然而,Solana 运行时允许程序无需私钥即可为 PDA "签名"。
作为背景知识,Solana 的密钥对是 Ed25519 曲线(椭圆曲线加密)上的点,包含一个公钥和对应的私钥。公钥用作链上账户的地址(唯一标识符)。
曲线上地址
PDA 是一个通过预定义输入集有意生成的点,位于 Ed25519 曲线之外。位于 Ed25519 曲线之外的点没有有效的对应私钥,无法执行加密操作(签名)。
PDA 可以作为链上账户的地址(唯一标识符),提供一种轻松存储、映射和获取程序状态的方法。
曲线外地址
如何生成 PDA
生成 PDA 需要以下三个输入:
- 可选种子:用于 PDA 生成的预定义输入(例如字符串、数字、其他账户地址)。
- Bump seed:附加到可选种子上的一个额外字节,用于确保生成有效的 PDA(曲线外)。Bump seed 从 255 开始,每次递减 1,直到找到有效的 PDA。
- 程序 ID:生成 PDA 的程序地址。该程序可以代表 PDA 进行签名。
PDA 生成
使用相应 SDK 中的以下函数来生成 PDA。
SDK | 功能 |
---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
要派生一个 PDA,请将以下输入提供给 SDK 函数:
- 转换为字节的预定义可选种子
- 用于派生的程序 ID(地址)
一旦找到有效的 PDA,该函数将返回地址(PDA)和用于派生的 bump seed。
示例
以下示例展示了如何使用相应的 SDK 派生 PDA。
点击“运行”按钮执行代码。
使用可选字符串种子派生 PDA
import { Address, getProgramDerivedAddress } from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const seeds = ["helloWorld"];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
使用可选地址种子派生 PDA
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
使用多个可选种子派生 PDA
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const optionalSeedString = "helloWorld";const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedString, optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
标准 Bump
PDA 的推导需要一个 "bump seed",这是附加到可选种子后的一个额外字节。推导函数从 255 开始迭代 bump 值,每次递减 1,直到找到一个生成有效非曲线地址的值。第一个生成有效非曲线地址的 bump 值被称为 "标准 bump"。
以下示例展示了使用所有可能的 bump seed(从 255 到 0)进行 PDA 推导:
示例中未包含 Kit 示例,因为 createProgramDerivedAddress 函数未被导出。
import { PublicKey } from "@solana/web3.js";const programId = new PublicKey("11111111111111111111111111111111");const optionalSeed = "helloWorld";// Loop through all bump seeds (255 down to 0)for (let bump = 255; bump >= 0; bump--) {try {const PDA = PublicKey.createProgramAddressSync([Buffer.from(optionalSeed), Buffer.from([bump])],programId);console.log("bump " + bump + ": " + PDA);} catch (error) {console.log("bump " + bump + ": " + error);}}
bump 255: Error: Invalid seeds, address must fall off the curvebump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6Xbump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4ybump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHHbump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdPbump 250: Error: Invalid seeds, address must fall off the curve...// remaining bump outputs
bump seed 255 会抛出错误,第一个推导出有效 PDA 的 bump seed 是 254。
请注意,bump seed
253-251 都能推导出有效的 PDA,但地址各不相同。这意味着在给定相同的可选种子和 programId
的情况下,不同值的 bump
seed 仍然可以推导出有效的 PDA。
在构建 Solana 程序时,请务必包含安全检查,以确保传递给程序的 PDA 是从标准 bump 推导而来的。如果未包含这些检查,可能会引入漏洞,允许在程序指令中使用意外的账户。最佳实践是在推导 PDA 时仅使用标准 bump。
创建 PDA 账户
以下示例程序展示了如何使用 PDA 作为新账户地址来创建账户。示例程序使用了 Anchor 框架。
该程序包含一个 initialize
指令,用于使用 PDA 作为账户地址创建新账户。新账户存储了 user
的地址以及用于推导 PDA 的 bump
种子。
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 bumpaccount_data.bump = ctx.bumps.pda_account;Ok(())}}#[derive(Accounts)]pub struct Initialize<'info> {#[account(mut)]pub user: Signer<'info>,#[account(init,// define the seeds to derive the PDAseeds = [b"data", user.key().as_ref()],// use the canonical bumpbump,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
种子。
#[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 作为地址创建一个新账户。Anchor 通过
CPI 实现这一操作。
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
测试文件包含用于推导 PDA 的 Typescript 代码。
const [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);
测试文件中的交易调用了 initialize
指令,使用 PDA 作为地址创建一个新的链上账户。在此示例中,Anchor 可以从指令账户中推断出 PDA 地址,因此无需显式提供。
it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).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));});
请注意,在此示例中,如果您多次调用 initialize
指令,并使用相同的 user
地址作为种子,则交易会失败。这是因为在推导出的地址上已经存在一个账户。
Is this page helpful?