Skip to content

Program Derived Addresses (PDAs)

On Stateless Programs Act on Accounts we established the shape of the whole model: a program is pure code, and every scrap of mutable state lives in a separate account the program owns. That leaves one very practical hole. If a program holds no state and no address book, how does it find the account for a given user next time? A program that manages one balance per user has to store those balances somewhere, and it has to be able to locate each one again — deterministically, with no lookup table, from a stateless function.

Program Derived Addresses are the answer. A PDA is an account whose address is computed from the owning program’s id plus a few chosen seeds, rather than being the public half of a keypair someone generated. That single change — an address with no private key — is what turns a stateless program into something that can keep organized, findable, per-user storage and even act as an authority over the accounts it owns. This page builds up exactly why the address has no key, how the derivation forces that property, and what a program unlocks by exploiting it.

The problem: a stateless program needs findable slots

Section titled “The problem: a stateless program needs findable slots”

Picture a staking program. Alice deposits; the program must record Alice’s stake in an account. Bob deposits; a different account. A month later Alice withdraws, and the program must find her account again — from nothing but her request and its own code.

The naive fix is a lookup table: a master account mapping user → stake account. But that table is itself state the program has to store, grow, and pay for, and every lookup is a second account read. Worse, it re-centralizes exactly what Solana spread out into external accounts. We want the opposite: a rule that, given the same inputs, always names the same address — so the address is the lookup.

Naive: a program-owned lookup table
────────────────────────────────────
request("alice") ─► read table account ─► "alice lives at 0x9f…" ─► read that account
(extra state, extra read, must be maintained)
PDA: the address IS the lookup
───────────────────────────────
request("alice") ─► address = derive(program_id, ["stake", alice_pubkey]) ─► read that account
(pure function — no table, no extra read)

A PDA makes the address a pure function of its inputs. Same program id, same seeds, same address — every time, computed by anyone, on or off chain, with no state consulted.

Deriving the address: a hash forced off the curve

Section titled “Deriving the address: a hash forced off the curve”

Here is the mechanism. A PDA’s address is a hash:

address = SHA-256( seeds ‖ bump ‖ program_id ‖ "ProgramDerivedAddress" )

The seeds are chosen by the program author — typically a fixed label plus something that identifies the slot, like ["stake", alice_pubkey]. The program id ties the address to this program. But the interesting byte is the bump, and to see why it exists you need one fact about Solana addresses.

A normal Solana address is an ed25519 public key: a point on the ed25519 elliptic curve, whose matching private key can produce signatures. Roughly half of all 32-byte values happen to be valid points on that curve; the other half are not points at all — they lie off the curve, and no private key can ever correspond to them.

A PDA is defined to be an address that lies off the curve. That is the whole trick: because the address is off the curve, no keypair exists that controls it, and none ever can. There is no private key to lose, leak, or be forged — the address is keyless by construction.

But a plain hash of your seeds lands off the curve only about half the time. So the derivation adds the bump: a single extra byte. The runtime starts at bump 255 and counts down, hashing seeds + bump + program_id at each step, until the result happens to fall off the curve. The first bump that works is the canonical bump, and the resulting off-curve address is the PDA for those seeds.

find_program_address(seeds, program_id):
for bump in 255, 254, 253, … :
candidate = SHA-256(seeds ‖ [bump] ‖ program_id ‖ "ProgramDerivedAddress")
if candidate is OFF the ed25519 curve:
return (candidate, bump) # canonical PDA + the bump that found it
# (statistically, a valid bump is found almost immediately)

Because the search is deterministic and always starts at 255, everyone who runs it with the same seeds gets the same address and the same canonical bump — no coordination required.

Store the bump so you don’t search twice

Section titled “Store the bump so you don’t search twice”

Finding the bump means hashing up to a few times — cheap off chain, but on chain, compute is metered. The standard pattern is to compute the PDA once off chain with find_program_address, then pass the bump into the instruction (or read it back from the account itself), so on-chain code can verify the address with a single hash using the known bump rather than re-running the search. In Anchor this is the bump on an #[account(seeds = …, bump)] constraint: the framework stores and re-supplies the canonical bump for you.

Now the staking program’s problem dissolves. It never stores a table. It just derives:

// Real Solana derivation (solana_program). Off chain a client does the same call
// with the same seeds and gets the same address — no on-chain lookup needed.
use solana_program::pubkey::Pubkey;
fn stake_account(program_id: &Pubkey, user: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[b"stake", user.as_ref()], // seeds: a label + who it's for
program_id, // ties the address to THIS program
)
// returns (pda_address, canonical_bump)
}

Every user gets exactly one stake account, at an address anyone can recompute. To read Alice’s stake, a client derives ["stake", alice]; to read Bob’s, ["stake", bob]. The seeds are a naming scheme, and the derivation is the directory. One account per user, one account per market, one config account per program — all addressable by a rule instead of a stored map.

This connects straight back to the model from Everything Is an Account: a PDA is still just an ordinary account — lamports, a data buffer, an owner. What is special is only how its address was chosen. It is owned by the program (so only the program may write it), and its address is reproducible from seeds (so it is always findable).

The second superpower falls out of the same off-curve fact. Because a PDA has no private key, it can never sign a transaction the ordinary way. Instead, the runtime lets the owning program sign on the PDA’s behalf — but only for PDAs derived from that program’s own id.

The call is invoke_signed: when a program makes a cross-program call, it can pass the seeds of a PDA it controls. The runtime re-derives the address from those seeds and the calling program’s id; if it matches the PDA in the instruction, the runtime treats that PDA as a signer for the inner call.

Program X wants its PDA to authorize a token transfer:
X calls invoke_signed(
instruction: token::transfer(from = X_pda, to = bob, amount),
signer_seeds: [ b"vault", [bump] ],
)
runtime re-derives PDA from (seeds, X's program_id)
matches the `from` account? ─► YES ─► PDA counts as a signer, transfer proceeds
─► NO ─► rejected

This is how a program becomes an authority: a vault PDA can own tokens and only the vault’s program can authorize moving them; an escrow PDA can hold funds until the program’s logic releases them; a mint’s authority can be a PDA so only the program can mint. No human holds a key that can override the program — the program’s code is the key. The off-curve address that made the PDA unforgeable is the very thing that lets the program hold power no external signer can seize.

Crucially, invoke_signed only works for PDAs of the calling program. A program cannot sign for another program’s PDAs, because it cannot produce seeds that derive to them under the other program’s id. Ownership and signing authority stay bound together.

This is Ethereum’s mapping, turned inside out

Section titled “This is Ethereum’s mapping, turned inside out”

If you have written Solidity, you have solved the staking problem with one line:

mapping(address => uint256) public stakes; // storage lives INSIDE the contract

The mapping is state inside the contract. stakes[alice] is a slot the contract computes and reads internally; nobody outside addresses it directly. It works, but it is exactly the design The Account Model overview explains Solana rejected: state hidden inside code means the runtime cannot know which slots a call will touch until it runs, so calls must execute serially.

PDAs give Solana the same convenience — a keyed slot per user, derived not stored — but as external, independently addressable accounts:

Ethereum Solana
──────── ──────
mapping(address => Stake) inside the one PDA account per user, OUTSIDE the
contract; slot = hash(key, slot#) program; address = derive(seeds, program_id)
state hidden in contract storage state in named external accounts
→ footprint unknown until run → footprint declarable up front
→ runtime executes calls serially → runtime can parallelize (Part: Parallel Execution)

Same mental model — “look up the record for this key” — but Solana pays it out as real accounts the transaction can name in advance. That is the whole reason the account model exists, and the contrast is drawn in full in Contract-Owned Storage vs External Accounts. The convenience Ethereum bakes into a contract, Solana recovers with a derivation rule — and gets a parallelizable footprint as the reward.

Under the hood — creating a PDA account, and why the owner rule holds

Section titled “Under the hood — creating a PDA account, and why the owner rule holds”

Deriving an address does not create the account; it only names it. To bring a PDA into existence, the program calls the System program (via invoke_signed, signing as the PDA) to allocate its data buffer, assign its owner to the program, and fund it to be rent-exempt. From that point the runtime enforces the invariant from the account model: only the owning program may mutate that account’s data or debit its lamports.

The bump matters here for safety. Only the canonical (highest) bump is what find_program_address returns, but other bumps can also produce off-curve addresses for the same seeds — so a program that accepts an attacker-supplied bump can be tricked into using a different, attacker-chosen PDA. The rule is: derive with find_program_address (canonical bump) or constrain to it in Anchor, and never trust a bump you did not verify. This is the PDA-flavored version of “validate every account you are handed.”

  • Why does it exist? Because a stateless program still needs stable, per-user storage it can find again, and it needs a way to authorize actions on accounts it owns — both without holding any private key. PDAs give it an address that is computed, not generated, and keyless by construction.
  • What problem does it solve? Two: findability (locate the right account from seeds alone, no lookup table, no extra read) and authority (a program can sign for accounts it owns via invoke_signed, so code — not a human keyholder — controls the funds).
  • What are the trade-offs? Addresses are unforgeable and reproducible, but derivation costs a hash (store the bump to avoid re-searching), and account validation becomes your job: accept the wrong seeds or a non-canonical bump and you have opened a door. The convenience is real; the discipline is mandatory.
  • When should I avoid it? When the account genuinely needs an off-chain owner who signs with a real key (an ordinary user wallet), or when a random keypair account is simpler and no one needs to re-derive the address. Do not force a PDA where a plain keypair account fits.
  • What breaks if I remove it? Stateless programs lose organized per-user storage (back to lookup tables and the centralization the account model exists to avoid) and lose the ability to be an authority — no program-owned vaults, escrows, or mint authorities. Much of Solana’s DeFi is unbuildable.
  1. A PDA has no private key. What specific property of its address guarantees that, and how does the derivation force it?
  2. What is the bump byte, why is it needed, and why does the search almost always finish in one or two tries?
  3. A program needs one storage account per user and holds no state of its own. Explain how PDAs let it find the right account without storing any lookup table.
  4. What does invoke_signed let a program do that an external keyholder cannot do to that same account, and why can a program only sign for its own PDAs?
  5. Ethereum solves per-user storage with mapping(address => Stake) inside the contract. What does Solana do instead, and why does that difference help the runtime parallelize?
Show answers
  1. Its address lies off the ed25519 curve, so it is not a valid public key and no private key can correspond to it. The derivation hashes seeds + bump + program_id and (via the bump search) deliberately selects a result that is off the curve — keyless by construction, not by convention.
  2. The bump is a single byte added to the seeds so the derivation can be nudged until the hash lands off the curve. It is needed because a plain hash of the seeds is off-curve only about half the time. Since each bump has a ~50% chance of being off-curve, the search (starting at 255 and counting down) succeeds on the first try ~50% of the time and within two tries ~75% of the time.
  3. The account’s address is a pure function of its seeds: derive(program_id, ["stake", user]). To find Alice’s account the program (or any client) re-derives with Alice’s key as a seed and gets the same address every time. The naming scheme is the directory, so no table is stored, maintained, or read.
  4. invoke_signed lets the program sign as the PDA for a cross-program call, making the PDA an authority over accounts it owns (vaults, escrows, mint authority) — power no external signer holds, since the PDA has no key. A program can only sign for its own PDAs because the runtime re-derives the address from the supplied seeds and the calling program’s id; it cannot produce seeds that derive to another program’s PDA.
  5. Solana stores each record as an external, independently addressable account at a PDA (derive(seeds, program_id)) instead of a slot hidden inside contract storage. Because the state is in named external accounts, a transaction can declare which accounts it touches up front, so the runtime can tell which transactions are independent and run them in parallel — impossible when state is hidden inside a contract and its footprint is unknown until it runs.