Files
gh-tenequm-claude-plugins-s…/skills/solana-development/references/pda.md
2025-11-30 09:01:25 +08:00

797 lines
20 KiB
Markdown

# Program Derived Addresses (PDAs)
This reference provides comprehensive coverage of Program Derived Addresses (PDAs) for native Rust Solana program development, including derivation mechanics, security implications, and best practices.
## Table of Contents
1. [What are PDAs](#what-are-pdas)
2. [PDA Derivation Mechanics](#pda-derivation-mechanics)
3. [Canonical Bump Seeds](#canonical-bump-seeds)
4. [Creating PDA Accounts](#creating-pda-accounts)
5. [PDA Signing](#pda-signing)
6. [Common PDA Patterns](#common-pda-patterns)
7. [Security Considerations](#security-considerations)
8. [Best Practices](#best-practices)
---
## What are PDAs
**Program Derived Addresses (PDAs) are deterministic account addresses derived from a program ID and optional seeds.**
### Key Characteristics
1. **Deterministic**: Same inputs always produce the same PDA
2. **No private key**: PDAs are intentionally off the Ed25519 curve
3. **Program-signable**: The deriving program can sign for PDAs
4. **Hashmap-like**: Enable key-value storage patterns on-chain
### Why PDAs Exist
PDAs solve critical problems in Solana program development:
**Problem 1: State Storage**
- How do you store program state without tracking account addresses?
- Solution: Derive addresses from user pubkeys + seeds
**Problem 2: Program Signing**
- How can a program sign transactions without a private key?
- Solution: Runtime enables programs to sign for their PDAs
**Problem 3: Account Discovery**
- How do clients find accounts created by programs?
- Solution: Derive PDAs client-side using known seeds
### PDA vs Regular Account
| Property | Regular Account | PDA |
|----------|----------------|-----|
| Address derivation | Random (from keypair) | Deterministic (from seeds) |
| Has private key | ✅ Yes | ❌ No (off-curve) |
| Can sign transactions | ✅ Yes (with private key) | ✅ Yes (via program) |
| Who can sign | Holder of private key | Only the deriving program |
| Use case | User wallets | Program state storage |
---
## PDA Derivation Mechanics
### How PDAs are Derived
PDAs are created using a hash function that combines:
1. Program ID
2. Optional seeds (strings, numbers, pubkeys)
3. Bump seed (0-255)
The process intentionally finds an address that falls **off** the Ed25519 elliptic curve.
```
┌──────────────────────────────────────────────┐
│ Input Seeds │
├──────────────────────────────────────────────┤
│ - Program ID │
│ - Optional Seed 1 (e.g., "user_data") │
│ - Optional Seed 2 (e.g., user pubkey) │
│ - Bump seed (starts at 255) │
└──────────────────────────────────────────────┘
┌──────────────────────┐
│ Hash Function │
│ (SHA256 + checks) │
└──────────────────────┘
┌──────────────────────┐
│ Is address off-curve?│
└──────────────────────┘
│ │
│ No │ Yes
▼ ▼
Decrement bump Return (PDA, bump)
```
### Native Rust API
```rust
use solana_program::pubkey::Pubkey;
// Find PDA with canonical bump
let (pda, bump_seed) = Pubkey::find_program_address(
&[
b"user_data", // Seed 1: static string
user_pubkey.as_ref(), // Seed 2: user's public key
],
program_id,
);
// pda: The derived address (off-curve)
// bump_seed: The canonical bump (first valid bump found, starting from 255)
```
### Manual PDA Creation (Advanced)
You can manually create a PDA with a specific bump using `create_program_address`:
```rust
use solana_program::pubkey::Pubkey;
// This may fail if the bump doesn't produce a valid off-curve address
let pda = Pubkey::create_program_address(
&[
b"user_data",
user_pubkey.as_ref(),
&[bump_seed], // Specific bump
],
program_id,
)?;
```
**⚠️ Warning:** Only use `create_program_address` when you're certain the bump is valid. Prefer `find_program_address` for safety.
---
## Canonical Bump Seeds
### What is a Canonical Bump?
The **canonical bump** is the first bump seed (starting from 255, decrementing) that produces a valid off-curve address.
```rust
// Example: Finding all valid bumps
for bump in (0..=255).rev() {
if let Ok(pda) = Pubkey::create_program_address(
&[b"data", user.as_ref(), &[bump]],
program_id,
) {
println!("Bump {}: {}", bump, pda);
}
}
// Typical output:
// Bump 255: Error (on-curve)
// Bump 254: AValidPDAAddress... ← CANONICAL BUMP
// Bump 253: AnotherValidPDA...
// Bump 252: AThirdValidPDA...
// ...
```
### Why Use the Canonical Bump?
**Security Reason:** Multiple bumps can derive different valid PDAs for the same seeds. Accepting arbitrary bumps enables PDA substitution attacks.
**Attack Scenario:**
```rust
// ❌ Vulnerable - accepts any bump
pub fn update_user_balance(
program_id: &Pubkey,
accounts: &[AccountInfo],
bump: u8, // User provides bump
) -> ProgramResult {
let user = &accounts[0];
let user_pda = &accounts[1];
// Creates PDA with user-provided bump
let expected_pda = Pubkey::create_program_address(
&[b"balance", user.key.as_ref(), &[bump]],
program_id,
)?;
// Attacker can provide bump 253 instead of canonical 254
// This derives a DIFFERENT PDA the attacker controls!
// ...
}
```
**Secure Pattern:**
```rust
// ✅ Secure - uses canonical bump only
pub fn update_user_balance(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let user = &accounts[0];
let user_pda = &accounts[1];
// Derive with canonical bump
let (expected_pda, _bump) = Pubkey::find_program_address(
&[b"balance", user.key.as_ref()],
program_id,
);
// Validate
if expected_pda != *user_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Safe to proceed
// ...
}
```
### Storing the Canonical Bump
**Best Practice:** Store the canonical bump in the account data:
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserAccount {
pub bump: u8, // Store canonical bump
pub user: Pubkey,
pub balance: u64,
}
// On creation
let (pda, bump) = Pubkey::find_program_address(&[b"user", user.key.as_ref()], program_id);
let account_data = UserAccount {
bump, // Save for future operations
user: *user.key,
balance: 0,
};
```
**Why store it?**
- Saves compute units on subsequent operations
- `find_program_address` iterates from 255, costs ~3,000 CU
- Using stored bump with `create_program_address` costs ~300 CU (10x cheaper!)
---
## Creating PDA Accounts
### Creation Process
PDAs cannot create themselves. Accounts at PDA addresses must be created by:
1. Invoking the System Program via CPI
2. Using `invoke_signed` to sign with the PDA
3. The System Program creates the account and transfers ownership
### Native Rust Pattern
```rust
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::{invoke_signed},
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_user_account(
program_id: &Pubkey,
accounts: &[AccountInfo],
user_id: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?; // Pays for account
let user_pda = next_account_info(account_info_iter)?; // PDA to create
let system_program = next_account_info(account_info_iter)?; // System Program
// Signer check
if !payer.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Derive PDA
let user_id_bytes = user_id.to_le_bytes();
let (pda, bump_seed) = Pubkey::find_program_address(
&[b"user", payer.key.as_ref(), user_id_bytes.as_ref()],
program_id,
);
// Validate provided PDA matches derivation
if pda != *user_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Calculate space and rent
let account_size: usize = 1 + 32 + 8; // bump + pubkey + u64
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(account_size);
// Create account via CPI
let create_account_ix = system_instruction::create_account(
payer.key, // Payer
user_pda.key, // New account address (the PDA)
rent_lamports, // Lamports
account_size as u64, // Space
program_id, // Owner (our program)
);
// Sign with PDA using bump seed
let signer_seeds: &[&[&[u8]]] = &[&[
b"user",
payer.key.as_ref(),
user_id_bytes.as_ref(),
&[bump_seed], // Critical: Include bump in signer seeds
]];
invoke_signed(
&create_account_ix,
&[payer.clone(), user_pda.clone(), system_program.clone()],
signer_seeds, // PDA signs here
)?;
// Initialize account data
let mut account_data = UserAccount::try_from_slice(&user_pda.data.borrow())?;
account_data.bump = bump_seed;
account_data.owner = *payer.key;
account_data.user_id = user_id;
account_data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?;
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize)]
struct UserAccount {
bump: u8,
owner: Pubkey,
user_id: u64,
}
```
### Key Points
1. **Signer Seeds Format**: `&[&[&[u8]]]` (3 levels of slicing)
- Outer: Array of seed sets (for multiple PDAs)
- Middle: Single seed set (one PDA)
- Inner: Individual seed slices
2. **Bump Must Be Included**: Always append `&[bump_seed]` to signer seeds
3. **System Program Required**: Must pass System Program account for CPI
4. **Ownership Transfer**: Account starts owned by System Program, transfers to your program
---
## PDA Signing
### How Programs Sign for PDAs
When a program makes a CPI with `invoke_signed`, the runtime:
1. Receives the signer seeds
2. Derives the PDA using seeds + calling program's ID
3. Verifies the derived PDA matches an account in the instruction
4. Grants signing authority to that PDA
### invoke_signed vs invoke
```rust
// invoke: No PDA signing
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo],
) -> ProgramResult
// invoke_signed: With PDA signing
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]], // PDA seeds
) -> ProgramResult
```
### Practical Example: PDA Transfers SOL
```rust
pub fn pda_transfer_sol(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pda_account = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Derive PDA and verify
let (pda, bump_seed) = Pubkey::find_program_address(
&[b"vault", recipient.key.as_ref()],
program_id,
);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
// Create transfer instruction
let transfer_ix = system_instruction::transfer(
pda_account.key, // From: PDA (needs signing!)
recipient.key, // To: recipient
amount,
);
// PDA signs the transfer
let signer_seeds: &[&[&[u8]]] = &[&[
b"vault",
recipient.key.as_ref(),
&[bump_seed],
]];
invoke_signed(
&transfer_ix,
&[pda_account.clone(), recipient.clone(), system_program.clone()],
signer_seeds, // Runtime verifies and grants signing authority
)?;
Ok(())
}
```
### Multiple PDA Signers
You can sign with multiple PDAs in a single CPI:
```rust
let signer_seeds: &[&[&[u8]]] = &[
&[b"pda1", &[bump1]], // First PDA
&[b"pda2", &[bump2]], // Second PDA
];
invoke_signed(&instruction, &accounts, signer_seeds)?;
```
---
## Common PDA Patterns
### 1. User-Specific Accounts
**Pattern:** One PDA per user for storing user data.
```rust
// Seeds: ["user_data", user_pubkey]
let (user_pda, bump) = Pubkey::find_program_address(
&[b"user_data", user.key.as_ref()],
program_id,
);
```
**Use case:** User profiles, balances, inventory
**Advantages:**
- Easy client-side discovery
- One account per user
- User's pubkey acts as unique identifier
### 2. Global State
**Pattern:** Single PDA for program-wide state.
```rust
// Seeds: ["global_state"]
let (global_pda, bump) = Pubkey::find_program_address(
&[b"global_state"],
program_id,
);
```
**Use case:** Program configuration, global counters, admin settings
**Advantages:**
- Single source of truth
- Easy to find (no variable seeds)
- Reduced account proliferation
### 3. Association Pattern
**Pattern:** PDA associates two entities.
```rust
// Seeds: ["escrow", seller_pubkey, buyer_pubkey]
let (escrow_pda, bump) = Pubkey::find_program_address(
&[b"escrow", seller.key.as_ref(), buyer.key.as_ref()],
program_id,
);
```
**Use case:** Escrow accounts, peer-to-peer trades, relationships
**Advantages:**
- Unique per relationship
- Deterministic discovery
- Prevents duplicate associations
### 4. Index/Counter Pattern
**Pattern:** PDA with numeric index for multiple instances.
```rust
// Seeds: ["note", author_pubkey, note_id]
let note_id: u64 = 42;
let (note_pda, bump) = Pubkey::find_program_address(
&[b"note", author.key.as_ref(), note_id.to_le_bytes().as_ref()],
program_id,
);
```
**Use case:** Notes, posts, items, sequential data
**Advantages:**
- Multiple accounts per user
- Enumerable (iterate by incrementing ID)
- Scalable
**Implementation:**
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserState {
pub note_count: u64, // Track next available ID
}
pub fn create_note(
program_id: &Pubkey,
accounts: &[AccountInfo],
content: String,
) -> ProgramResult {
let user = &accounts[0];
let user_state_pda = &accounts[1];
let note_pda = &accounts[2];
// Load user state
let mut user_state = UserState::try_from_slice(&user_state_pda.data.borrow())?;
// Derive PDA for new note
let note_id = user_state.note_count;
let (expected_note_pda, bump) = Pubkey::find_program_address(
&[b"note", user.key.as_ref(), note_id.to_le_bytes().as_ref()],
program_id,
);
if expected_note_pda != *note_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Create note account...
// Initialize note data...
// Increment counter
user_state.note_count += 1;
user_state.serialize(&mut &mut user_state_pda.data.borrow_mut()[..])?;
Ok(())
}
```
### 5. Vault/Treasury Pattern
**Pattern:** PDA holds funds for the program.
```rust
// Seeds: ["vault"]
let (vault_pda, bump) = Pubkey::find_program_address(
&[b"vault"],
program_id,
);
```
**Use case:** Staking pools, treasuries, escrow
**Advantages:**
- Program controls funds
- No external keypair needed
- Can't lose "private key"
---
## Security Considerations
### 1. Always Validate PDAs
**❌ Vulnerable:**
```rust
pub fn update_balance(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let user_pda = &accounts[0];
// No PDA validation!
let mut user_data = UserData::try_from_slice(&user_pda.data.borrow())?;
user_data.balance += 100;
user_data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?;
Ok(())
}
```
**✅ Secure:**
```rust
pub fn update_balance(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let user = &accounts[0];
let user_pda = &accounts[1];
// Derive and validate
let (expected_pda, _) = Pubkey::find_program_address(
&[b"user", user.key.as_ref()],
program_id,
);
if expected_pda != *user_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Safe to proceed
let mut user_data = UserData::try_from_slice(&user_pda.data.borrow())?;
user_data.balance += 100;
user_data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?;
Ok(())
}
```
### 2. Non-Canonical Bump Attack
**Vulnerability:** Accepting user-provided bumps allows PDA substitution.
**Impact:** Attacker can manipulate which account is used.
**Prevention:**
- Always use `find_program_address` (canonical bump)
- Never accept bump as instruction parameter
- Store bump in account data after creation
### 3. Seed Confusion
**Vulnerability:** Ambiguous seed ordering can create collisions.
```rust
// ❌ Problematic - seeds can collide
let seed1 = "hello";
let seed2 = "world";
// These derive the SAME PDA:
Pubkey::find_program_address(&[b"helloworld"], program_id);
Pubkey::find_program_address(&[b"hello", b"world"], program_id);
```
**Prevention:**
```rust
// ✅ Use fixed-size types and clear separators
Pubkey::find_program_address(
&[
b"prefix_", // Fixed prefix
user.key.as_ref(), // 32 bytes (fixed)
&id.to_le_bytes(), // 8 bytes (fixed)
],
program_id,
);
```
### 4. Ownership Verification
**Always verify PDA ownership:**
```rust
// ✅ Check ownership after PDA validation
if user_pda.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
```
---
## Best Practices
### 1. Seed Design
**Good Seed Patterns:**
- Use descriptive prefixes: `b"user_profile"`, `b"escrow"`, `b"vault"`
- Include entity identifiers: user pubkeys, IDs
- Use fixed-size types: `u64.to_le_bytes()`, `Pubkey::as_ref()`
- Maintain logical ordering: most general → most specific
**Example:**
```rust
&[
b"note", // What type of account
author.key.as_ref(), // Who owns it
note_id.to_le_bytes(), // Which instance
]
```
### 2. Always Store the Bump
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct PdaAccount {
pub bump: u8, // Always first field for efficiency
// ... other fields
}
```
**Benefits:**
- Saves ~2,700 CU per operation
- Enables efficient re-derivation
- Documents canonical bump
### 3. Validate Everything
**Security Checklist:**
- ✅ Derive PDA with canonical bump
- ✅ Compare derived PDA to provided account
- ✅ Verify PDA owner is your program
- ✅ Check initialization status
- ✅ Validate signer requirements
### 4. Document Your Seed Schema
```rust
/// Derives a user profile PDA.
///
/// Seeds: ["user_profile", user_pubkey]
/// Bump: Stored in account.bump
pub fn derive_user_profile_pda(
user: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[b"user_profile", user.as_ref()],
program_id,
)
}
```
### 5. Use Helper Functions
```rust
pub struct PdaDerivation;
impl PdaDerivation {
pub fn user_profile(user: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[b"user", user.as_ref()], program_id)
}
pub fn note(
author: &Pubkey,
note_id: u64,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[b"note", author.as_ref(), note_id.to_le_bytes().as_ref()],
program_id,
)
}
}
// Usage
let (user_pda, bump) = PdaDerivation::user_profile(user.key, program_id);
```
---
## Summary
**Key Takeaways:**
1. **PDAs are deterministic addresses** derived from program ID + seeds
2. **No private key exists** for PDAs (they're off-curve by design)
3. **Only the deriving program can sign** for its PDAs
4. **Always use canonical bump** to prevent substitution attacks
5. **Validate PDAs before use** - never trust client-provided accounts
6. **Store the bump** in account data for compute efficiency
7. **Design clear seed schemas** to prevent collisions and confusion
**Security Mantra:**
```rust
// Always follow this pattern
let (expected_pda, bump) = Pubkey::find_program_address(&seeds, program_id);
if expected_pda != *provided_pda.key {
return Err(ProgramError::InvalidSeeds);
}
if provided_pda.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
```
PDAs are the foundation of state management in Solana programs. Master them, validate them religiously, and your programs will be secure and efficient.