程序派生地址

Solana 的账户地址指向区块链上账户的位置。许多账户地址是 keypair 的公钥,在这种情况下,相应的私钥用于签署涉及该账户的交易。

公钥地址的一个有用替代方案是程序派生地址 (PDA)。PDA 提供了一种简单的方法来存储、映射和获取程序状态。PDA 是使用程序 ID 和一组可选的预定义输入确定性创建的地址。PDA 看起来与公钥地址类似,但没有对应的私钥。

Solana 运行时允许程序为 PDA 签名而无需私钥。使用 PDA 消除了跟踪账户地址的需要。相反,您可以回忆用于 PDA 派生的特定输入。(要了解程序如何使用 PDA 进行签名,请参阅跨程序调用部分。)

背景

Solana 的 keypairEd25519 曲线(椭圆曲线加密)上的点。它们由公钥和私钥组成。公钥成为账户地址,私钥用于为账户生成有效的签名

两个具有曲线地址的账户两个具有曲线地址的账户

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 派生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);
}
}
Console
Click to execute the code.
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 抛出错误。第一个派生出有效 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}`);
Console
Click to execute the code.

使用地址种子派生 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}`);
Console
Click to execute the code.

使用多个种子派生 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}`);
Console
Click to execute the code.

创建一个 PDA 账户

下面的示例使用 Anchor 框架 创建一个带有程序派生地址的新账户。程序包含一个 initialize 指令,用于创建新账户,该账户将存储用于派生 PDA 的 用户地址bump seed

Program
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 bumpd
account_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 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,
}

init 约束指示 Anchor 调用系统程序,使用 PDA 作为地址创建一个新账户。用于创建 PDA 的 种子 包括:

  • 指令中提供的用户账户地址
  • 固定字符串:"data"
  • 规范的 bump seed

在此示例中,bump 约束未分配值,因此 Anchor 将使用 find_program_address 来派生 PDA 并找到 bump。

pda_account
#[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 的代码。

该示例还展示了如何获取将要创建的新账户。

Test
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 program
const [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?

Table of Contents

Edit Page

管理者

©️ 2025 Solana 基金会版权所有
取得联系