Transactions & Instructions
The account model part settled where state lives: one flat global map from a 32-byte
Pubkey to an Account, with every piece of state — wallets, token balances, program data, even program
code — sitting at its own top-level, publicly addressable slot. That part ended on a promise it could not
yet pay off: because every account has an external address, a request to change state can name every
account it will touch, up front. This part is where that promise turns into a concrete data structure on
the wire.
A transaction is a client’s request to the runtime: “run this program logic against these accounts, and either apply all of it or none of it.” This part is about the shape and lifecycle of that request — the exact bytes a client signs and sends, and how the runtime reads them. It is deliberately not about writing program logic; that comes later, in the programs and Sealevel parts. Here we stay on the format: what a transaction is made of, why each field exists, and why the format is engineered the way it is.
The central claim of this part
Section titled “The central claim of this part”Everything in this part unpacks a single sentence. Read it slowly:
A transaction is an atomic, all-or-nothing array of instructions, and every instruction declares the accounts it will touch before it runs.
Two claims are bundled there, and both are load-bearing.
Atomic array of instructions. A transaction is not one action — it is an ordered list of instructions, each a call to one program. They execute in sequence, and they succeed or fail together: if instruction 3 errors, instructions 0–2 are rolled back as if they never happened. This is the same clone-then-commit atomicity the account model hinted at, now scoped to the whole request. You can, in one atomic unit, create an account, initialize it, and fund it — with no half-finished state ever visible.
Declared account access. This is the decision the entire part exists to explain. Every instruction lists exactly which accounts it will read or write, by address, and flags each as writable or read-only — before the runtime executes a single byte of program logic. The runtime does not discover a transaction’s footprint by running it. It reads the footprint off the transaction, like a manifest, while the transaction is still just data.
A transaction, at a glance ──────────────────────────
Transaction ├── signatures (proof the fee-payer & required signers approved these exact bytes) └── message ├── account keys [ Alice, Counter, SystemProgram, CounterProgram, ... ] ← the footprint ├── recent blockhash (a freshness stamp — bounds how long this tx is valid) └── instructions ├── ix0 → program: SystemProgram accounts: [Alice(w,s), New(w,s)] data: create └── ix1 → program: CounterProgram accounts: [New(w), Alice(s)] data: init(0)Notice what the runtime can compute from that structure without executing anything: the complete set of accounts the transaction reads and writes. That is its footprint, and knowing it in advance is the precondition for the whole rest of Solana.
Why declaring the footprint is the whole game
Section titled “Why declaring the footprint is the whole game”Recall the throughline: how do you build a single global state machine that runs at hardware speed without falling apart? Hardware speed means using every core of the machine at once — running many transactions in parallel. But two transactions can only run in parallel safely if they don’t clash over the same mutable state. The account model part framed the rule and the Sealevel scheduler will spend a whole part on it:
Two transactions conflict if they share an account and at least one of them writes it. Read–read on the same account is not a conflict.
That is “shared XOR mutable” — the same rule a read/write lock enforces on one structure and the borrow checker enforces on one variable — lifted to the level of whole transactions over accounts. To apply the rule, the scheduler must know each transaction’s read set and write set. If it had to run a transaction to find out what it touches, it could never schedule anything in parallel: it would already be running everything, serially, to learn the very thing it needed in order to parallelize.
Declaring the footprint up front breaks that circularity. The scheduler reads two transactions’ account lists, checks for a shared writable account, and — without executing either — decides whether they can share a batch. The transaction format is engineered so the scheduler can know a transaction’s footprint before running it. That single sentence is the throughline for this part. Every field you are about to meet exists either to declare the footprint, to authenticate the request, or to bound it in time and compute.
The circularity, and how declaration breaks it ──────────────────────────────────────────────
Without declaration: to schedule in parallel you must know footprints to know footprints you must run the tx to run the tx you must schedule it ← deadlock of reasoning
With declaration: footprint is DATA on the tx, readable before execution scheduler diffs read/write sets → parallel batches → runThis is the same trade the account model already made once, now paid a second time. There, external addressable state (instead of contract-private storage) is what let a footprint be named at all. Here, the transaction format is what actually names it. The two decisions are one idea seen from two sides: state is externally addressable, so requests can be externally described.
What a transaction is not
Section titled “What a transaction is not”Three quick clarifications, because the format only makes sense once these are ruled out.
- A transaction is not a program. The program is code that already lives on-chain as an account. A transaction merely invokes programs by their address and hands them accounts and a data blob. The logic is not in the transaction; a pointer to it is.
- A transaction is not free to touch whatever it likes at runtime. If a program tries to read or write an account the transaction never declared, the runtime rejects the transaction. The declaration is not a hint — it is a hard boundary the runtime enforces, which is exactly what makes it trustworthy enough to schedule against.
- A transaction is not open-ended in time or cost. It carries a recent blockhash that expires, and it runs under a compute budget it cannot exceed. Both exist so the runtime can bound a request it has not yet run — the same “know it before you run it” instinct as the footprint, applied to freshness and cost.
Roadmap for this part
Section titled “Roadmap for this part”Read these in order. Each page introduces exactly one component of the format and shows how it serves the footprint-before-execution goal above.
| # | Page | Concept it introduces | The one idea |
|---|---|---|---|
| 2 | The Anatomy of a Transaction | Signatures + message; the atomic instruction array | A transaction is signatures over a message, and the message is an all-or-nothing list of instructions. |
| 3 | The Instruction: Programs, Accounts, and Access Flags | One instruction = program id + account metas + data | The is_signer / is_writable flags are the footprint the scheduler reads. |
| 4 | The Message and the Recent Blockhash | The signed message; the blockhash as a freshness + replay bound | A transaction is only valid for a short window, and that window doubles as replay protection. |
| 5 | Versioned Transactions and Address Lookup Tables | The v0 format and ALTs | How the format grows the number of accounts a transaction can name past the wire-size ceiling. |
| 6 | The Compute Budget and Priority Fees | Compute units, the CU limit, priority fees | A transaction’s cost, like its footprint, is bounded and biddable before it runs. |
| 900 | Revision: Transactions & Instructions | — | Recap and self-test for the whole part. |
The columns are the same lesson twice: the middle names the mechanism, the right names the reason it exists. Every reason traces back to the one sentence — the runtime, and the scheduler behind it, must be able to reason about a transaction before executing it.
A first look at the shape (companion crate)
Section titled “A first look at the shape (companion crate)”The book’s companion crate, solmini, strips a transaction down to the three fields that carry the core
idea. It drops signatures, the blockhash, and versioning — production concerns you’ll meet on later pages —
and keeps only what the scheduler needs:
/// One account reference inside a transaction, with its access mode declared.pub struct AccountMeta { pub key: Pubkey, pub is_writable: bool, // the flag the conflict rule reads}
/// A transaction: which program to invoke, the accounts it touches (in the order/// the program expects them), and the opaque instruction `data`.pub struct Transaction { pub program: Pubkey, pub accounts: Vec<AccountMeta>, pub data: Vec<u8>,}Two honest simplifications are worth flagging now, because later pages will restore what’s missing:
solminifolds a transaction down to a single instruction (oneprogram+ one account list). Real Solana transactions carry an array of instructions over a shared account-keys table. The atomic all-or-nothing property lives at the transaction level; the teaching model keeps the footprint idea and postpones the array. The anatomy page restores it.solminitrusts the declaration. Its comment is explicit: “a transaction that lies about its footprint would be unschedulable safely — real Solana enforces honesty by failing any program that touches an undeclared account; here we simply trust the declaration, since the program only ever receives the accounts it named.” The lesson is the same either way: the account list is the footprint, and the whole runtime is built to believe it.
That is_writable flag is the entire hinge of this part. It is one bit per account, and on it rests whether
two transactions can run on two cores at the same time. The rest of the format is what makes that one bit
trustworthy: signatures prove the request is authorized, the blockhash proves it is fresh, versioning and
lookup tables let it name enough accounts to be useful, and the compute budget bounds what it may spend once
the scheduler lets it run.
The thread
Section titled “The thread”What does the transaction format force you to understand? That parallelism is not something the runtime adds after the fact — it is something the client declares up front, in the shape of the request itself. The account list is not bookkeeping the runtime fills in; it is a promise the client makes and the runtime enforces, and that promise is the only reason the scheduler can look at two pending transactions and know, before running either, whether they can share a core. Design the request so its footprint is legible as data, and the independent work was parallel all along. The rest of this part is just the fields that make that promise complete: authorized, fresh, large enough, and bounded in cost.
Check your understanding
Section titled “Check your understanding”- State the central claim of this part in one sentence, and name the two distinct properties it bundles together.
- What is a transaction’s footprint, and why must the runtime be able to compute it without executing the transaction? Tie your answer to the conflict rule.
- Explain the circularity that would arise if a transaction did not declare its accounts up front, and how declaration breaks it.
- A transaction is “atomic.” Concretely, what happens to instructions 0–2 if instruction 3 of the same transaction errors?
- The
solminiTransactionkeepsprogram,accounts, anddata, and itsAccountMetakeepskeyandis_writable. Which single field is “the entire hinge of this part,” and why does so much rest on one bit?
Show answers
- A transaction is an atomic, all-or-nothing array of instructions, and every instruction declares the accounts it will touch before it runs. The two bundled properties are (a) atomicity — the whole array of instructions applies or none of it does — and (b) declared account access — the read/write footprint is stated up front, as data, before any logic executes.
- A transaction’s footprint is the complete set of accounts it will read and write, each flagged read-only or writable. The runtime must know it before execution because the conflict rule (“two transactions conflict if they share an account and at least one writes it”) is what lets the scheduler run non-conflicting transactions in parallel — and to apply that rule it needs each transaction’s read and write sets before choosing what to run.
- Without declaration, to schedule in parallel you’d need to know footprints; to know a footprint you’d have to run the transaction; but to run it you’d have to schedule it — a circular dependency that forces everything to run serially. Declaration breaks it by making the footprint plain data on the transaction: the scheduler reads the account lists, diffs read/write sets, and decides parallelism without executing anything.
- All of instruction 3’s effects, and those of instructions 0–2, are rolled back — the transaction is all-or-nothing, so a failure anywhere leaves the world exactly as it was before the transaction, with no half-applied state ever visible.
is_writable. It is one bit per account, and the conflict rule turns entirely on it: two transactions sharing an account clash only if at least one marks that account writable. Whether two transactions can run on two cores at once therefore comes down to that single bit per shared account — which is why the rest of the format exists mainly to make the account list (and its flags) authorized, fresh, large enough, and trustworthy.