PDAs: addresses with no private key
A normal Solana account is controlled by whoever holds its private key. That works fine for wallets, where a person is in charge. It does not work for programs. Programs can't hold keys, can't sign with them, can't be trusted to keep one secret from anyone watching the chain. The fix is a different kind of address: one derived from a program ID and some seeds, with no private key in existence anywhere. The program signs for that address using the seeds themselves. These are called Program-Derived Addresses, or PDAs, and they're how every nontrivial Solana program manages its own state.
Why PDAs have to exist
A normal account's address is the public key of an ECDSA keypair. To authorize anything that account does, you sign a transaction with the matching private key. That model is fine when there's a human or a server holding the key in secret.
A program can't do this. The program is open-source bytecode running on every validator at once. Anything the program "knows" is visible to anyone running it. If a program tried to hold a private key, every validator would see it, which means every validator could sign with it, which means the key is effectively public, which means it isn't a key at all. The whole concept of a key the program controls just collapses.
But programs need to control accounts. They need vaults that hold user deposits. They need state accounts that only the program is allowed to mutate. They need to sign for token transfers out of their own pools. None of that works with the standard keypair model.
The resolution is to invent a kind of address that nobody can sign for in the normal way, and grant the program a special ability to sign for it instead. That's what a PDA is. A 32-byte value that looks like a public key but isn't one, derived deterministically from the program's address and some seeds chosen by the developer. No private key exists for it, because the derivation lands at a point that isn't on the secp256k1 curve, and the curve is where private keys come from. The runtime gives the program a back door: if a program submits the seeds, the runtime treats it as authorization for the PDA those seeds derive to.
Both halves of the diagram produce the same kind of artifact: a 32-byte address the runtime understands. The difference is in how that address is reached and who's allowed to sign for it. A wallet's address is reachable through a private key. A PDA's address is reachable only by re-running the derivation with the seeds.
The bump trick
The derivation looks roughly like this. Take the seeds, append a single byte called the bump, append the program ID, append a tag string, and hash the whole thing with SHA-256. The result is 32 bytes. If those 32 bytes happen to land on the secp256k1 curve, the derivation could have a corresponding private key, and the whole point of PDAs would be broken. So if the result is on the curve, the derivation rejects it and tries again with a smaller bump byte.
The bump starts at 255 and decreases. Roughly half of all 32-byte values land on the curve, so on average two or three tries are enough to find one that doesn't. The first off-curve result encountered, with the highest bump, is the canonical PDA for those seeds. The bump that produced it is the canonical bump.
The function that runs this loop is called find_program_address. You hand it the seeds and the program ID, and it returns the canonical PDA along with the bump that produced it. Off-chain code calls it before submitting a transaction so the right account address can be included. On-chain code mostly avoids find_program_address, since running the loop costs compute units, and instead uses a cheaper function called create_program_address that takes a known bump and checks it directly. The pattern is: compute the canonical bump once with find_program_address, store it, and from then on pass it in everywhere.
Anchor handles this for you when you write seeds = [...] and bump. The bump keyword without a value means "compute and verify the canonical bump." If you store the bump on the account, you can write bump = self.bump instead, which skips the search and uses the stored value. Most programs store the bump on the account they're deriving so subsequent instructions stay cheap.
Programs sign for their PDAs by re-deriving them
The reason all of this works is the runtime's special signing rule for PDAs. When a program invokes another program through a cross-program invocation, it can include a list of signers_seeds. The runtime takes each entry, re-derives a PDA from those seeds and the calling program's ID, and treats the resulting PDA as a signer of the inner call. No keypair has been involved.
This is how a vault PDA can authorize a token transfer out of its own associated token account. The program calls into the Token Program's Transfer instruction, passes the vault PDA as the authority, and provides the seeds that derive the vault PDA. The Token Program sees the authority field, the runtime confirms the program ID matches and the seeds re-derive correctly, and the transfer goes through. The vault has "signed" the transfer without any private key existing.
For this to be secure, only one program can ever sign for a given PDA: the program whose ID is mixed into the derivation. Two different programs cannot independently sign for the same PDA, because the program ID is part of the hash input. This is the foundation of program-owned state.
The address is the lookup
The second reason PDAs are powerful, beyond the signing ability, is what their derivation enables architecturally. Because the address is a hash of public inputs, you can encode application logic directly into the address.
Want one vault per user? Use seeds [b"vault", user.key()]. Each user's pubkey produces a unique PDA. To find a user's vault, you compute the PDA from their pubkey. No mapping table needed.
The mental shift is the same one anyone who's used content-addressed storage has already made. In a normal database, you'd have a users table mapping user IDs to vault row IDs, and you'd look up the vault by joining. With PDAs, the address of the vault is a hash of the user's identity. You don't look up the mapping. You compute the location from the identity itself.
This composes well. A vote record that's unique per user and per proposal can use seeds [b"vote", user.key(), proposal.key()]. A daily counter PDA could use [b"counter", day_number.to_le_bytes()]. Any tuple of public values that uniquely identifies the account you want can be the seeds. The runtime guarantees the resulting address is unique to that tuple under your program.
The constraint is that seeds together cannot exceed 32 bytes per seed and the total seed count is capped at 16. That's plenty for almost any indexing scheme you'd want, and it's a small price for the architectural simplicity.
Storing the bump
One last practical detail. When you initialize a PDA account, you'll usually store the canonical bump as a field on the account itself. The next instruction that needs to sign for this PDA can read the bump from the account in one fetch instead of running find_program_address again. The savings add up: a single find_program_address can cost 1,500 to 12,000 compute units depending on how many bumps it has to try, while reading a stored bump from an already-loaded account is essentially free.
The convention shows up in account structs as a field like pub bump: u8, populated in the init instruction by the value Anchor computed, and read back in every subsequent instruction via bump = state.bump on the seeds constraint. This is the standard idiom and worth adopting from your first program.
What a PDA does and doesn't change
A PDA does not change anything about the account model. The account at a PDA still has the same five fields: lamports, data, owner, executable, and an address. Reading from it works exactly the way reading from any account works. Writing to it requires it to be owned by the program, exactly the way any program-owned account requires.
What changes is who can sign for it. A normal account's signature comes from a keypair. A PDA's "signature" comes from a program re-deriving the seeds inside a cross-program invocation. The runtime treats the two as equivalent for authorization purposes.
That sentence is the whole conceptual core. Once you've internalized it, every PDA pattern in real Solana code follows: programs creating accounts they own, programs signing for token transfers, programs maintaining per-user state without storing a key per user, programs holding pool funds without anyone holding the pool's key.