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
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:
seedsdefines the PDA inputsbumpis 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.
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:
- the correct authority can create the PDA-backed profile
- 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
Why does seed order matter when you design a PDA?
Why should creation and later validation use the same PDA seed formula?