797 lines
20 KiB
Markdown
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.
|