Versioned Transactions and Address Lookup Tables
The previous page fixed the time dimension of a transaction — the recent blockhash that stamps a message to a narrow window and makes it a use-once object. This page hits the space dimension. A transaction has to fit in a single network packet, and that packet has a hard ceiling. For a simple transfer the ceiling is invisible. For a real DeFi transaction that routes a swap across five pools, it is a wall you slam into — and the thing you slam into is not compute, not fees, but raw bytes.
This page shows why the byte budget caps how many accounts a legacy transaction can name, and how the versioned (v0) transaction format plus address lookup tables move that wall by replacing 32-byte account keys with 1-byte indices. It is a small, almost boring encoding trick — and it is exactly the kind of trick that lets a single global state machine keep composing bigger atomic operations without changing the wire.
The wall: 1232 bytes per transaction
Section titled “The wall: 1232 bytes per transaction”Recall the anatomy of a transaction: a transaction is a list of signatures followed by a message, and the message carries — among other things — the full list of account keys every instruction will touch. Each account key is a 32-byte ed25519 public key, inlined right there in the message.
That message has to travel as a single UDP-style packet. Solana caps a transaction at 1232 bytes. The number is not arbitrary: it is the standard 1280-byte IPv6 minimum MTU (the smallest packet size every IPv6 network must forward without fragmentation) minus 48 bytes of headers. Staying under one guaranteed-deliverable packet means a transaction never gets fragmented in flight — no reassembly, no “half a transaction arrived” edge cases. It is the same instinct as the recent blockhash: keep the atomic unit small, self-contained, and impossible to half-process.
Now do the arithmetic on what that budget buys you.
So the problem is precise and physical:
A legacy transaction inlines every 32-byte account key into a 1232-byte packet, so the number of distinct accounts it can touch is capped at a few dozen. Complex composability outgrows the packet.
You cannot make the packet bigger without giving up the guarantee that a transaction is one un-fragmented unit. So instead you make the keys smaller.
The move: reference accounts by index, not by value
Section titled “The move: reference accounts by index, not by value”Here is the whole idea in one line: most of the accounts a complex transaction touches are long-lived and known in advance — the AMM’s market account, its vaults, well-known program ids, oracle accounts. They don’t change from transaction to transaction. So why re-send 32 bytes for each of them, every single time, forever?
Store those addresses once, on-chain, in a dedicated account — an Address Lookup Table (ALT) — and let a transaction refer to each one by its position in that table: a single byte.
Legacy: inline every key v0 + ALT: reference by index
message.account_keys = [ lookup_table #Xy9… holds: Wp7a…9fK2 (32 bytes) index 0: Wp7a…9fK2 Vk3b…m1Qz (32 bytes) index 1: Vk3b…m1Qz Or4c…tT8s (32 bytes) index 2: Or4c…tT8s … 32 more … … 253 more … ] ]
~35 keys × 32 bytes ≈ your whole budget message references: table Xy9…, indices [0, 1, 2, …] → 32 bytes for the table address → 1 byte per account thereafterA 32-byte key becomes a 1-byte index. Thirty-two-to-one is the compression that moves the wall. A single ALT can hold up to 256 addresses, and a transaction can reference several tables at once — so a v0 transaction can address on the order of hundreds of accounts while still fitting in the same 1232-byte packet. The wire didn’t change. The encoding did.
Versioned (v0) transactions: a one-byte version prefix
Section titled “Versioned (v0) transactions: a one-byte version prefix”There’s a compatibility problem to solve first. The old (legacy) transaction format has no version field — the message just starts with its header bytes. If you want a new format that behaves differently, existing parsers have to be able to tell the two apart without guessing.
Solana solves this with a version prefix. In a versioned transaction, the first byte of the
message has its high bit set (0x80). The legacy format can never set that bit, because a legacy
message’s first byte is a small count (the number of required signatures) that is always well under
128. So the runtime looks at bit 7 of the first message byte:
first message byte:
0xxx xxxx → high bit clear → LEGACY transaction (first byte = num required signatures) 1000 0000 → high bit set → VERSIONED, version 0 (the low 7 bits carry the version number)The 0x80 prefix means “version 0”. The low seven bits leave room for 127 future versions, so the
format is extensible without ever colliding with legacy. It is a classic self-describing
tag — the same discipline as an 8-byte account discriminator or a file magic number: one cheap,
unambiguous marker so a reader never has to guess what it’s holding.
A v0 message adds one section that a legacy message lacks: a list of address table lookups. Each entry names a lookup-table account and two lists of indices into it.
v0 message layout (simplified):
[0x80] ← version prefix [ header: #sigs, #ro-signed, #ro-unsigned ] [ static account_keys[] ] ← still inlined: signers + writable-signer keys + program ids [ recent_blockhash ] [ instructions[] ] [ address_table_lookups[] ] ← NEW: { table_key, writable_indexes[], readonly_indexes[] }Resolving the indices at execution time
Section titled “Resolving the indices at execution time”The message doesn’t carry the looked-up addresses — only the table’s address and the indices. So before the runtime can execute the transaction, it loads each referenced lookup-table account and substitutes in the real addresses, rebuilding the full account list the instructions expect:
1. read address_table_lookups: table = Xy9…, writable = [0,1], readonly = [2] 2. load the ALT account Xy9… from state 3. resolve: index 0 → Wp7a…, index 1 → Vk3b…, index 2 → Or4c… 4. append resolved keys to the static keys → the full account list 5. NOW the runtime knows the transaction's complete footprint → schedule + executeNotice what step 5 means for the parallel scheduler: the writable-vs-readonly split is preserved through the lookup. The ALT entry says which resolved accounts are writable and which are read-only, so the scheduler still sees the exact same shared-XOR-mutable footprint it needs to decide which transactions can run on different cores. Lookup tables shrink the encoding of the footprint; they don’t hide it.
The setup cost and the constraints
Section titled “The setup cost and the constraints”This power isn’t free, and the constraints fall straight out of the mechanism.
A lookup table must exist before the transaction that uses it. You can’t reference index 5 of a table that doesn’t hold five addresses yet. So an ALT is created and extended by its own transactions, ahead of time:
# create an empty lookup table (returns its address)solana address-lookup-table create --authority <AUTH_KEYPAIR>
# extend it with the addresses you'll reference later (repeat as needed)solana address-lookup-table extend <TABLE_ADDR> \ --addresses <PUBKEY_1>,<PUBKEY_2>,<PUBKEY_3>Two more wrinkles that matter in practice:
- A newly-extended table isn’t usable in the very same slot. The runtime requires the extension to be “warmed” (confirmed and one slot old) before a transaction may reference the new entries. This stops a transaction from racing its own table setup.
- Tables are the setup tax. For a one-off transfer, building a table is pure overhead — you’d send more bytes and more transactions than just inlining the two keys. ALTs pay off only for repeated, account-heavy patterns: a DEX aggregator that hits the same pools thousands of times, a market maker touching the same order books all day.
The referenced accounts cannot be signers. This is the sharpest constraint, and it’s forced by ordering. A transaction’s signatures are verified against the front of the message — the signer keys must be known before the runtime loads any lookup table (you can’t verify a signature against an address you haven’t resolved yet). So signer keys and writable-signer keys stay inlined in the static key list; only non-signer accounts may come from a lookup table.
Where a key can live:
signer (writable or readonly) → MUST be inlined in static keys (verified up front) non-signer, writable → may come from an ALT non-signer, readonly (pools, → may come from an ALT ← the big win program ids, oracles, vaults)In DeFi that split is almost perfect: the user signs (one or two inlined keys), and the dozens of pool/vault/program accounts are all non-signers that live happily in a table.
Why this matters for the whole machine
Section titled “Why this matters for the whole machine”Step back to the book’s throughline: how do you build a single global state machine that runs at hardware speed without falling apart? Two of Solana’s answers depend on transactions being able to touch many accounts atomically.
- Composability. The point of a global state machine is that one transaction can atomically compose many programs — swap, then deposit the output as collateral, then borrow against it, all or nothing. Each hop drags in more accounts. Without ALTs, composition dies at the packet boundary: you’d have to split the operation across transactions and lose atomicity (and reintroduce the MEV/failure gaps between them). Lookup tables let a bigger operation stay a single atomic unit.
- Parallelism. The scheduler needs each transaction to declare its full footprint up front. A bigger atomic transaction still declares everything (the resolved ALT entries carry their writable/readonly flags), so the scheduler keeps its perfect information — it just gets it in a more compact form. Fitting more accounts per transaction means fewer transactions overall for the same work, which is less scheduling and less signature verification per unit of value moved.
So versioned transactions aren’t a fee optimization or a nicety. They’re what keeps the unit of atomic work from being throttled by the unit of network delivery. The packet stays one guaranteed MTU; the operation inside it grows.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because a legacy transaction inlines every 32-byte account key into a fixed 1232-byte packet, capping it at a few dozen accounts — too few for real DeFi routes. Versioned transactions plus lookup tables exist to break that ceiling without enlarging the packet.
- What problem does it solve? It decouples how many accounts an operation touches from how many bytes those keys cost on the wire, by turning a repeated 32-byte key into a reusable on-chain entry addressed by a 1-byte index.
- What are the trade-offs? You gain hundreds of addressable accounts per transaction; you pay a setup tax (create-and-extend a table ahead of time, warm it a slot, and eat the extra state), and you accept that looked-up accounts can never be signers.
- When should I avoid it? For simple, low-account transactions (a plain transfer, a single-program call). Building an ALT there sends more bytes and more transactions than just inlining the two or three keys — the setup tax swamps the saving.
- What breaks if I remove it? Complex atomic composability. Multi-hop swaps and any transaction that must touch dozens of accounts stop fitting in one packet, forcing them to split across transactions and lose all-or-nothing atomicity — reintroducing exactly the partial-execution gaps the single-packet design was meant to prevent.
Check your understanding
Section titled “Check your understanding”- What is the hard byte ceiling on a Solana transaction, where does the number come from, and why is it kept small rather than raised?
- Walk through why that byte ceiling limits the number of accounts a legacy transaction can touch, and give the rough cap.
- How does a v0 transaction let a reader distinguish it from a legacy transaction, and why can the two formats never be confused?
- An address lookup table replaces a 32-byte key with a 1-byte index. Name the two constraints this mechanism forces, and explain what each one is forced by.
- Lookup tables shrink how a transaction’s accounts are encoded. Explain why they do not weaken the parallel scheduler’s information, and why that fact is what makes bigger atomic transactions worth having.
Show answers
- 1232 bytes. It’s the IPv6 minimum MTU of 1280 bytes minus ~48 bytes of headers — the largest payload guaranteed to travel as a single un-fragmented packet on any network. It’s kept small so a transaction is always one atomic, self-contained unit that can’t arrive half-delivered; raising it would sacrifice that guarantee.
- A legacy message inlines every account key as its full 32-byte value. Fixed overhead (a signature at 64 bytes, the header, the 32-byte blockhash, framing) consumes a chunk of the 1232 bytes; whatever remains, split into 32-byte keys, tops out around ~35 accounts — and lower (low 30s) once you add real instruction data and extra signers.
- A versioned transaction sets the high bit (
0x80) of the first message byte; the low 7 bits are the version (0 today). A legacy message’s first byte is the required-signature count, always well under 128, so its high bit is always clear. The reader just checks bit 7 — the two encodings can never overlap. - (a) The table must be created and extended before use (and warmed a slot) — forced by the fact that you can’t reference an index that doesn’t exist yet, and the warm-up stops a transaction racing its own setup. (b) Looked-up accounts can’t be signers — forced by verification ordering: signatures are checked against the front of the message before any lookup table is loaded, so signer keys must be inlined, not resolved from a table.
- Each ALT entry carries its own writable/readonly flag, so after the runtime resolves the indices it reconstructs the complete account list with the shared-XOR-mutable footprint intact — the scheduler sees exactly what it would have seen from inlined keys. Because the footprint is preserved, a bigger atomic transaction stays fully schedulable, letting one all-or-nothing operation compose many programs and accounts instead of being split across transactions and losing atomicity.