Skip to main content
← all notes
April 15, 2024· Solana· 8 min read

Understanding Program Derived Addresses in Solana blockchain

A deep dive into Program Derived Addresses (PDAs) in Solana - one of the most powerful and unique features of the Solana blockchain.

Introduction

If you're building on Solana, you've probably heard about Program Derived Addresses (PDAs). They're one of the most powerful and unique features of the Solana blockchain, but they can also be quite confusing at first. In this post, I'll break down what PDAs are, why they exist, and how to use them effectively in your Solana programs.

What are Program Derived Addresses?

Program Derived Addresses (PDAs) are addresses that are deterministically derived from a program ID and a set of seeds. Unlike regular Solana addresses (which have a corresponding private key), PDAs don't have private keys - they exist "off the curve" of the Ed25519 elliptic curve.

// Example: Deriving a PDA
let (pda, bump) = Pubkey::find_program_address(
    &[b"user_stats", user.key().as_ref()],
    program_id
);

Why Do PDAs Exist?

PDAs solve a critical problem in blockchain development: how can a program securely own and control accounts?

In traditional blockchain systems, only entities with private keys can sign transactions and control accounts. But programs don't have private keys - they're just code! PDAs solve this by creating addresses that:

  1. Are deterministically derived - You can always find the same address using the same seeds
  2. Don't have private keys - They exist off the Ed25519 curve
  3. Can only be "signed" by the program - The program can sign for these addresses programmatically

Key Concepts

1. Seeds

Seeds are the inputs used to derive a PDA. They can be:

  • Static strings (like b"vault")
  • User public keys
  • Other identifiers
// Common seed patterns
let seeds = &[
    b"user_account",           // Static identifier
    user.key().as_ref(),       // User's public key
    &index.to_le_bytes(),      // Numeric identifier
];

2. Bump Seed

Since PDAs must be off the curve, we use a "bump seed" to find a valid PDA. Solana tries values from 255 down to 0 until it finds an address that's off the curve.

// Finding a PDA with its bump
let (pda, bump) = Pubkey::find_program_address(seeds, program_id);
 
// Later, you can use this bump to verify
let pda = Pubkey::create_program_address(
    &[seeds, &[bump]],
    program_id
)?;

Practical Example: User Profile System

Let's build a simple user profile system using PDAs:

use anchor_lang::prelude::*;
 
#[program]
pub mod user_profiles {
    use super::*;
 
    pub fn create_profile(
        ctx: Context<CreateProfile>,
        name: String,
    ) -> Result<()> {
        let profile = &mut ctx.accounts.profile;
        profile.owner = ctx.accounts.user.key();
        profile.name = name;
        profile.created_at = Clock::get()?.unix_timestamp;
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct CreateProfile<'info> {
    #[account(
        init,
        payer = user,
        space = 8 + 32 + 50 + 8,
        seeds = [b"profile", user.key().as_ref()],
        bump
    )]
    pub profile: Account<'info, UserProfile>,
    
    #[account(mut)]
    pub user: Signer<'info>,
    
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct UserProfile {
    pub owner: Pubkey,
    pub name: String,
    pub created_at: i64,
}

Benefits of PDAs

1. Deterministic Addresses

You can always find the same account using the same seeds:

// Frontend code to find the same PDA
const [profilePDA] = await PublicKey.findProgramAddress(
  [Buffer.from("profile"), userPublicKey.toBuffer()],
  programId
);

2. No Private Key Management

Programs can sign for PDAs without needing to manage private keys:

// Program can sign for the PDA
let signer_seeds = &[
    b"vault",
    &[bump],
];
 
invoke_signed(
    &instruction,
    &accounts,
    &[signer_seeds],
)?;

3. Account Relationship Mapping

PDAs are perfect for creating relationships between accounts:

// User's token account PDA
seeds = [b"token_account", user.key().as_ref()]
 
// User's staking account PDA
seeds = [b"stake", user.key().as_ref(), mint.key().as_ref()]
 
// NFT metadata PDA
seeds = [b"metadata", mint.key().as_ref()]

Common Patterns

Pattern 1: One-Per-User Accounts

seeds = [b"user_data", user.key().as_ref()]

This creates one unique account per user.

Pattern 2: One-Per-Token Accounts

seeds = [b"token_vault", mint.key().as_ref()]

This creates one unique account per token type.

Pattern 3: Multiple Accounts Per User

seeds = [b"user_nft", user.key().as_ref(), &index.to_le_bytes()]

This allows multiple accounts per user, indexed by number.

Common Pitfalls

1. Seed Length Limits

Seeds must be ≤ 32 bytes each, and total seeds ≤ 32 seeds.

// avoid: seed too long
let seeds = &[very_long_string_over_32_bytes];
 
// prefer: hash long data
let hash = hash(very_long_string);
let seeds = &[hash.as_ref()];

2. Forgetting the Bump

// avoid: missing bump
invoke_signed(
    &instruction,
    &accounts,
    &[&[b"vault"]],
)?;
 
// prefer: include the bump
invoke_signed(
    &instruction,
    &accounts,
    &[&[b"vault", &[bump]]],
)?;

3. Seed Ordering

The order of seeds matters! Different orders create different PDAs.

// These create DIFFERENT PDAs:
seeds = [user.key(), mint.key()]
seeds = [mint.key(), user.key()]

Testing PDAs

Here's how to test PDA derivation in your tests:

import { PublicKey } from '@solana/web3.js';
 
describe("PDA Tests", () => {
  it("derives the same PDA consistently", async () => {
    const [pda1] = await PublicKey.findProgramAddress(
      [Buffer.from("profile"), userKeypair.publicKey.toBuffer()],
      program.programId
    );
    
    const [pda2] = await PublicKey.findProgramAddress(
      [Buffer.from("profile"), userKeypair.publicKey.toBuffer()],
      program.programId
    );
    
    expect(pda1.toString()).toBe(pda2.toString());
  });
});

Real-World Example: NFT Staking

In my NFT Staking DApp, I use PDAs extensively:

// Stake account PDA - one per user per NFT
seeds = [
    b"stake",
    user.key().as_ref(),
    nft_mint.key().as_ref()
]
 
// Reward vault PDA - one per staking pool
seeds = [
    b"reward_vault",
    pool.key().as_ref()
]

This ensures each user can only stake each NFT once, and the program controls the reward distribution.

Conclusion

Program Derived Addresses are a fundamental building block of Solana development. They enable:

  • Secure program-controlled accounts
  • Deterministic address generation
  • Clean account relationship management

Once you understand PDAs, you'll find they're essential for building robust Solana programs. They might seem complex at first, but with practice, they become second nature.

Resources


Have questions about PDAs or Solana development? Reach out on X.

enjoyed it?

· end of transmission⌘K to navigate
© 2026
away