Derive your first PDA from seeds
On Solana, programs need a way to store state without relying on a keypair generated by the client. This challenge shows how that works: derive an address from known inputs, then prove it changes only when the inputs change.
Derive your first PDA from seeds
Arc theme: PDAs and State
Web2 bridge: PDAs are like deterministic database keys derived from your program logic
Every address you have generated on Solana so far has had a private key sitting somewhere on your machine. The address you derive in the next twenty minutes does not, and that single difference is the entire reason your programs can own state.
The Scenario
You spent the last arc building a counter program in Anchor. It compiled, it ran, it refused the wrong wallet, and your tests went green. There is one thing it could not do, and the gap has been bugging you: it can only store state in an account whose keypair you generated yourself and passed in from the client. That works as we start. It does not work for a real application where you want one counter per user, or one vault per token, or one game per match, derived from inputs your program already knows about.
The Solana answer to this is a Program Derived Address. A PDA is a public key that lives off the ed25519 curve, which means no private key produces it, which means no human can sign for it. Instead, the program it belongs to can sign for it on demand. Today you are not going to use one in a program yet. You are going to derive one in a script, print it to the terminal, and look at it until the mechanic stops feeling magical and starts feeling like a hash function with one extra rule. Tomorrow you will use what you derive today to give every user their own counter account.
The Challenge
What you’ll need
- The counter Anchor project you built in Arc 9, with a known program ID in
Anchor.tomlandlib.rs - Node.js and the existing
@solana/web3.jsdependency that ships with every Anchor project - Your terminal in the project root
- A text editor
Steps
- Open
Anchor.tomland copy the program ID under[programs.localnet]. This is the same public key declared indeclare_id!inside yourlib.rs. Both must match for any of this to work. - From the project root, create a new file called
scripts/derive-pda.ts. - Paste the script below into that file, then replace
YOUR_PROGRAM_ID_HEREwith the program ID you copied:
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("YOUR_PROGRAM_ID_HERE");
const [pda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("counter")],
programId
);
console.log("Seeds: [\"counter\"]");
console.log("Program ID: ", programId.toBase58());
console.log("PDA: ", pda.toBase58());
console.log("Canonical bump:", bump);
- Read the script before you run it. The seed is a single byte string,
"counter". The functionfindProgramAddressSynchashes your seeds together with the program ID and a one-byte bump value, starting at 255 and counting down, until the hashed result lands at a point that is not on the ed25519 curve. The first bump that produces an off-curve address is the canonical bump, and that is the one returned. - Run the script with the command below and confirm the output:
npx ts-node --transpile-only scripts/derive-pda.ts
You should see a base58 PDA and a bump between 0 and 255. Roughly three out of four runs will give you 254 or 255 for a short seed list, with lower values like 252 or 253 showing up the rest of the time, because each candidate bump has about a fifty-fifty chance of landing on the curve.
6. Now change the seed. Edit the file so the seeds line reads:
[Buffer.from("counter"), Buffer.from("alice")],
Run the script again. The PDA is completely different. The bump may also differ.
- Change the second seed from
"alice"to"bob"and run a third time. Different PDA again. Same program ID, same first seed, but a different namespace. - Finally, restore the seeds to just
[Buffer.from("counter")], run the script a fourth time, and verify that the address matches your first run byte for byte. Determinism is the entire point.
Run it
npx ts-node --transpile-only scripts/derive-pda.ts
The --transpile-only flag tells ts-node to skip whole-program type checking and just run the file. Without it, the default Anchor tsconfig.json (which only pulls in mocha and chai types) will complain that Buffer and console are not defined.
What Just Happened
You turned a list of byte strings and a program ID into a public key without ever generating a keypair. The function ran a SHA-256 over your seeds plus the program ID plus a candidate bump, checked whether the resulting 32 bytes correspond to a valid point on the ed25519 curve, and if it did, decremented the bump and tried again. The first bump value that landed off the curve is what you got back. There is no private key that produces this address, and there never will be, because the curve has no point there to take a discrete log of.
The reason this matters is ownership. On Solana, accounts are owned by programs, and only the owning program can mutate the account’s data. A normal keypair account is owned by the System Program by default, and only the holder of the private key can authorize changes. A PDA derived from your program’s ID can be owned by your program, and your program can sign for it using the same seeds plus bump that produced it. You did not invoke any of that today. You only derived the address. But you now have the deterministic key that tomorrow’s program will reach for whenever it needs the counter for a particular user, without you ever passing it in from the client.
If you have used Web2 systems where a database row key is computed from a user ID plus a namespace, the analogy is close. The difference is that the Solana runtime treats the derivation as cryptographic proof of provenance, and the program that derived the address is the only thing in the world that can write to it.
Resources
- Program Derived Addresses, official Solana docs
- PDA Derivation, the algorithm in detail
- PDA reference in the Anchor docs
- QuickNode guide to PDAs in Anchor programs
Submission
Submit a screenshot of your terminal showing the four runs side by side, with the different seeds and the different PDAs they produced.