Guide
Solana versioned transactions and address lookup tables
A simple SOL transfer lists two accounts and fits easily in a transaction packet. A Jupiter swap, an NFT mint with metadata, or a liquidation across five programs might need thirty or more accounts — and each account pubkey is 32 bytes on the wire. Solana's answer is versioned (v0) transactions paired with address lookup tables (ALTs): on-chain tables that let you reference frequently used addresses with a single-byte index instead of repeating full pubkeys.
Why transaction size matters
Solana transactions are capped at 1,232 bytes for the serialized message that validators sign and broadcast. In a legacy transaction, every account address in the message header and every instruction's account list is a full 32-byte Ed25519 public key. Signatures add 64 bytes each. Instructions add program IDs, account indexes, and data payloads.
Do the math: twenty accounts alone consume 640 bytes before you count signatures, blockhash, or instruction data. Complex DeFi routes hit the limit quickly. Wallets and aggregators responded by splitting work across multiple transactions (slower, worse UX, atomicity lost) — until v0 and lookup tables shipped in 2022.
Versioned transactions do not change Solana's execution model — programs still receive the same account metas at runtime. They only change how addresses are encoded in the serialized message. The account model (owners, lamports, writable flags) is unchanged.
Legacy vs versioned (v0) wire format
Every Solana transaction message starts with a version prefix:
- Legacy — no prefix byte; the first byte is the count of signatures required. Wallets and libraries default here for simple transfers.
- v0 — prefixed with
0x80(high bit set on the version byte). Enables an optional address table lookup section that points at on-chain ALTs.
At execution time the runtime resolves v0 messages: it loads each referenced lookup table account, expands 1-byte indexes into full pubkeys, and builds the flat account list programs see. From a program's perspective, nothing differs — only the serializer and deserializer change.
What you see in explorers
Solscan and other explorers label transactions as "legacy" or "v0." A v0 swap may show fewer raw pubkeys in the compact view because many addresses were pulled from lookup tables. Our Solscan walkthrough explains how to read account roles once the table is expanded.
How address lookup tables work
An address lookup table is a normal on-chain
account
owned by the Address Lookup Table program
(AddressLookupTab1e1111111111111111111111111). Its data is a list of
pubkeys — up to 256 addresses per table (index 0–255). Creating and
extending a table costs rent (lamports locked in the account) plus transaction fees.
Lifecycle
- Create — allocate the table account with a chosen authority (usually a program or multisig).
- Extend — append addresses in batches. Common entries: USDC mint, Serum/OpenBook market accounts, popular pool PDAs, program IDs used on every swap route.
- Freeze (optional) — deactivate further extends for immutable routing tables run by protocols.
- Reference in v0 txs — the transaction message lists which tables to load and which indexes to read; indexes become full pubkeys before execution.
A single v0 transaction can reference up to 64 lookup tables and pull addresses from all of them. In practice, one or two well-maintained tables (Jupiter's, a DEX's, or your own app's) cover most routes.
Writable lookups
Lookup entries can be marked writable in the message. The runtime enforces the same rules as legacy txs: a program cannot write to an account unless the instruction declared it writable and the signer authorized the change. Read-only table entries behave like read-only account metas.
When you need v0 and ALTs
- DEX aggregators and routers — multi-hop swaps across pools need many pool, vault, and oracle accounts in one atomic transaction.
- NFT mints with Metaplex — metadata, master edition, token accounts, and collection accounts add up fast.
- Lending liquidations — obligation, reserves, oracles, and liquidity pools in a single instruction bundle.
- Your own programs — if integration tests fail with "transaction too large," v0 is the first fix before dropping features.
You usually do not need v0 for: single SOL transfers, simple SPL sends, or two-instruction flows with under a dozen accounts. Wallets pick legacy format automatically when the message fits.
Building and debugging v0 transactions
Libraries
@solana/web3.js (v1.66+) exposes
VersionedTransaction, TransactionMessage.compileToV0Message(),
and helpers to fetch lookup tables from RPC before signing. Anchor and most wallet
adapters pass versioned transactions through unchanged once built.
Simulation is non-negotiable
Wrong table indexes, deactivated tables, or stale addresses produce failures that look cryptic in wallet popups. Always simulate before asking users to sign. Simulation expands lookup tables server-side and returns program logs for the fully resolved account set.
Common errors
- Address lookup table account not found — table was closed or wrong pubkey in the message.
- Invalid lookup index — table was extended after you cached indexes; refresh table state from RPC.
- Transaction too large — even v0 has a cap; split instructions or prune unused accounts.
- Blockhash not found — unrelated to v0, but complex txs take longer to build; use a fresh blockhash — see confirmation times.
Priority fees still apply
v0 does not exempt you from compute limits or congestion. Heavy routes should set priority fees so validators include the transaction during busy slots.
Security notes for users
Lookup tables are indirection — a wallet popup may show "Address Table Lookup" instead of listing every pool address. That is normal for aggregator swaps, not automatically suspicious. Still apply the usual rules:
- Only sign transactions from sites you trust.
- Check the top-level program IDs (Jupiter, Raydium, etc.) match what you intended.
- Verify token mints and amounts in the human-readable summary when the wallet provides one.
- See wallet security best practices for phishing patterns.
Practical checklist
For developers
- Measure serialized size early; switch to v0 when legacy encoding exceeds ~1,000 bytes.
- Cache lookup table accounts in your backend; refresh on extend events or TTL.
- Test on devnet with small tables before mainnet deployment.
- Use dedicated RPC endpoints — fetching multiple lookup tables adds round trips.
For power users
- Expect v0 on complex DeFi; legacy on simple sends.
- If a swap simulation fails, retry — routes and table state change frequently.
- Explorers expand tables; use them to audit unfamiliar transactions.