PDA 派生

摘要

PDA 是通过将 seed、program ID 和 bump 组合后用 SHA-256 哈希,直到结果落在 Ed25519 曲线之外为止。规范 bump 是第一个生成曲线外地址的值。最多支持 16 个 seed,每个 seed 最多 32 字节。

背景

Solana Keypair 的值是 Ed25519 曲线 上的点。一个 keypair 包含公钥(用作账户地址)和私钥(用于生成签名)。拥有私钥的人可以为该地址签署交易。

两个曲线上的账户地址两个曲线上的账户地址

PDA 是有意派生到 Ed25519 曲线之外的。由于它不是有效的曲线点,因此不存在私钥,也没有外部方可以为其生成签名。只有派生该 PDA 的程序可以通过 invoke_signed 授权 PDA 上的操作。

曲线外地址曲线外地址

PDA 与 keypair 账户的区别

属性keypair 账户PDA 账户
地址类型Ed25519 曲线上的地址Ed25519 曲线外的地址
是否有私钥没有
能否签署交易可以(有私钥时)不可以
CPI 时能否签名不可以(除非交易中包含签名)可以(通过 invoke_signed
派生方式生成 Ed25519 keypair由 seed + program ID 决定性派生
典型用途用户钱包、Program ID程序拥有的数据账户

可选 seed

可选 seed 是用户自定义的字节串,作为 PDA 派生的输入。它们用于为某个程序创建唯一且可预测的地址。例如,使用 ["user", user_pubkey] 作为 seed,可以为每个用户派生出不同的 PDA。

seed 必须遵循以下约束:

  • 每次派生最多 16 个 seed(MAX_SEEDS
  • 每个 seed 最多 32 字节(MAX_SEED_LEN

bump seed

bump seed 是在派生过程中附加到可选 seed 后的单字节(0-255)。 find_program_address 会从 255 递减到 0,依次调用 create_program_address,直到结果不在 Ed25519 曲线上。第一个成功的值就是规范 bump

程序应始终使用规范 bump,以确保从 seed 到地址的唯一且可预测的映射。

派生 PDA 时务必使用规范 bump。如果使用非规范 bump,会为同一组 seed 创建第二个有效地址,这可能导致攻击者替换为非预期账户,从而产生安全漏洞。

PDA 派生PDA 派生

派生算法

PDA 的派生在 SDK 的 create_program_address 函数中实现。算法流程如下:

  1. 校验 seed 数量不超过 MAX_SEEDS(16 个),且每个 seed 不超过 MAX_SEED_LEN(32 字节)。如有不符,返回 PubkeyError::MaxSeedLengthExceeded
  2. 将所有 seed、程序 ID 以及字符串 "ProgramDerivedAddress" 一起进行 SHA-256 哈希,得到 32 字节结果。
  3. 检查结果是否为 Ed25519 曲线上的有效点。
  4. 如果结果在曲线上,返回 PubkeyError::InvalidSeeds(此地址会有对应私钥,违背 PDA 的安全性)。
  5. 如果结果不在曲线上,则将其作为 PDA 返回。

计算单元成本

链上 syscall针对 create_program_address 每次调用收取1,500 CU

try_find_program_address syscall 在进入时收取 1,500 CU(在循环前),然后每次循环中 bump 失败时额外收取 1,500 CU。

常见 seed 模式

seed 是应用特定的。常见模式包括:

模式seed用例
全局单例["global"]整个程序范围的配置账户
每用户账户["user", user_pubkey]每个用户每个程序一个账户
每用户每实体["vault", user_pubkey, mint_pubkey]代币金库、每用户每代币
计数器 / 顺序["order", user_pubkey, &order_id.to_le_bytes()]每用户的顺序记录

seed 在哈希前会被拼接,因此 ["ab", "cd"]["abcd"] 会生成相同的 PDA。请使用定长 seed 或分隔符以避免冲突。例如,["ab", "-", "cd"] 就不会产生歧义。

示例:派生 PDA

派生 PDA 仅计算地址,并不会在该地址上创建链上账户。账户必须通过单独的指令(通常是 create_account 通过 CPI)显式创建。

Solana SDK 提供了 PDA 派生函数。每个函数都需要:

  • Program ID:用于派生 PDA 的程序地址,该程序可以代表 PDA 签名。
  • 可选 seed:预定义输入,如字符串、数字或其他账户地址。
SDK函数
@solana/kit (TypeScript)getProgramDerivedAddress
@solana/web3.js (TypeScript)findProgramAddressSync
solana_sdk (Rust)find_program_address

以下示例演示如何使用 Solana SDK 派生 PDA。点击 ▶ 运行 即可执行代码。

使用字符串 seed 派生 PDA

下面的示例演示如何使用 program ID 和可选的字符串 seed 派生 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.

使用地址 seed 派生 PDA

下面的示例演示如何使用 program ID 和可选的地址 seed 派生 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.

使用多个 seed 派生 PDA

下面的示例演示如何使用 program ID 和多个可选 seed 派生 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.

遍历所有 bump

以下示例展示了如何使用所有可能的 bump seed(255 到 0)进行 PDA 派生,说明 find_program_address 如何返回规范 bump

未包含 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 255 会生成一个在曲线上的地址,因此失败。第一个有效的 bump 是 254,这使其成为规范的 bump。

Is this page helpful?

Table of Contents

Edit Page

管理者

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