Revision: Transactions & Instructions
This part turned a single promise from the account model into a concrete data structure on the wire. The promise was this: because every account has an external, top-level address, a request to change state can name every account it will touch, up front. This part is where that naming became bytes — the exact fields a client signs and sends, and how the runtime reads them before it runs anything.
Everything here unpacked one sentence, and it is worth saying once more before we walk back through the pieces:
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 live in that sentence — atomicity and declared access — and every field of the format serves one of them, or serves to authenticate, time-bound, or cost-bound the request around them. Let us recap the part as a whole, and see how each page added exactly one component to that structure.
The atomic array of instructions
Section titled “The atomic array of instructions”The anatomy page established the outermost shape. A transaction is two things stacked: a list of signatures, and a message those signatures cover. The message is where all the meaning lives; the signatures are just proof that the fee-payer and any other required signers approved these exact bytes.
Inside the message sits the property the whole book leans on: the instructions form an atomic array. A transaction is not one action. It is an ordered list of calls, each to one program, and they succeed or fail together. If instruction 3 errors, the effects of instructions 0, 1, and 2 are rolled back as if they never happened. No half-finished state is ever visible on-chain.
Transaction ├── signatures [ sig_fee_payer, sig_other_signer, ... ] └── message ├── account keys [ Alice, Counter, SystemProgram, CounterProgram, ... ] ├── recent blockhash └── instructions [ ix0, ix1, ix2, ... ] ← runs in order, all-or-nothingRecall why atomicity is enforceable so cheaply here. The solmini runtime shows the mechanism directly: each transaction runs against an owned clone of its accounts, and the runtime commits those writes back into the account database only if the transaction succeeded. A failed transaction never commits — its clone is simply discarded. Atomicity is not a rollback log bolted on afterward; it falls out of commit-on-success. Clone, run, and only then write back.
That is worth holding onto as the recurring shape of the whole part: the runtime prefers to know or bound a request before running it, and to make the act of running cheap to undo. Atomicity is the first instance. Footprint, freshness, and cost are the next three.
The instruction and its access flags
Section titled “The instruction and its access flags”The instruction page zoomed into one element of that array. A single instruction is three fields:
- a program id — the address of the on-chain program to invoke;
- a list of account references, each carrying its access flags;
- an opaque data blob the program interprets however it likes.
The account references are the load-bearing part. Each one flags the account as read-only or writable, and as a signer or not. Those flags are not documentation. They are the footprint declaration — the promise, made in data, of exactly what the instruction will read and write, stated before the runtime executes a single byte of program logic.
This is the hinge of the entire part, because of the conflict rule the account model handed us:
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 borrow checker’s rule — lifted from variables to accounts. To apply it, the scheduler needs each transaction’s read set and write set. The is_writable flag is where those sets come from. The companion crate makes it almost embarrassingly small:
/// One account reference inside a transaction, with its access mode declared.pub struct AccountMeta { pub key: Pubkey, pub is_writable: bool, // the one bit the conflict rule reads}
/// Do two transactions conflict? Only if they share an account and at least/// one of them writes it.pub fn conflicts(a: &Transaction, b: &Transaction) -> bool { for ma in &a.accounts { for mb in &b.accounts { if ma.key == mb.key && (ma.is_writable || mb.is_writable) { return true; } } } false}On that one bit per account rests whether two transactions can run on two cores at the same instant. Everything else in the format exists to make that bit trustworthy: the signatures prove the request is authorized, and the runtime enforces the declaration by failing any program that touches an account the transaction never listed. The declaration is a hard boundary, not a hint — which is exactly what lets the scheduler schedule against it without ever running it.
The message and the recent blockhash
Section titled “The message and the recent blockhash”The message page opened up the message’s structure and added the time bound. Two recaps matter here.
Index-based account references. The message keeps a single flat table of account keys, listed once, and every instruction refers to its accounts by their index into that table rather than by repeating the 32-byte address. A transaction that touches the same account from three instructions names it once and points at it three times. This is not only a wire-size saving — it means the message carries a single, canonical set of accounts, which is precisely the footprint the scheduler wants to read.
account keys: [0] Alice [1] Counter [2] SystemProgram [3] CounterProgram └──────────┬──────────┘ ix0 → program 2 (System), accounts [0, 1] ← references, not copies ix1 → program 3 (Counter), accounts [1, 0]The recent blockhash. Every message embeds a recent blockhash — the hash of a block produced in roughly the last couple of minutes. It does two jobs at once:
- Freshness / expiry. The blockhash is only valid for a bounded window (in the current protocol, on the order of ~150 blocks). Once it is too old, validators reject the transaction. A transaction the client signed but never landed simply expires instead of hanging around forever as a live liability.
- Replay protection. Because the blockhash is part of the signed bytes and validators remember which transactions they have already processed within the valid window, the same signed transaction cannot be replayed a second time. To resubmit, the client must re-sign against a new blockhash, producing different bytes.
Notice the pattern again: the blockhash lets the runtime bound a request in time before running it, the same instinct as declaring the footprint before execution. Fresh, and provably not a replay.
Versioned transactions and address lookup tables
Section titled “Versioned transactions and address lookup tables”There is a ceiling hiding in the design so far. A transaction is capped in total serialized size (~1232 bytes on the wire), and every account it names by full address eats 32 of those bytes in the keys table. That caps how many distinct accounts a legacy transaction can reference — a real problem for a DeFi instruction that must touch dozens of accounts in one atomic step.
The versioned-transactions page recapped the fix in two parts:
- Versioned transactions. The
v0format adds a version prefix so the wire layout can evolve without breaking older clients — the format grew a version number precisely so it could be extended. - Address Lookup Tables (ALTs). An ALT is an on-chain account that stores a list of addresses. A
v0transaction can then reference those addresses by a small index into the table instead of spelling out each 32-byte key inline. Publish the accounts once into an ALT, and afterward a transaction points at them cheaply.
Legacy: every account = 32 bytes inline → few accounts fit under the size cap
v0 + ALT: ALT account (on-chain) holds [ acctA, acctB, acctC, ... ] transaction says "index 0, 2, 5 from table T" → ~1 byte each far more accounts named under the same size capThe footprint idea is untouched — the transaction still declares every account it will touch, and the runtime still resolves the full set before scheduling. ALTs only change the encoding: they let a transaction name enough accounts to be useful for real programs without breaking the size ceiling or the up-front-declaration rule.
The compute budget and priority fees
Section titled “The compute budget and priority fees”The last page, on the compute budget, added the final bound: cost. Just as the runtime refuses to discover a transaction’s footprint by running it, it refuses to discover its cost by running it unbounded.
- Compute units (CU) and the CU limit. Every operation a transaction performs is priced in compute units, and each transaction runs under a CU limit it may not exceed. Run past the limit and the transaction fails — bounded, before it can monopolize a validator. This is metering: a request declares (or defaults to) a ceiling on the work it may do.
- Priority fees. On top of the base fee, a transaction can attach a priority fee — a small bid, per compute unit, for the leader to include it sooner when blockspace is contested. This turns block inclusion into an auction under load.
- Per-account (localized) fee markets. Crucially, that auction is scoped to the accounts a transaction writes. Because the footprint is declared up front, congestion on one hot writable account raises the priority-fee price for transactions touching that account — without taxing unrelated transactions elsewhere in the state. The same declared footprint that powers parallel scheduling also localizes the fee market.
Tie this back to the throughline of the whole book: how do you build a single global state machine that runs at hardware speed without falling apart? Metering keeps any one transaction from starving a validator; the per-account fee market keeps a hot spot in the state from congesting the whole chain. Both are throughput protections, and both are only possible because the transaction told the runtime what it would touch and how much it would spend — before it ran.
The part in one breath
Section titled “The part in one breath”Read the sentence one final time, now with every field slotted into it:
A transaction is an atomic, all-or-nothing array of instructions, and every instruction declares the accounts it will touch before it runs.
- Atomic array of instructions — signatures over a message, whose instruction list commits on success and rolls back on any failure.
- Every instruction — a program id, account references, and data, where the
is_writable/is_signerflags are the footprint. - Declares the accounts — a message with an index-based keys table, made fresh and replay-proof by a recent blockhash, and grown past the size ceiling by
v0transactions and address lookup tables. - Before it runs — under a declared compute budget and priority fee, so cost and congestion are bounded, per account, ahead of execution.
The unifying instinct across all five is one line: know or bound the request before you run it, and make the running cheap to undo. Footprint before execution. Freshness before acceptance. Cost before it can be spent. Commit only on success.
That is why the format looks the way it does. The scheduler that the Sealevel part spends its whole length on can look at two pending transactions and decide, without running either, whether they may share a core — because the client already wrote the answer into the shape of the request. Parallelism here is not something the runtime adds after the fact. It is something the client declares up front, and the rest of Solana is built to trust that declaration.
Where this part leads
Section titled “Where this part leads”With the request format settled, three later parts pick up the threads this one exposed:
- The Sealevel part takes the declared footprint and builds the parallel scheduler on top of it — the conflict rule, batching, and the run-in-parallel-commit-in-order machinery that
solmini’sscheduleandrun_parallelsketch. - The programs part is where the instruction
datablob finally gets interpreted — the on-chain code a transaction merely points at here. - Proof of History (in the consensus material) is what makes the “recent blockhash” a meaningful, verifiable clock in the first place — a freshness stamp is only useful if there is an agreed, unfakeable notion of recent.
You now have the whole shape of a Solana request in your head: authorized, atomic, fresh, large enough, and bounded in cost — and, above all, legible as data before it runs.
Check your understanding
Section titled “Check your understanding”- The part’s central sentence bundles two properties. Name both, and for each, name the field or mechanism in the transaction format that carries it.
- Atomicity is enforced by “commit-on-success.” Describe the clone-run-commit sequence and explain why a failed transaction leaves no trace, using the
solminiruntime’s behavior. - What are the two jobs the recent blockhash performs, and how does the same mechanism accomplish both expiry and replay protection?
- A legacy transaction hits a ceiling on how many accounts it can name. What is the ceiling, and how do versioned transactions plus address lookup tables raise it without abandoning the up-front-declaration rule?
- Explain how the declared footprint powers both parallel scheduling and the per-account (localized) fee market. Why is one declaration enough for both?
Show answers
- Atomicity — carried by the transaction-level instruction array, whose writes commit only on success and roll back as a unit on any failure. Declared account access — carried by each instruction’s account references and their
is_writable/is_signerflags, which state the read/write footprint before execution. (Signatures authenticate, the recent blockhash time-bounds, and the compute budget cost-bounds the request around these two.) - The runtime pulls the transaction’s declared accounts out of the database as an owned clone (its private working set), runs the program against that clone, and commits the writable accounts back only if the run returned success. On failure it simply discards the clone and commits nothing — so a failed transaction leaves the world exactly as it was, with no half-applied state ever visible. Atomicity is a consequence of commit-on-success, not a separate rollback log.
- The blockhash provides (a) freshness/expiry — it is valid only for a bounded window (~150 blocks in the current protocol), after which validators reject the transaction, so a signed-but-unsent transaction harmlessly expires; and (b) replay protection — because the blockhash is part of the signed bytes and validators remember transactions processed within that window, the same signed bytes cannot be replayed. One mechanism does both: a transaction is only accepted once, and only within the blockhash’s short validity window.
- The ceiling is the transaction’s serialized size cap (~1232 bytes on the wire); every full 32-byte account address inline eats into it, limiting how many accounts a legacy transaction can name. Versioned (
v0) transactions add a version prefix so the format can carry new features, and address lookup tables store addresses in an on-chain account so a transaction references them by a small index (~1 byte) instead of the full key. Far more accounts fit under the same cap. The declaration rule is untouched: the runtime still resolves and knows the complete set of accounts before scheduling — only the encoding got cheaper. - Both features read the same declared write set. The scheduler uses it to apply the conflict rule — two transactions can run in parallel unless they share a writable account. The fee market uses it to localize congestion — because the runtime knows which accounts a transaction writes, it can raise the priority-fee price only for the specific hot writable account under contention, sparing unrelated transactions. One up-front footprint declaration answers both questions (“who conflicts?” and “who is contending for this account?”), which is why declaring accounts before execution pays off twice.