Revision — Writing Programs & Anchor
This part took you from the runtime you understood to the program you can write. Everything before it —
the account model, the Sealevel scheduler, the miniature
solmini runtime you may have built in the rust-playbook — described the machine from the outside. This part
put you at the author’s desk in front of it, and answered the book’s question at that altitude: how do you
write code for a machine that runs your transaction in parallel with thousands of others and trusts you to
have declared everything you touch?
The short answer, restated in one breath before we walk it slowly: a Solana program is stateless code that receives an untrusted, positional list of accounts, so correctness is nothing but parsing the bytes you were handed and checking every account by hand — and Anchor’s whole job is to make that checking declarative instead of heroic, without ever making it someone else’s responsibility.
The arc, in one pass
Section titled “The arc, in one pass”Read the six pages back to front and they tell a single story. Here is the shape of it.
native structure → manual checks → Anchor ──────────────── ───────────── ────── entrypoint!(process_ix) owner check #[program] fn per instruction (program_id, accounts, signer check #[derive(Accounts)] struct instruction_data) writable check #[account(...)] constraints one fn, hand-parse key / PDA check Context<T> deserializes for you discriminator check seeds = [...], bump ─────────────── ───────────── ───────────── nothing hidden, the danger, fully the checks are DECLARED, nothing enforced exposed the framework ENFORCES them
then, resting on all of it: PDAs = accounts your program owns at a deterministic address CPI = one stateless process calling another IDL = the program's typed interface, generated from the code localnet = solana-test-validator + anchor test = the loop that closes on the truthFrom the native entrypoint to hand-rolled checks
Section titled “From the native entrypoint to hand-rolled checks”The native program is the runtime’s raw truth with no framework in the
way. There is exactly one function — process_instruction(program_id, accounts, instruction_data) — reached
through the entrypoint! macro, and everything a real program does happens inside it. This is solmini’s
fn process(&self, accounts: &mut [Account], data: &[u8]) with production names attached: accounts is the
positional slice the transaction declared, and instruction_data is the opaque blob you must decode yourself.
That single signature is where the danger lives. The runtime hands your program a list of accounts and guarantees only two things about them: that your program was named as the target, and — this is the load-bearing guarantee for parallelism — that every account the transaction touches was declared in its footprint. It does not guarantee they are the right accounts. So the manual-checks page is the heart of native correctness: for each account you were given you must ask, by hand,
- owner — is this account owned by the program that’s allowed to write it? An account you did not create is just bytes an attacker fully controls.
- signer — did the party who must authorize this actually sign? Without the check, anyone can pass anyone else’s account.
- writable — is this account marked writable, matching what you intend to mutate?
- key / PDA — is this the specific account you expected, at the address you can re-derive?
- discriminator / type — do the first bytes say this is the account type you’re about to deserialize, and not a different struct of the same size?
Miss one and you have a real vulnerability, not a bug. This is why the native model is described as completely explicit and completely dangerous: nothing is hidden, and nothing is enforced.
From hand-rolled checks to Anchor’s constraints
Section titled “From hand-rolled checks to Anchor’s constraints”Anchor does not invent a new security model — it makes the one you
just wrote by hand declarative. #[program] marks the module whose functions are your instruction
handlers (the solmini process bodies). #[derive(Accounts)] defines a struct listing the accounts each
handler expects, and Context<T> hands your function that struct already deserialized. Every hand-written
check from the previous page becomes an annotation on a field:
#[derive(Accounts)]pub struct Increment<'info> { #[account(mut, has_one = authority)] // writable check + key check, declared pub counter: Account<'info, Counter>, // owner + discriminator check, from the type pub authority: Signer<'info>, // signer check, from the type}Read that against the list above and each line names a manual check: Account<'info, Counter> is the owner
and discriminator check (Anchor verifies the account is owned by this program and its type tag matches),
Signer<'info> is the signer check, mut is the writable check, and has_one = authority is a key check
tying two accounts together. The macro expands to the same if account.owner != program_id { return Err }
code you would have written — you can even ask cargo expand to show you. That is the point of having done it
the hard way first: no constraint is magic to you anymore, because you can name the hand-written check each one
generates.
The trade this part kept naming is worth restating plainly: Anchor makes the checks declared and
framework-enforced, so they are hard to forget — but it does not remove your responsibility for them. If you
leave off mut, or forget a has_one, or reach for UncheckedAccount and skip validation, the danger is
right back where it was in the native model. The magic is legible, not absent.
PDAs and CPI: convenience that never breaks the scheduler
Section titled “PDAs and CPI: convenience that never breaks the scheduler”The two ideas that sit on top of the account model — PDAs and CPI — are the ones most likely to feel exotic, so this is exactly where the throughline earns its keep. Neither is new machinery.
A PDA (program-derived address) is nothing more than an account your program owns, at an address it can
re-derive. It is a Pubkey computed by hashing a set of seeds together with the program’s id, off the
ed25519 curve so no private key can ever exist for it. In solmini terms it is simply “an Account whose
owner is the program” — the exotic part is only that the address is deterministic, so the program can find
its own state again without storing a pointer. That determinism is also why a PDA is a key check made cheap:
you re-derive the address from seeds and bump and compare, which is exactly the “is this the account I
expected?” check, now impossible to spoof.
A CPI (cross-program invocation) is one program calling another — in solmini terms, one stateless
process calling another process. When your program calls the System program to create an account, that is a
CPI. invoke performs the call; invoke_signed performs it while letting your program “sign” for a PDA it
owns, because a PDA has no key to sign with — the runtime accepts the program’s authority over its own seeds
instead.
Here is the load-bearing fact, and the reason these two live at the end of the part rather than the start: neither PDAs nor CPI let a program touch an account outside the transaction’s declared footprint. A CPI can only pass along accounts the caller already had in hand and already declared. So the scheduler’s invariant — the one that makes the whole global state machine parallelizable — holds straight through a chain of cross-program calls. The set of accounts a transaction can read or write is fixed before it runs, PDAs and CPI included. Authoring convenience is layered on top of the footprint invariant; it is never allowed to widen it.
transaction declares footprint: [ counter(w), authority(s), system_program(r) ] │ ▼ your #[program] handler runs ──── CPI ───▶ System program's handler │ only these accounts can be passed along ─────┐ ▼ the scheduler already knew this exact set — so two non-overlapping transactions still run on two threads, in parallel, by constructionThat is the sentence to carry: declaring accounts and threading them through CPI is what keeps the machine parallel. The author’s convenience and the scheduler’s guarantee are the same mechanism seen from two ends.
The IDL and localnet: closing the loop
Section titled “The IDL and localnet: closing the loop”The IDL and the localnet loop are what turn a compiled program back into something a human and a client can work with. The IDL is a JSON description of your program’s instructions, accounts, and types, generated straight from the annotated code — the typed interface a TypeScript or Rust client uses to build correctly-shaped transactions without hand-encoding bytes. It is the machine-readable form of “here is exactly what this program expects,” which is the same question your account checks answer, now published outward for callers.
The localnet loop — solana-test-validator plus anchor test — is where you find out whether the checks
you declared are the checks you meant. You run a real validator on your own machine, deploy the program, and
drive it with tests that assert both the happy path and the attacks: pass the wrong owner, drop a signer,
substitute an account, and confirm the program rejects each one. That is the disciplined descendant of
solmini’s unit tests, which already asserted that a bad transfer left balances untouched. Testing sits last
in the part for a plain reason: you need a whole program before you have something to test.
Where this leaves you
Section titled “Where this leaves you”You can now read and write a real Anchor program and reason about its security from first principles, not from
faith in the framework. Every annotation maps to a mechanism you can name: #[account] is a typed buffer owned
by your program, Signer is a signature check, seeds/bump is a re-derivable address, CPI is one process
calling another. Nothing here is a spell; every piece is the runtime you studied in the earlier parts, now with
a name you can type.
Concretely, carry these forward into the capstone and into any program you read in the wild:
- A program’s accounts are an untrusted positional list. Owner, signer, writable, key, and type checks are not defensive extras — they are the program’s correctness.
- Anchor makes those checks declarative and enforced, but a missing constraint or an
UncheckedAccountputs the danger back exactly where the native model had it. - PDAs are accounts the program owns; CPI is process calling process. Both operate strictly inside the transaction’s declared footprint, so the scheduler’s parallelism survives them untouched.
- The IDL publishes your interface; the localnet loop proves your checks by attacking them.
The throughline held the whole way down: a single global state machine runs at hardware speed because every transaction declares its footprint, and your program’s job is to make sure the accounts inside that footprint are the right ones. The runtime gives you the parallelism for free; you supply the correctness. With that, you are ready to build the capstone — a full program whose every line you can defend.