How Compression Works
State compression stores a large set of records as leaves in a merkle tree. Only the root hash (and optional canopy nodes) live on-chain; updaters supply proofs that a leaf change is valid without storing every leaf in a Solana account.
Recipe
On-chain: MerkleTreeAccount { root, canopy[], change_log }
Off-chain: Full tree / leaf data indexed by provider
Per update: leaf + merkle proof + valid root transition
const proof = await dasRpc("getAssetProof", { id: assetId });
// proof.proof = sibling hashes from leaf to rootWhen to reach for this:
- Understanding why cNFT transfers need DAS proofs
- Sizing concurrent merkle trees for collection capacity
- Debugging failed Bubblegum transactions (stale proof, wrong root)
- Evaluating indexer dependency for your product
Working Example
// Conceptual merkle update for leaf transfer
type MerkleProof = {
root: string;
proof: string[]; // sibling hashes bottom-up
node_index: number;
leaf: string;
};
async function explainProof(assetId: string) {
const proof = await dasRpc<MerkleProof>("getAssetProof", { id: assetId });
console.log("Current root:", proof.root);
console.log("Proof depth:", proof.proof.length);
console.log("Leaf hash:", proof.leaf);
// Bubblegum verifies proof against on-chain tree account root
// then replaces leaf (ownership) with new hash
}// On-chain SPL Account Compression verifies proof and applies leaf replace
// Bubblegum wraps metadata semantics around compression CPIs
// Programs rarely reimplement proof verification - delegate to compression programWhat this demonstrates:
- Proof length ≈ tree depth − canopy depth
- Root on-chain must match proof root at transaction build time
- Leaf content hash encodes cNFT ownership and metadata fields
Deep Dive
How It Works
- Tree creation allocates Concurrent Merkle Tree account with chosen
maxDepthandmaxBufferSize - Mint appends leaf hash at next index; program verifies append proof
- Transfer replaces leaf data (new owner) with replace proof
- Canopy stores upper proof nodes on-chain to reduce transaction size
- Indexers reconstruct state and serve proofs via DAS
On-Chain vs Off-Chain
| Data | Location |
|---|---|
| Root hash | Tree account on-chain |
| Canopy nodes | Tree account on-chain |
| All leaves | Indexer / DAS infrastructure |
| Proofs | Served per-request by DAS |
Tree Sizing
| maxDepth | Max leaves (approx) |
|---|---|
| 14 | 16,384 |
| 20 | 1,048,576 |
| 24 | 16,777,216 |
Choose depth for peak supply + headroom; deeper trees cost more rent.
Gotchas
- Stale proof after concurrent mints - root changed. Fix: fetch fresh proof immediately before signing.
- Undersized tree depth - cannot mint past capacity. Fix: plan peak supply at
createTree. - Zero canopy on deep tree - tx too large. Fix: set canopy depth per Metaplex recommendations for your depth.
- Assuming leaf in wallet account - no SPL token account for cNFT. Fix: ownership is leaf state, read via DAS.
- Self-hosting proofs without indexer - heavy infrastructure. Fix: use DAS provider or run merkle indexer.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Full on-chain NFT | Low supply, max composability | Million-item drop |
| Off-chain DB only | No on-chain provenance needed | Need cryptographic ownership proof |
| zk compression (other) | Different product stack | Standard Metaplex cNFT path |
FAQs
What is stored on-chain?
Merkle root, canopy cache, change log metadata - not every NFT account.
What is a merkle proof?
Sibling hashes proving a leaf's inclusion and enabling verified updates.
Why concurrent tree?
Supports multiple leaf updates per slot without global tree lock contention.
What is canopy?
On-chain cache of upper tree nodes shrinking proof size in transactions.
Who serves proofs?
DAS-enabled RPC providers index trees and expose getAssetProof.
Can proofs be forged?
Bubblegum/compression program verifies against on-chain root - invalid proofs fail.
Root changes when?
Every mint, transfer, burn updating a leaf.
Program IDs?
Compression: cmtDvXnzBMY46rZyrBGX7p7r4Xc78DGpXEC5rVjrvAf. Bubblegum: BGUMAp9Gq7iN3nEkgqX1FbwwdBonfLCEEgxoj36SYPk.
Can I read leaf without DAS?
Theoretically via custom indexer ingesting compression program logs - DAS is production default.
Proof size limits?
Drives tx size - canopy tuning and versioned transactions with ALTs help.
Burn compressed asset?
Leaf replace to null/burn state with proof - Bubblegum burn instruction.
Devnet trees?
Same mechanics on devnet; verify DAS devnet support on your provider.
Related
- State Compression Basics - intro
- Reading Compressed Assets (DAS) - proof fetch
- Transferring cNFTs - proof in txs
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.