程序派生地址
Solana 的账户地址指向区块链上账户的位置。许多账户地址是 keypair 的公钥,在这种情况下,相应的私钥用于签署涉及该账户的交易。
公钥地址的一个有用替代方案是程序派生地址 (PDA)。PDA 提供了一种简单的方法来存储、映射和获取程序状态。PDA 是使用程序 ID 和一组可选的预定义输入确定性创建的地址。PDA 看起来与公钥地址类似,但没有对应的私钥。
Solana 运行时允许程序为 PDA 签名而无需私钥。使用 PDA 消除了跟踪账户地址的需要。相反,您可以回忆用于 PDA 派生的特定输入。(要了解程序如何使用 PDA 进行签名,请参阅跨程序调用部分。)
背景
Solana 的 keypair 是 Ed25519 曲线(椭圆曲线加密)上的点。它们由公钥和私钥组成。公钥成为账户地址,私钥用于为账户生成有效的签名。
两个具有曲线地址的账户
PDA 被有意派生为落在 Ed25519 曲线之外。这意味着它没有有效的对应私钥,无法执行加密操作(例如提供签名)。然而,Solana 允许程序为 PDA 签名而无需私钥。
非曲线地址
您可以将 PDA 理解为一种在链上使用预定义输入集(例如字符串、数字和其他账户地址)创建类似哈希映射结构的方式。
程序派生地址
派生一个 PDA
在使用 PDA 创建账户之前,您必须首先派生地址。派生 PDA 并不会 自动在该地址创建链上账户——账户必须通过用于派生 PDA 的程序显式创建。您可以将 PDA 想象成地图上的一个地址:仅仅因为地址存在并不意味着那里已经建造了什么。
Solana SDK 支持使用下表中显示的函数创建 PDA。每个函数接收以下输入:
- 程序 ID:用于派生 PDA 的程序地址。该程序可以代表 PDA 签名。
- 可选种子:预定义的输入,例如字符串、数字或其他账户地址。
| SDK | 函数 |
|---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
该函数使用程序 ID 和可选种子,然后通过迭代 bump 值尝试创建一个有效的程序地址。bump 值的迭代从 255 开始,每次递减 1,直到找到一个有效的 PDA。找到有效的 PDA 后,函数返回 PDA 和 bump seed。
bump seed 是附加到可选种子上的一个额外字节,用于确保生成一个有效的非曲线地址。
PDA 派生
标准 bump
bump seed 是附加到可选种子后的一个额外字节。派生函数从 255 开始迭代 bump 值,每次递减 1,直到找到一个生成有效非曲线地址的值。第一个生成有效非曲线地址的值被称为“标准 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 抛出错误。第一个派生出有效 PDA 的 bump seed 是 254。bump seed 253-251 也派生出唯一且有效的 PDA。
这意味着在相同的可选种子和 programId 下,不同值的 bump
seed 仍然可以派生出有效的 PDA。
始终包含安全检查,以确保传递给程序的 PDA 是从标准 bump 派生的。如果未执行此操作,可能会引入漏洞,允许在程序指令中使用意外的账户。最佳实践是在派生 PDA 时仅使用标准 bump。
示例
以下示例使用 Solana SDK 派生 PDA。点击 ▷ 运行 执行代码。
使用字符串种子派生 PDA
以下示例使用程序 ID 和一个可选的字符串种子派生 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
下面的示例使用程序 ID 和一个可选的地址种子派生 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
下面的示例使用程序 ID 和多个可选种子派生 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}`);
创建一个 PDA 账户
下面的示例使用 Anchor 框架
创建一个带有程序派生地址的新账户。程序包含一个
initialize
指令,用于创建新账户,该账户将存储用于派生 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 bumpdaccount_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,}
init 约束指示 Anchor
调用系统程序,使用 PDA 作为地址创建一个新账户。用于创建 PDA 的
种子 包括:
- 指令中提供的用户账户地址
- 固定字符串:"data"
- 规范的 bump seed
在此示例中,bump 约束未分配值,因此 Anchor 将使用 find_program_address
来派生 PDA 并找到 bump。
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
下面的测试文件包含一个交易,该交易调用了 initialize
指令,以创建一个具有程序派生地址的新账户。文件中包含了 派生 PDA
的代码。
该示例还展示了如何获取将要创建的新账户。
import * as anchor from "@coral-xyz/anchor";import { Program } from "@coral-xyz/anchor";import { PdaAccount } from "../target/types/pda_account";import { PublicKey } from "@solana/web3.js";describe("pda-account", () => {const provider = anchor.AnchorProvider.env();anchor.setProvider(provider);const program = anchor.workspace.PdaAccount as Program<PdaAccount>;const user = provider.wallet as anchor.Wallet;// Derive the PDA address using the seeds specified on the programconst [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);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?