learn.sol
Anchor Programs • Pda Seed Derivation
Lesson 6 of 12

PDAs and Seed Derivation

Learn how Anchor uses seeds and bumps to derive deterministic program-controlled addresses, and how to design PDA state safely.

The last lesson used a normal keypair-backed account for the counter.

That is useful for learning, but it is not how many real Solana programs organize state.

Real programs often need addresses they can derive again later.

That is where PDAs come in.

The Core Idea

A PDA is a deterministic address derived from:

  • seed bytes
  • a program ID
  • a bump

In practice, that means your program can decide what an address should be before the account exists.

That is the real power.

You are not waiting for a random keypair anymore.

You are defining the address from the business rule itself.

Why Programs Need PDAs

A normal keypair account is fine when a client can create any arbitrary account and hand it to the program.

A PDA is better when the program needs a predictable rule like:

  • one profile per user
  • one review per (reviewer, movie) pair
  • one config account for the whole program
  • one vault authority for one escrow

Those are not random accounts.

They are accounts with a deterministic relationship to your program state.

The Right Mental Model

If a normal keypair is user-chosen state, a PDA is program-chosen state.

That does not mean the program magically owns everything about it.

It means the address is derived from rules your program can reproduce.

That is why PDAs are so useful for:

  • unique records
  • namespaced state
  • authority accounts
  • vault patterns
PDATap to reveal
A deterministic program-derived address computed from seeds, a bump, and one program ID.
PDA
Seed FormulaTap to reveal
The ordered list of seed bytes that defines one stable account-address rule for your program.
Seed Formula
Namespace SeedTap to reveal
A fixed label like `b"profile"` that makes the purpose of the PDA explicit and avoids collisions with other account types.
Namespace Seed
BumpTap to reveal
The extra byte used with the seeds to derive a valid PDA that the runtime accepts.
Bump

What Anchor Adds

The official Anchor PDA docs emphasize that you usually define PDAs directly in the account constraints.

That means the PDA derivation rule also becomes a validation rule.

This is the main pattern:

#[account(
    seeds = [b"profile", authority.key().as_ref()],
    bump,
)]
pub profile: Account<'info, Profile>,

Two important things are happening here at once:

  • Anchor derives the expected PDA from those seeds
  • Anchor checks that the provided account matches that expected address

That is why PDAs are not just an address trick.

They are part of your validation model.

seeds and bump Belong Together

The official account constraints reference is explicit here:

  • seeds defines the PDA inputs
  • bump is used with those seeds to derive a valid PDA

You should think of them as a pair.

#[account(
    seeds = [b"profile", authority.key().as_ref()],
    bump,
)]
pub profile: Account<'info, Profile>,

Anchor supports:

  • static seeds like b"profile"
  • dynamic seeds like authority.key().as_ref()
  • combinations of both

That is how you move from vague state to deterministic state.

Your First Useful PDA Example

Start with a profile account.

The business rule is simple:

one profile per user.

That rule maps directly to a PDA formula.

#[account]
#[derive(InitSpace)]
pub struct UserProfile {
    pub authority: Pubkey,
    #[max_len(32)]
    pub username: String,
}

Now define the creation context.

#[derive(Accounts)]
pub struct CreateProfile<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        init,
        payer = authority,
        space = 8 + UserProfile::INIT_SPACE,
        seeds = [b"profile", authority.key().as_ref()],
        bump,
    )]
    pub profile: Account<'info, UserProfile>,

    pub system_program: Program<'info, System>,
}

Read that rule in plain language:

  • namespace this account under b"profile"
  • tie it to one authority public key
  • derive one deterministic address from those inputs
  • create the account there

That means the same user cannot create ten different profile accounts under this rule.

The seed formula defines uniqueness.

Why The Namespace Seed Matters

Do not start your PDA design with only dynamic values.

Start with a namespace seed.

Good:

seeds = [b"profile", authority.key().as_ref()]

Weak:

seeds = [authority.key().as_ref()]

The namespace seed does three things:

  • it makes the PDA purpose obvious
  • it avoids collisions with other PDA types in your program
  • it makes the design easier to review later

This is a small habit, but it matters a lot once programs grow.

Updating The Same PDA Later

The next important rule is simple:

if you create a PDA with one seed formula, you must validate it with the same seed formula later.

Here is an update context:

#[derive(Accounts)]
pub struct UpdateProfile<'info> {
    pub authority: Signer<'info>,

    #[account(
        mut,
        seeds = [b"profile", authority.key().as_ref()],
        bump,
        has_one = authority,
    )]
    pub profile: Account<'info, UserProfile>,
}

This gives you two layers of protection:

  • the account must be the correct PDA for that authority
  • the stored authority field must still match the signer

That combination is stronger than signer checks alone.

Seed Order Is Part Of The Address

This is one of the easiest mistakes to make.

These are not the same:

seeds = [b"review", reviewer.key().as_ref(), movie.as_bytes()]
seeds = [b"review", movie.as_bytes(), reviewer.key().as_ref()]

Changing seed order changes the derived address.

That means seed order is part of your state model.

Pick the rule once, document it, and keep it consistent everywhere.

Seed Order Is Part Of The Schema

If you ever have to ask “does seed order matter?”, the answer is yes.

Treat the seed formula like a schema, not like a casual helper expression.

Do Not Put Unstable Data In Seeds Without Thinking

A seed should usually be based on data that is stable enough to derive again later.

Good seed candidates:

  • fixed namespace bytes
  • public keys
  • stable IDs
  • immutable labels

Risky seed candidates:

  • editable usernames
  • mutable titles
  • arbitrary text you may want to change later

If a value can change, and you use it as a seed, your addressing scheme gets harder to reason about.

That does not always mean “never do it.”

It means “do it intentionally.”

PDA Creation Versus PDA Validation

These are related, but not identical.

During creation, you usually have something like:

#[account(
    init,
    payer = authority,
    space = 8 + UserProfile::INIT_SPACE,
    seeds = [b"profile", authority.key().as_ref()],
    bump,
)]
pub profile: Account<'info, UserProfile>,

Later, when the account already exists, you remove init and keep the validation rule:

#[account(
    mut,
    seeds = [b"profile", authority.key().as_ref()],
    bump,
    has_one = authority,
)]
pub profile: Account<'info, UserProfile>,

That pattern matters.

Creation is one moment.

Validation is every moment after that.

PDA Seeds In The IDL

The official Anchor PDA docs point out an important detail:

PDA seed information defined in account constraints is included in the program IDL.

Why that matters:

  • clients can understand more of the account derivation rule
  • account resolution can be more structured
  • the program interface becomes less opaque

That does not remove the need to understand the seeds yourself.

But it does make the program contract richer and easier to work with.

One Small Full Example

Here is the minimal shape for a profile creation instruction.

use anchor_lang::prelude::*;

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

    pub fn create_profile(ctx: Context<CreateProfile>, username: String) -> Result<()> {
        let profile = &mut ctx.accounts.profile;
        profile.authority = ctx.accounts.authority.key();
        profile.username = username;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct CreateProfile<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        init,
        payer = authority,
        space = 8 + UserProfile::INIT_SPACE,
        seeds = [b"profile", authority.key().as_ref()],
        bump,
    )]
    pub profile: Account<'info, UserProfile>,

    pub system_program: Program<'info, System>,
}

#[account]
#[derive(InitSpace)]
pub struct UserProfile {
    pub authority: Pubkey,
    #[max_len(32)]
    pub username: String,
}

If you understand why that address is deterministic, and why the seed rule belongs in the account context, you understand the heart of PDA usage.

Common PDA Mistakes

Mistake 1: treating PDAs like random addresses

They are not random.

Their value comes from being reproducible.

Mistake 2: changing the seed formula between instructions

If creation uses one formula and update uses another, your validation model is broken.

Mistake 3: relying only on the signer and ignoring stored authority

A signer check alone is often not enough.

Use the PDA rule and the stored authority rule together when the business logic requires both.

Mistake 4: using mutable business data as a seed casually

If a seed can change, your addressing model gets harder to preserve and review.

Mistake 5: forgetting what the bump is doing

The bump is part of deriving a valid PDA.

Do not treat it like random syntax noise.

One Good Practice Exercise

Add a UserProfile PDA to a fresh Anchor workspace with this rule:

  • namespace: b"profile"
  • identity: authority.key()

Then write two tests:

  1. the correct authority can create the PDA-backed profile
  2. a mismatched PDA address is rejected

That is a much better PDA exercise than jumping straight into a large escrow or vault design.

What Comes Next

The next lesson moves from deterministic state to token-aware state.

That is where PDAs start getting used for real authority flows, not just profile-style records.

Quick Check

Quick Check
Single answer

Why does seed order matter when you design a PDA?

Quick Check
Single answer

Why should creation and later validation use the same PDA seed formula?

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px