A Program Derived Address can make Solana state easy to find and safe for a program to control. A weak seed recipe can also merge unrelated records, break integrations after an upgrade, or give an instruction the wrong authority.
This guide turns PDA design into eight checks you can apply before mainnet. It covers seed domains, canonical bumps, Anchor constraints, CPI signing, account lifecycle, and the negative tests that expose bad assumptions.
New to ownership, executable programs, and data accounts? Begin with the Solana account model guide.
Checks 1 and 2: define identity before writing code
Check 1: write the identity rule in plain language
A PDA is a deterministic 32-byte address derived from a program ID and a list of byte-array seeds. Solana requires the result to sit off the Ed25519 curve, so no private key exists for it. The program can act for that address through runtime-verified signer seeds during a CPI.
Deriving an address does not create an account at that address. It also does not assign account ownership or allocate data. Your instruction must still create or validate the account and enforce its intended type.
Write one sentence before choosing seeds:
One position account exists for this market and this user under this program version.
That sentence exposes the identity fields. If two valid business objects can satisfy the same sentence and seed tuple, the namespace is incomplete.
Check 2: give every account family a domain seed
Start each recipe with a stable, type-specific prefix:
[b"position", market_pubkey, user_pubkey]
[b"vault", market_pubkey]
[b"config"]The prefix separates account families that otherwise use the same dynamic keys. Keep each seed at or below the current 32-byte limit and keep the complete list within the current 16-seed limit.
Never feed an arbitrary username, URL, or serialized object directly into a seed. Normalize the business value and hash it to a fixed 32-byte digest when the raw input can exceed the limit or has ambiguous encoding.
Document the exact byte encoding. The Rust program, TypeScript client, indexer, and migration script must derive the same address.
Checks 3 and 4: make derivation canonical and constrained
Check 3: use the canonical bump
PDA derivation adds a one-byte bump because a candidate hash can land on the Ed25519 curve. The canonical bump is the first valid bump found by searching from 255 downward.
Discover an address with find_program_address or the SDK equivalent. When the program already stores or receives a bump, verify it against the same seed recipe. Reject an arbitrary valid bump that would create a second identity for the same logical record.
Solana's current documentation also lists derivation compute costs. Avoid repeatedly searching for a bump inside a hot loop. Derive once, use Anchor's validated bump, and pass the exact signer seeds to the CPI that needs them.
Check 4: bind the PDA to its business relationships
Anchor's seeds and bump constraints prove that an account key matches a derivation. They do not prove every relationship stored inside the account.
Combine address validation with type, authority, and relationship checks:
#[derive(Accounts)]
pub struct UpdatePosition<'info> {
pub authority: Signer<'info>,
pub market: Account<'info, Market>,
#[account(
mut,
seeds = [
b"position",
market.key().as_ref(),
authority.key().as_ref(),
],
bump = position.bump,
has_one = market,
has_one = authority,
)]
pub position: Account<'info, Position>,
}The PDA recipe selects the expected address. Account<Position> checks ownership and the discriminator. has_one binds stored relationships to the accounts supplied for this instruction.
The Solana security checklist extends this review across the full program boundary.
Checks 5 and 6: control creation and signing
Check 5: separate initialization from repeat use
Choose init when the instruction must create a new record. Reserve init_if_needed for a business flow that permits repeat execution and applies the same authorization and state checks to existing accounts.
Each PDA account needs a written policy for:
- who pays for creation and growth
- the exact allocated space
- which fields establish initialization
- whether the address may be closed
- who receives reclaimed lamports
- whether the same address may ever be initialized again
Avoid treating an empty balance as proof that a business object never existed. Application state, close behavior, and future address-reuse rules need an explicit policy.
Check 6: reconstruct signer seeds at the CPI boundary
Only the program whose ID participated in derivation can sign for its PDA. The runtime verifies that authority when the program invokes another program with the original seeds and bump.
In Anchor, build the signer seed array from validated account data:
let market_key = ctx.accounts.market.key();
let signer_seeds: &[&[&[u8]]] = &[&[
b"vault",
market_key.as_ref(),
&[ctx.bumps.vault_authority],
]];
let cpi = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);
token_interface::transfer_checked(cpi, amount, decimals)?;Never reuse signer seeds from a different account family. Keep a state-owning PDA distinct from a PDA used only as a token authority. Give each authority the narrowest role its CPIs require.
Checks 7 and 8: preserve compatibility and prove rejection
Check 7: treat the seed recipe as a public contract
Anchor includes PDA seed constraints in the IDL, which can let compatible clients resolve accounts automatically. That convenience makes a seed change an integration change.
Version a namespace deliberately when identity must change:
[b"position", b"v2", market_pubkey, user_pubkey]Then define discovery and migration. Decide whether readers must check v1 and v2, whether old state stays readable, and how indexers distinguish them. A program upgrade cannot move existing account data by changing a constant.
Record the recipe beside your IDL and SDK. Indexers should derive or ingest the address using the same versioned contract. RPC Edge can improve the delivery and observation boundary for account reads, subscriptions, and transaction traces, but it cannot repair inconsistent seeds across clients.
Check 8: test invalid accounts, not only the happy path
Every PDA instruction needs negative cases. Assert that it rejects:
- the correct account type at the wrong PDA
- the correct PDA derived for another user or market
- a non-canonical or incorrect bump
- an account with a mismatched stored authority
- an account owned by the wrong program
- a duplicate mutable account where two roles must differ
- a repeat initialization that violates lifecycle policy
- a CPI signer assembled with the wrong seed order
Also test seed parity across Rust and each supported client SDK with fixed vectors. Store the expected address and bump in fixtures. A refactor that changes encoding should fail before deployment.
When a failure reaches transaction simulation, follow the five-layer debugging trace to separate derivation errors from instruction, CPI, state, and compute failures.
The 8-Check PDA Production Gate
Before shipping a PDA-backed account family:
- state the business identity in one sentence
- use a stable domain prefix and unambiguous byte encoding
- keep seeds within protocol limits and hash variable-length inputs
- derive and validate the canonical bump
- combine seeds with owner, type, authority, and relationship constraints
- document initialization, allocation, growth, closure, and reuse rules
- reconstruct CPI signer seeds only from validated values
- run cross-SDK fixtures plus rejection tests for wrong accounts and seeds
The durable artifact is a seed manifest containing the account family, program ID, ordered seed schema, encoding, bump policy, lifecycle owner, and test vectors. Keep it versioned with the IDL.
FAQ, sources, and next step
Is a PDA an account?
It is an address. An account must still be created at that address, funded, allocated, assigned to an owner, and initialized.
Can a user sign with a PDA?
No private key exists for a PDA. The deriving program can make the runtime treat it as a signer during a CPI by supplying matching seeds and bump.
Should I store the bump in account data?
Storing it can make validation and CPI construction explicit. The program must still bind it to the intended seed recipe and canonical address.
Can I change seeds during a program upgrade?
Code can change, but existing account addresses do not move. Treat a new recipe as a versioned namespace with a migration and discovery plan.
Primary sources
- Solana Program Derived Addresses
- Solana Cross Program Invocation
- Anchor PDA documentation
- Anchor account constraints
Continue with the Solana transaction guide for the layer that carries these accounts. When your seed model crosses multiple programs, client SDKs, and indexers, book a Builderz architecture review.



