Initial commit
This commit is contained in:
796
skills/solana-development/references/pda.md
Normal file
796
skills/solana-development/references/pda.md
Normal file
@@ -0,0 +1,796 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user