Compressed NFTs & State Compression
The previous page built a real NFT: a mint with supply 1, a token account proving ownership, and a metadata account holding the name, image, and royalties. That is a clean design — but it left a bill on the table. Every one of those pieces is a separate account, and every account on Solana must be rent-exempt. For a one-off collectible, fine. For a game that wants to airdrop a badge to ten million players, or a loyalty program minting a coupon per purchase, the account-per-NFT model becomes the single most expensive thing you do. You are paying validator-memory prices to store a JPEG reference ten million times.
This page is about the escape hatch Solana built for exactly that: state compression. The trick is to stop storing each NFT in its own account and instead store one account holding a Merkle root — a single 32-byte fingerprint that commits to all the NFTs at once — while the actual per-NFT data lives in the far cheaper transaction ledger. It is the same Merkle-root idea the Bitcoin book uses to fit thousands of transactions under one block-header hash, repurposed here to fit millions of NFTs under one account.
The problem, stated precisely: accounts are the expensive resource
Section titled “The problem, stated precisely: accounts are the expensive resource”Recall from the account model that state on Solana lives in accounts, and accounts occupy validator RAM. To stop the world from filling that RAM with junk, Solana requires every account to hold a rent-exempt minimum balance proportional to its size. That deposit is refundable, but it is capital you must lock up per account, forever, to keep it alive.
A Metaplex NFT is not one account — it is several. Mint, metadata, master edition, and the owner’s token account. Multiply by the number of NFTs and the rent-exempt deposits dominate:
Regular NFT (one per collectible): mint account ~0.0015 SOL rent-exempt metadata account ~0.0056 SOL rent-exempt master edition ~0.0028 SOL rent-exempt + token account for the owner ──────────────────────────────── order of ~0.01 SOL locked PER NFT (figures point-in-time; verify current rent)
1,000,000 NFTs → ~10,000 SOL locked just in rent-exempt depositsAt any nonzero SOL price that is a serious sum, and it scales linearly with the number of NFTs. The resource that is scarce here is not compute and it is not bandwidth — it is account space. State compression attacks that specific cost and nothing else.
The core move: store a root, not the leaves
Section titled “The core move: store a root, not the leaves”A Merkle tree hashes data in pairs, up the levels, until everything collapses into one root hash. Change any single leaf and the root changes; leave every leaf alone and the root is a stable commitment to all of them. (If that mechanism is new, the Bitcoin book’s Merkle Trees & SPV page derives it from first principles — the idea is identical here.)
State compression applies that directly. Instead of N NFT accounts, you create one account that
stores the Merkle root over N NFT leaves:
Regular NFTs Compressed NFTs ──────────── ─────────────── NFT #1 → account (rent-exempt) leaf #1 ┐ NFT #2 → account (rent-exempt) leaf #2 ├─ hashed pairwise up the tree NFT #3 → account (rent-exempt) leaf #3 ┘ │ ... ... ▼ NFT #N → account (rent-exempt) ONE account = { Merkle root, config }
cost grows with N account cost is ~fixed; only the ROOT is on-chainThe account holding the root is called a Merkle tree account (the ConcurrentMerkleTreeAccount). It stores
the current root, the tree’s depth and capacity, and a little bookkeeping — but crucially not the leaf
data. So where does the per-NFT data go?
Where the leaves actually live: the ledger, not accounts
Section titled “Where the leaves actually live: the ledger, not accounts”Here is the sleight of hand. When you mint a compressed NFT, the full leaf data — owner, metadata, collection — is emitted in the transaction’s logs, which become part of Solana’s permanent, ordered ledger (the sequence of all transactions ever processed). The ledger is dramatically cheaper storage than account RAM: it is append-only, streamed to disk and archives, and never needs to sit in a validator’s working memory the way an account does.
So the split is:
- On-chain, in an account (expensive, in RAM): just the 32-byte Merkle root.
- On-chain, in the ledger (cheap, on disk/archive): the actual leaf data, written once when the transaction runs, never re-read by the runtime.
The runtime only ever needs the root to verify things. It does not need to hold a million leaves in memory to know they exist — the root already commits to all of them. That is the entire saving: you moved the bulk data from the scarce resource (account RAM) to the abundant one (the ledger), and kept only the cryptographic commitment in the scarce place.
Why a concurrent Merkle tree
Section titled “Why a concurrent Merkle tree”A naive Merkle tree has a problem under Solana’s parallel, high-throughput execution. To update a leaf you must recompute the path from that leaf to the root, and that recomputation assumes the root you started from is still current. But on a chain doing thousands of writes per second, several transactions may all try to modify the same tree in the same slot. By the time your transaction lands, the root it was built against may already be stale — and a stale update would corrupt the tree.
Solana’s concurrent Merkle tree (from the spl-account-compression program) fixes this by keeping a
small on-chain changelog of the most recent root updates — a buffer of the last K roots and the proof
paths that produced them. A transaction can then be validated against a recent root, not only the single
latest one, as long as its proof still reconciles with the changelog:
plain Merkle tree: update must match the ONE current root → two updates in the same slot collide, one fails
concurrent tree: keeps a changelog buffer of the last K roots + paths → an update built against a slightly-stale root can still be replayed forward and applied → many updates per slotThe buffer size (the “max buffer size”) sets how many concurrent writes the tree tolerates per slot;
tree depth sets its capacity (a depth-d tree holds up to 2^d leaves). Both are chosen at creation
time and, along with the “canopy” (how many upper proof nodes are cached on-chain), determine the tree’s
one-time cost. This is the concrete reason compression works on Solana specifically: a design built for
parallel execution needs a Merkle tree that also tolerates concurrent updates.
Proofs: the price you pay to touch a compressed NFT
Section titled “Proofs: the price you pay to touch a compressed NFT”Because the leaf data is not in an account, the runtime cannot just look up “who owns compressed NFT #7.”
It only has the root. So to transfer or modify a compressed NFT, the caller must prove the current
state of that leaf by supplying a Merkle proof: the leaf’s data plus the sibling hashes along its path
to the root. The program recomputes the root from (leaf, proof) and checks it matches the root stored in
the account. If it matches, the leaf is genuine and current; the program then writes the new leaf and
updates the root.
To transfer compressed NFT at leaf L: 1. client fetches leaf L's data + the sibling hashes on its path to the root 2. tx sends { old leaf, new leaf, proof (siblings) } to the compression program 3. program recomputes root from (old leaf, proof) 4. recomputed root == on-chain root ? → accept, write new leaf, update root → else reject (stale or forged)This is the mirror image of a regular NFT. With a normal token, ownership is stored (in a token account) and reading it is a cheap account lookup, but storing it is expensive. With a compressed NFT, ownership is proven (via a Merkle proof) — storage is nearly free, but every read-then-write must carry a proof, which costs transaction size and compute. You traded storage cost for proof-handling cost. For a badge that is minted once and rarely moved, that trade is overwhelmingly worth it; for an asset traded every few seconds, it may not be.
The indexer problem: you can’t query a root
Section titled “The indexer problem: you can’t query a root”There is a catch that reshapes how you build clients. A normal Solana app reads state by fetching accounts: “give me the token account, tell me the balance.” That works because the state is the account. But a compressed NFT’s state is a leaf sitting in the ledger’s history, committed to only by a root. You cannot ask a validator “list all compressed NFTs owned by Alice” — validators don’t index leaves, they only hold the root, and reconstructing a proof means walking historical transactions.
So the ecosystem relies on indexers: services (run by RPC providers) that watch every transaction,
decode the compression program’s leaf-emission logs, and rebuild a queryable database of all leaves plus
the sibling hashes needed to construct proofs on demand. The standard interface over this is the Digital
Asset Standard (DAS) API — a common RPC method set (getAsset, getAssetsByOwner, getAssetProof, …)
that returns both regular and compressed NFTs uniformly, and crucially hands you the proof you need to
build a transfer transaction.
Regular NFT read: client ──"getAccount(mint/metadata)"──► validator ──► answer (state is IN the account)
Compressed NFT read: client ──"getAssetsByOwner / getAssetProof"──► DAS indexer ──► leaf data + Merkle proof (the indexer reconstructed this by watching the ledger; the validator can't answer directly)This is the deep structural cost of compression, and it is easy to under-weight: you have reintroduced a trusted-ish off-chain component into a system whose whole point was trustlessness. The proof itself is still trustless — a forged proof won’t recompute to the on-chain root, so the program rejects it, and you can in principle rebuild any proof yourself from the public ledger. But in practice, if every DAS indexer is down or lying-by-omission, an ordinary client can’t easily find its assets or assemble a valid transfer. The chain still knows the truth (the root); getting at it just now depends on infrastructure the base NFT model didn’t need.
Under the hood — Bubblegum, the compression stack
Section titled “Under the hood — Bubblegum, the compression stack”Compressed NFTs are assembled from a small stack of programs, each doing one job:
spl-account-compression— the generic engine: creates and mutates concurrent Merkle trees, maintains the changelog buffer, and verifies proofs. It knows nothing about NFTs; it just stores hashed leaves. This is the reusable “compress any list of things” primitive.spl-noop(the “no-op” logging program) — the trick that gets leaf data into the ledger. The compression program CPIs into it to emit the leaf as a log; the no-op does nothing on-chain but its invocation is recorded in the transaction, so indexers can read it. Cheap ledger storage, by design.- Bubblegum (
mpl-bubblegum, Metaplex) — the NFT-specific layer on top: it defines what an NFT leaf contains (owner, delegate, metadata hash, collection) and exposesmint,transfer,burninstructions that speak the Metaplex NFT model but store everything as compressed leaves.
So a compressed NFT is: Bubblegum (NFT semantics) → spl-account-compression (the tree + proofs) → spl-noop (leaf data into the ledger) → DAS indexers (make it queryable again). Each layer is the minimal piece needed to move NFT data out of accounts without losing the ability to verify or find it.
The architect’s lens
Section titled “The architect’s lens”State compression is a major, opinionated component — a genuine change in where state lives. Interrogate the bargain before you reach for it:
- Why does it exist? Because accounts are Solana’s scarce, RAM-priced resource, and minting millions of NFTs the normal way means millions of rent-exempt accounts — a linear cost that becomes prohibitive at scale.
- What problem does it solve? It collapses per-NFT account cost to near zero by storing only a Merkle root in one account and pushing the actual leaf data into the cheap, append-only ledger — orders-of-magnitude cheaper bulk mints.
- What are the trade-offs? Every read-then-write must carry a Merkle proof (transaction size + compute), and because leaf data isn’t in accounts, clients depend on off-chain indexers / DAS to find assets and build proofs. You trade storage cost for proof-handling cost and a new liveness dependency.
- When should I avoid it? When assets are few, or traded/queried so constantly that the proof overhead and indexer reliance outweigh the rent you’d save — a handful of high-value 1/1s belong in plain accounts where reads are a trivial lookup.
- What breaks if I remove it? Use-cases that only exist because mints are nearly free — mass airdrops, in-game item drops, per-action loyalty tokens — stop being economically viable and collapse back into the rent wall this page opened with.
Compression is one specific answer to the book’s recurring question — how do you build a single global state machine at hardware speed without falling apart? — applied to storage: keep only the commitment in the fast, expensive place, and push the bulk to the slow, cheap place, paying for it in proofs and indexers. Next: Major Protocols & Wallets surveys the applications and tooling built on everything this part has covered.
Check your understanding
Section titled “Check your understanding”- Minting a million regular NFTs is prohibitively expensive on Solana. Which specific resource is the cost, and why does it scale linearly with the number of NFTs?
- State compression stores “a root, not the leaves.” What exactly is kept in the on-chain account, where does the per-NFT leaf data actually live, and why is that location cheaper?
- Why does a compressed NFT need a concurrent Merkle tree rather than a plain one — what would go wrong with a plain tree under Solana’s throughput?
- To transfer a compressed NFT you must supply a Merkle proof. Walk through how the program uses
(old leaf, proof)to decide whether to accept the transfer, and state the trade-off this represents versus a regular NFT. - Why can’t a client just ask a validator to list the compressed NFTs an address owns? What component fills that gap, what API standardizes it, and what new risk does that dependency introduce?
Show answers
- The scarce resource is account space (validator RAM), enforced via rent-exempt deposits. A Metaplex NFT is several accounts (mint, metadata, master edition, token account), each needing a rent-exempt balance, so total locked capital grows linearly with the number of NFTs — a million NFTs means on the order of a million times the per-NFT rent.
- The account keeps only the 32-byte Merkle root (plus tree config: depth, buffer size, canopy). The actual leaf data — owner, metadata, collection — is emitted into the transaction and lives in the permanent ledger (append-only, on disk/archive). The ledger is cheaper because it is never held in a validator’s working RAM the way an account is; the runtime only needs the root to verify.
- Under Solana’s parallel, high-throughput execution, multiple transactions may update the same tree in the
same slot. A plain tree requires each update to match the single current root, so concurrent updates
collide and all but one fail. A concurrent Merkle tree keeps a changelog buffer of the last
Kroots and their proof paths, so an update built against a slightly-stale root can still be replayed forward and applied — allowing many writes per slot. - The client fetches the leaf’s data and the sibling hashes on its path to the root; the transaction sends
{ old leaf, new leaf, proof }. The program recomputes the root from(old leaf, proof)and checks it equals the on-chain root; if it matches, the leaf is genuine and current, so it writes the new leaf and updates the root, otherwise it rejects (stale or forged). The trade-off: a regular NFT stores ownership (cheap read, expensive storage) while a compressed NFT proves it (cheap storage, but every read-then-write costs a proof in transaction size and compute). - Because the leaf data isn’t in an account — the validator only holds the root, and reconstructing a
proof means walking historical ledger transactions, which validators don’t index. Indexers (run by
RPC providers) fill the gap by decoding the compression logs into a queryable database; the Digital
Asset Standard (DAS) API standardizes access (
getAssetsByOwner,getAssetProof, …) and returns the proof. The new risk is an availability/liveness dependency: the cryptography stays trustless (a bad proof can’t match the root), but if indexers are down or stale, ordinary clients can’t easily find assets or build valid transfers.