Program-Derived Addresses and Cross-Program Invocation
The previous page showed how Anchor’s #[derive(Accounts)] turns the manual account checks from parsing accounts by hand into declarative constraints the framework enforces before your handler runs. That got you a program that can safely read and write the accounts a transaction hands it. This page adds the two moves that turn such a program from a leaf into a full participant in the state machine: it can own accounts at addresses it controls without any private key (Program-Derived Addresses), and it can call other programs mid-execution (Cross-Program Invocation).
Both of these were introduced from first principles earlier in the book — PDAs as off-curve geometry and CPI as composition inside the runtime. This page assumes that what and teaches the how you write it: the exact calls (find_program_address, invoke_signed, CpiContext), the constraints Anchor gives you, and the one invariant that, if you break it, breaks the parallel scheduler from solmini. Everything here slots onto the same model: accounts owned by programs, and process calling process.
A PDA from the author’s seat: find_program_address
Section titled “A PDA from the author’s seat: find_program_address”Recall the account definition you have carried since Part 1 — an account is lamports + data + owner. A PDA is just an account; the only special thing is its address, which is derived deterministically from seeds plus your program id, and deliberately pushed off the ed25519 curve so no private key exists for it. (The geometry — why off-curve means keyless — is the cryptography page; the derivation algorithm is Deriving a PDA.)
As a program author you touch this through one function, find_program_address:
use anchor_lang::prelude::Pubkey;
// seeds: an ordered list of byte-strings you choose.// program_id: your program's id.// Returns the off-curve PDA and the canonical bump.let (pda, bump): (Pubkey, u8) = Pubkey::find_program_address( &[b"vault", user.key().as_ref()], &program_id,);find_program_address hashes seeds ++ bump ++ program_id ++ "ProgramDerivedAddress" with SHA-256, tests the result against the curve, and — if it landed on the curve — decrements the bump and tries again. It returns the first (address, bump) pair that lands off the curve. Because the search is deterministic, a client on a laptop and your program running in the cluster compute the same address from the same seeds, with no network round-trip. That is what lets a program compute where a piece of state lives instead of storing a lookup table.
seeds = [ b"vault", Alice.key ] program_id = YourProg │ ▼ find_program_address: bump = 255 → hash(...255...) on-curve? yes → reject bump = 254 → hash(...254...) on-curve? no → ACCEPT │ ▼ (pda = <off-curve 32 bytes>, bump = 254) ← "Alice's vault under YourProg"The bump, and why you store the canonical one
Section titled “The bump, and why you store the canonical one”The bump is the byte the search counted down. The canonical bump is the first value (highest, counting from 255) that produces an off-curve address — the one find_program_address returns. This matters for a subtle reason: there can be more than one bump that yields an off-curve address for the same seeds. If your program let a caller pass in any bump, an attacker could supply a different valid bump and derive a different PDA than the one your logic expects — a second, look-alike account under the same seeds. Two addresses for “the same” state is a collision waiting to be exploited.
So the rule is: derive with the canonical bump, then store it and reuse the stored one. find_program_address gives you the canonical bump. Save it in the account’s data on initialization; on every later instruction, re-derive with create_program_address (the cheap variant that takes a fixed bump and does not search) using the stored bump, and confirm the address matches. Anchor bakes this into a constraint:
#[derive(Accounts)]pub struct Withdraw<'info> { #[account( mut, seeds = [b"vault", user.key().as_ref()], // the seeds, minus the bump bump = vault.bump, // the CANONICAL bump you stored on init )] pub vault: Account<'info, Vault>, pub user: Signer<'info>,}On init you write vault.bump = ctx.bumps.vault (Anchor computes the canonical bump for you and hands it over). On every later call, bump = vault.bump tells Anchor to re-derive with that exact bump and reject the instruction unless the passed-in vault account’s address matches. No search, no ambiguity, no way to slip in an alternate-bump twin. Seed validation is not optional decoration — it is the check that makes “this really is the PDA I mean” true.
invoke_signed: how a program signs for its PDA
Section titled “invoke_signed: how a program signs for its PDA”A PDA has no private key, so it cannot produce a signature. Yet the token program will only move tokens out of a vault if the vault’s authority signs. If that authority is a PDA your program owns, how does your program authorize the move?
It does not sign — it calls invoke_signed and hands the runtime the seeds instead of a signature. The runtime re-derives the address from those seeds plus your calling program’s own id; if the result equals the account you are trying to sign for, it flips that account’s is_signer flag to true for the duration of the inner call. No cryptography runs; “signing” is a verified re-derivation. (The runtime-level mechanics are the CPI-and-PDAs page.)
Your program owns vault_authority = derive(["vault", Alice], YourProg)
invoke_signed( instruction = TokenProgram::Transfer { amount }, accounts = [vault_tokens, dest, vault_authority], signer_seeds= [ [ b"vault", Alice.key, &[bump] ] ] ← seeds, NOT a key ) │ ▼ runtime, before running TokenProgram: re-derive addr = find(seeds ++ YourProg.id) addr == vault_authority ? ├─ yes → mark vault_authority is_signer = true (this call only) └─ no → reject the CPI │ ▼ TokenProgram sees a signing authority → authorizes the transferNotice the seeds you pass to invoke_signed include the bump as a final one-byte seed (&[bump]), because you are asking the runtime to reproduce the exact off-curve address — it does not search. Use the canonical bump you stored, and the derivation matches. Because your program id is mixed into the hash, only your program can produce a valid invoke_signed for a PDA derived under it — a different program running the same seeds derives a different address and is rejected. Authority is partitioned per-program by geometry, not by a guarded secret.
CPI: process calling process
Section titled “CPI: process calling process”Cross-Program Invocation is the capstone’s “process calling process” made literal. Recall the shape of a program from solmini: fn process(accounts: &mut [Account], data: &[u8]). A transaction names a top-level program and hands it accounts. CPI is what happens when that program, mid-process, needs another program to do part of the work.
It does not import or link the callee’s code. It issues a syscall (invoke, or invoke_signed when a PDA must sign) that says: “Runtime, run program X on this instruction, with this subset of my accounts.” The runtime loads X, gives it a fresh sandbox, runs its process, and returns. The callee receives the same (accounts, data) contract as any top-level call — it cannot tell whether a user or another program invoked it. That uniformity is what makes Solana composable: any program is callable by any other over one account-passing interface.
In Anchor you assemble a CPI through a CpiContext: the program you are calling, plus the exact accounts to pass along.
use anchor_lang::prelude::*;use anchor_spl::token::{self, Transfer, Token, TokenAccount};
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> { // The seeds that authorize the vault PDA to sign. Note the canonical bump, // stored on the vault account, tacked on as the final seed. let user_key = ctx.accounts.user.key(); let signer_seeds: &[&[&[u8]]] = &[&[b"vault", user_key.as_ref(), &[ctx.accounts.vault.bump]]];
// Build the CPI: which program, which subset of accounts. let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.vault_tokens.to_account_info(), to: ctx.accounts.dest.to_account_info(), authority: ctx.accounts.vault.to_account_info(), // the PDA }, signer_seeds, // ← makes it invoke_signed );
// Call the token program. This is process → process. token::transfer(cpi_ctx, amount)}CpiContext::new produces a plain invoke; CpiContext::new_with_signer attaches the PDA seeds and produces an invoke_signed. Either way, the accounts field is a subset of the accounts this transaction already declared — your program passes along what it holds; it cannot conjure new accounts into existence.
The re-check that keeps CPI safe
Section titled “The re-check that keeps CPI safe”The runtime does not trust your program to have set up the call honestly. At the boundary it re-verifies that every privilege you pass down is one you were granted: an account can go to the callee writable only if you held it writable, and as a signer only if you held it as a signer (or proved a PDA via invoke_signed). Privileges narrow on the way down, never widen. This is the borrow checker’s shared XOR mutable, lifted to capabilities and enforced across the call boundary — the reason you can safely call untrusted programs: the worst a malicious callee can do is misuse the exact privileges you already held, never more.
The transitive-footprint invariant
Section titled “The transitive-footprint invariant”Here is the invariant that ties this whole page back to the throughline — a single global state machine that runs at hardware speed without falling apart. It is the one rule you must not break.
Recall the deal that buys Solana its parallelism, from declared footprints: every transaction lists up front every account it will touch, each marked read-only or writable. The solmini scheduler reads only those lists to decide which transactions can run on different cores at once. Two transactions conflict when they share an account and at least one writes it; non-conflicting transactions run in parallel. The scheduler never executes code to find out what an account is touched — that would defeat the entire design (it is exactly why the EVM runs serially).
CPI complicates this. When your top-level program CPIs into the token program, the token program touches accounts too — the vault, the destination, the authority. Those accounts must already be in the top-level transaction’s declared list. A CPI cannot introduce an account the transaction never named.
top-level tx declares: [ user(s), vault(w), vault_tokens(w), dest(w), token_program(r), YourProg(r) ] │ ▼ YourProg.process CPIs the token program, passing: [ vault_tokens(w), dest(w), vault(signer via seeds) ] │ └─ every one of these is ALREADY in the declared list ✓
If the CPI needed some account NOT in the list ─► the transaction fails.This is the transitive footprint: the declared account list must be the union of every account touched by the top-level instruction and every account any CPI it makes will touch, transitively, all the way down (capped at CPI depth 4). If it weren’t — if a nested call could reach an undeclared account — the scheduler’s conflict analysis would be incomplete. It might place two transactions in the same parallel batch believing they are disjoint, while one of them, three CPI levels deep, writes an account the other reads. That is a data race on global state: the exact thing the whole account-locks design exists to make unrepresentable.
So the invariant is load-bearing for correctness, not convenience: the top-level transaction must declare every account any CPI touches, or conflict-freedom breaks. In practice this means when you build a transaction that calls a program that itself CPIs, you must include the inner program’s accounts in your outer account list. Anchor’s IDL and generated clients help you assemble that full list; the next page is where you see it come together in tests against a localnet.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because a program with no keyless address of its own can only mutate accounts a human hands it, and a program that cannot call other programs is an island. PDAs give code an address it controls without a secret; CPI gives it a door to call other programs — together they promote a program from a leaf into a first-class entity in the state machine.
- What problem does it solve? “Who signs when there is no human, and how do programs compose safely?”
invoke_signedlets a program authorize actions on accounts it owns by re-derivation instead of a guarded key, andCpiContextlets one program borrow another’s logic while the runtime re-checks that no privilege was widened across the boundary. - What are the trade-offs? You gain keyless, deterministic ownership and permissionless composability — but authority now rests on your own seed validation (forget it and you reopen Wormhole), every CPI spends stack and compute budget, depth is capped at 4, and you must carry the entire transitive footprint in the top-level account list or the scheduler’s conflict-freedom breaks.
- When should I avoid it? Use a normal on-curve keypair when a human should be the authority — a PDA has no key precisely so no person can act for it. And inline work that is genuinely internal rather than paying a CPI’s setup cost; never CPI over accounts or into a program id you have not validated.
- What breaks if I remove it? Without PDAs, programs cannot own value or sign for anything autonomously — vaults, escrows, and program-controlled mints collapse back to human-held keys. Without CPI, composability collapses: no shared token standard, no on-chain integrations, and DeFi, which is composition all the way down, cannot exist.
Check your understanding
Section titled “Check your understanding”- What does
find_program_addressreturn, and what is the difference between it andcreate_program_address? When do you use each? - What is the canonical bump, and why must a program store it and reuse the stored value instead of accepting a bump passed in by the caller?
- A PDA has no private key. Walk through what your program hands
invoke_signedand what the runtime checks before it marks the PDA as a signer for the inner call. - In an Anchor CPI, what is a
CpiContext, and what is the difference betweenCpiContext::newandCpiContext::new_with_signer? What privilege rule does the runtime enforce on the accounts you pass along? - State the transitive-footprint invariant. Concretely, what goes wrong in the parallel scheduler if a CPI three levels deep writes an account the top-level transaction never declared?
Show answers
find_program_addresstakes seeds and a program id and searches — hashing with a bump that counts down from 255 until the result lands off the ed25519 curve — returning the(address, canonical bump)pair.create_program_addresstakes seeds and a fixed bump and does not search; it derives one address from that exact bump (erroring if it happens to be on-curve). Usefind_program_addressonce, typically on init, to discover the canonical bump; usecreate_program_address(or Anchor’sbump = stored_bump) on every later call to cheaply re-derive with the stored bump.- The canonical bump is the first (highest) bump value that produces an off-curve address — the one
find_program_addressreturns. More than one bump can yield a valid off-curve address for the same seeds, so if a program accepted an arbitrary caller-supplied bump, an attacker could derive a different, look-alike PDA under the same seeds — two addresses for “the same” state, a collision. Storing the canonical bump and re-deriving with it pins the account to exactly one address. - It hands the runtime the seeds (including the canonical bump as the final seed byte), not a key or signature. The runtime re-derives the address by hashing those seeds together with the calling program’s own id and pushing it off-curve with that bump; if the result equals the account the program is trying to sign for, it sets that account’s
is_signer = truefor the inner call only. No cryptography runs — signing is a verified re-derivation, and only the program whose id is in the hash can produce a match. - A
CpiContextbundles the program being called with the exact subset of accounts to pass to it.CpiContext::newproduces a plaininvoke;CpiContext::new_with_signerattaches PDA seeds and produces aninvoke_signed, letting a PDA authority sign. The runtime re-checks every account at the boundary: it can be passed writable only if the caller held it writable, and as a signer only if the caller held it as a signer (or proved a PDA) — privileges narrow on the way down, never widen. - The top-level transaction must declare every account any CPI touches, transitively. If a nested CPI writes an undeclared account, the scheduler — which reads only the declared footprint to decide conflicts — cannot see that write. It may place this transaction in the same parallel batch as another that reads or writes that same account, believing the two are disjoint. Both run on different cores at once and collide on shared state: a data race on the global ledger, the exact failure the account-locks design exists to make unrepresentable.