返回文章列表

Solana PDA 详解:程序派生地址的原理与实践

2025年12月26日|5 min read|链上存证

#什么是 PDA?

PDA(Program Derived Address)是 Solana 中一种特殊的账户地址:

  • 没有私钥 - 不在 Ed25519 椭圆曲线上
  • 程序控制 - 只有程序可以为其"签名"
  • 确定性派生 - 从固定的 seeds 可以计算出唯一地址
┌─────────────────────────────────────────────────────────────┐
│                         PDA 原理                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Seeds + Program ID + Bump → PDA Address                    │
│                                                             │
│  ┌─────────┐    ┌───────────┐    ┌──────┐                  │
│  │ "user"  │    │ Program   │    │ Bump │                  │
│  │ pubkey  │ +  │    ID     │ +  │ 255  │ → 0xABC...123   │
│  └─────────┘    └───────────┘    └──────┘                  │
│                                                             │
│  Bump 从 255 开始递减,直到找到不在曲线上的地址               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

#为什么需要 PDA?

#1. 程序拥有的账户

普通账户需要私钥签名才能操作,但程序没有私钥。PDA 让程序可以"拥有"账户:

// 用户的游戏数据,由程序控制
#[account(
    seeds = [b"player", user.key().as_ref()],
    bump
)]
pub player_data: Account<'info, PlayerData>,

#2. 确定性地址

给定相同的 seeds,任何人都能计算出相同的 PDA 地址:

// 前端计算
const [pda, bump] = PublicKey.findProgramAddressSync(
  [Buffer.from("player"), userPubkey.toBuffer()],
  programId
);

// 链上验证 - 使用相同的 seeds

#3. 关联数据

把用户和他的数据关联起来:

User A (Pubkey) → PDA ["user", A] → UserData for A
User B (Pubkey) → PDA ["user", B] → UserData for B

#Anchor 中使用 PDA

#创建 PDA 账户

use anchor_lang::prelude::*;

declare_id!("Your_Program_ID");

#[program]
pub mod pda_example {
    use super::*;

    pub fn create_profile(ctx: Context<CreateProfile>, name: String) -> Result<()> {
        let profile = &mut ctx.accounts.profile;
        profile.owner = ctx.accounts.owner.key();
        profile.name = name;
        profile.level = 1;
        profile.xp = 0;
        profile.bump = ctx.bumps.profile;  // 保存 bump
        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(name: String)]
pub struct CreateProfile<'info> {
    #[account(
        init,
        payer = owner,
        space = 8 + 32 + 4 + 32 + 8 + 8 + 1,
        seeds = [b"profile", owner.key().as_ref()],
        bump
    )]
    pub profile: Account<'info, Profile>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct Profile {
    pub owner: Pubkey,
    pub name: String,
    pub level: u64,
    pub xp: u64,
    pub bump: u8,
}

#访问 PDA 账户

#[derive(Accounts)]
pub struct UpdateProfile<'info> {
    #[account(
        mut,
        seeds = [b"profile", owner.key().as_ref()],
        bump = profile.bump,  // 使用保存的 bump
        has_one = owner       // 验证 owner 匹配
    )]
    pub profile: Account<'info, Profile>,
    pub owner: Signer<'info>,
}

#PDA 签名

当程序需要以 PDA 身份转移资金或调用其他程序时:

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let vault = &ctx.accounts.vault;

    // 构造 PDA 签名的 seeds
    let seeds = &[
        b"vault",
        ctx.accounts.owner.key.as_ref(),
        &[vault.bump],
    ];
    let signer_seeds = &[&seeds[..]];

    // 使用 PDA 签名进行转账
    let cpi_accounts = Transfer {
        from: ctx.accounts.vault_token.to_account_info(),
        to: ctx.accounts.user_token.to_account_info(),
        authority: vault.to_account_info(),  // PDA 作为 authority
    };
    let cpi_program = ctx.accounts.token_program.to_account_info();
    let cpi_ctx = CpiContext::new_with_signer(
        cpi_program,
        cpi_accounts,
        signer_seeds  // 传入签名 seeds
    );

    token::transfer(cpi_ctx, amount)?;
    Ok(())
}

#常见 PDA 模式

#1. 用户数据存储

// 每个用户一个数据账户
seeds = [b"user-data", user.key().as_ref()]

#2. 全局配置

// 程序的全局配置,只有一个
seeds = [b"config"]

#3. 关联账户

// 某个 NFT 的元数据
seeds = [b"metadata", mint.key().as_ref()]

#4. 计数器模式

// 第 N 个订单
seeds = [b"order", user.key().as_ref(), &order_id.to_le_bytes()]

#5. 多层嵌套

// 用户在某个游戏中的角色
seeds = [b"character", game.key().as_ref(), user.key().as_ref()]

#前端查找 PDA

import { PublicKey } from "@solana/web3.js";

// 查找 PDA
const [profilePda, bump] = PublicKey.findProgramAddressSync(
  [Buffer.from("profile"), userPubkey.toBuffer()],
  programId
);

// 使用 PDA 读取数据
const profileAccount = await program.account.profile.fetch(profilePda);

#注意事项

#1. Bump 的重要性

始终保存并使用保存的 bump,避免重复计算:

#[account]
pub struct MyPda {
    pub data: u64,
    pub bump: u8,  // 保存 bump
}

#2. Seeds 唯一性

确保 seeds 组合能产生唯一的 PDA:

// ❌ 不好 - 可能冲突
seeds = [b"data"]

// ✅ 好 - 包含用户信息
seeds = [b"data", user.key().as_ref()]

#3. 空间计算

别忘了 bump 占用 1 byte:

space = 8 + ... + 1  // 最后的 1 是 bump

#总结

概念说明
PDA程序派生地址,由 seeds 确定性生成
Bump用于确保地址不在曲线上的随机数
Seeds用于派生 PDA 的字节数组
签名程序可以用 PDA 的 seeds 进行 CPI 签名

PDA 是 Solana 开发的核心概念,掌握它才能开发复杂的链上应用。


本文内容哈希已存证,点击下方按钮可将哈希写入 Polygon 链上。

END

On-Chain Proof

将文章内容哈希写入 Polygon 链,证明内容在此时间点存在且未被篡改。

Content Hash (Keccak-256)
0x18caeeca591d8f28ed2bf2bd7e9a2fe5313fa037d0872c89e91cc1beb4b4e1e6

Connect your wallet to verify this article on-chain.

💬 评论与讨论

使用 GitHub 账号登录即可评论,欢迎讨论和提问!