solana-compression
ZK Compression on Solana
ZK Compression enables rent-free tokens and PDAs on Solana by storing state on the ledger instead of in accounts, using zero-knowledge proofs to validate state transitions. Built by Light Protocol and indexed by Helius Photon.
When to Use ZK Compression
Use ZK Compression when:
- Creating millions of token accounts (5000x cheaper than regular accounts)
- Minting to many recipients (airdrops, loyalty programs, gaming assets)
- Building apps with many user accounts that are infrequently updated
- Reducing rent costs for PDAs with low update frequency
Use regular accounts when:
- Account is updated frequently (>1000 lifetime writes)
- Account stores large data accessed in on-chain transactions
- Compute budget is critical (compression adds ~100k CU overhead)
Quick Start
Installation
# TypeScript client
npm install @lightprotocol/stateless.js @lightprotocol/compressed-token
# Rust SDK for programs
cargo add light-sdk
# CLI for development
npm install -g @lightprotocol/zk-compression-cli
Local Development
# Start local validator with compression support
light test-validator
# Initialize a new Anchor project with compression
light init my-program
Mint Compressed Tokens (TypeScript)
import { createRpc } from '@lightprotocol/stateless.js';
import { createMint, mintTo, transfer } from '@lightprotocol/compressed-token';
const rpc = createRpc(); // or createRpc('https://mainnet.helius-rpc.com?api-key=YOUR_KEY')
// Create mint with token pool for compression
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
// Mint compressed tokens (creates compressed token accounts)
await mintTo(rpc, payer, mint, recipient, payer, 1_000_000_000);
// Transfer compressed tokens
await transfer(rpc, payer, mint, 500_000_000, owner, recipient);
// Query compressed token accounts
const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint });
Build Program with Compressed PDAs (Anchor)
use anchor_lang::prelude::*;
use light_sdk::{
account::LightAccount,
address::v1::derive_address,
cpi::{v1::CpiAccounts, CpiSigner},
derive_light_cpi_signer,
instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator, LightHasher,
};
declare_id!("YourProgramID");
pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("YourProgramID");
#[program]
pub mod my_program {
use super::*;
use light_sdk::cpi::{v1::LightSystemProgramCpi, InvokeLightSystemProgram};
pub fn create_account<'info>(
ctx: Context<'_, '_, '_, 'info, MyAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let (address, address_seed) = derive_address(
&[b"my_account", ctx.accounts.signer.key().as_ref()],
&address_tree_info.get_tree_pubkey(&light_cpi_accounts)?,
&crate::ID,
);
let new_address_params = address_tree_info.into_new_address_params_packed(address_seed);
// Create new compressed account
let mut account = LightAccount::<MyAccount>::new_init(
&crate::ID,
Some(address),
output_state_tree_index,
);
account.owner = ctx.accounts.signer.key();
account.data = 0;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
Ok(())
}
pub fn update_account<'info>(
ctx: Context<'_, '_, '_, 'info, MyAccounts<'info>>,
proof: ValidityProof,
current_data: u64,
account_meta: CompressedAccountMeta,
) -> Result<()> {
// Modify existing compressed account
let mut account = LightAccount::<MyAccount>::new_mut(
&crate::ID,
&account_meta,
MyAccount {
owner: ctx.accounts.signer.key(),
data: current_data,
},
)?;
account.data = account.data.checked_add(1).unwrap();
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(account)?
.invoke(light_cpi_accounts)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct MyAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
#[event]
#[derive(Clone, Debug, Default, LightDiscriminator, LightHasher)]
pub struct MyAccount {
#[hash]
pub owner: Pubkey,
pub data: u64,
}
Core Concepts
Compressed Account Model
Compressed accounts are similar to regular Solana accounts but stored differently:
| Aspect | Regular Account | Compressed Account |
|---|---|---|
| Storage | AccountsDB (disk) | Ledger (call data) |
| Rent | Required (~0.002 SOL per 100 bytes) | None |
| Identification | Pubkey | Hash (changes on write) or Address |
| State validation | Runtime checks | ZK validity proofs |
Key differences:
- Hash changes on every write (accounts identified by content hash)
- Optional persistent address for PDAs (similar to regular PDAs)
- State stored in Merkle trees with only roots on-chain
State Trees
Compressed accounts are stored in concurrent Merkle trees using Poseidon hashing:
- Only the tree root is stored on-chain
- Leaves contain compressed account hashes
- Validity proofs prove account inclusion in tree
V2 Batched Merkle Trees (mainnet, January 2026): State root updates are batched and verified with ZK proofs, reducing costs by ~250x per update and overall CU usage by ~70% compared to V1 trees. V1 trees remain supported for existing deployments. New deployments automatically use V2 trees.
Validity Proofs
ZK proofs (Groth16) validate state transitions:
- Prove existence of N accounts in M state trees
- Constant 128-byte proof size regardless of accounts
- ~100k CU for proof verification
Transaction Lifecycle
- Build: Client fetches compressed accounts and validity proofs via RPC
- Submit: Transaction includes account data + proof in payload
- Validate: Light System Program verifies proof and account integrity
- Update: New state appended to tree, old state nullified
- Index: Photon RPC nodes index new state from ledger
LightAccount Operations
// Create new account (no input state, only output)
let account = LightAccount::<T>::new_init(&program_id, Some(address), tree_index);
// Modify existing account (input + output state)
let mut account = LightAccount::<T>::new_mut(&program_id, &account_meta, current_state)?;
account.field = new_value;
// Close account (input state, no output)
let account = LightAccount::<T>::new_close(&program_id, &account_meta, current_state)?;
Helius SDK Integration
Query compressed state via Helius RPC:
import { Helius } from 'helius-sdk';
const helius = new Helius('YOUR_API_KEY');
// Get compressed account by hash or address
const account = await helius.zk.getCompressedAccount({ address });
// Get all compressed accounts for owner
const accounts = await helius.zk.getCompressedAccountsByOwner(owner);
// Get compressed token accounts
const tokenAccounts = await helius.zk.getCompressedTokenAccountsByOwner(owner, { mint });
// Get validity proof for accounts
const proof = await helius.zk.getValidityProof({ hashes: [hash1, hash2] });
// Get compression signatures for account
const signatures = await helius.zk.getCompressionSignaturesForAccount(hash);
RPC Methods
ZK Compression RPC API (via Helius or self-hosted Photon):
| Method | Description |
|---|---|
getCompressedAccount |
Get account by hash or address |
getCompressedAccountsByOwner |
Get all accounts owned by pubkey |
getCompressedTokenAccountsByOwner |
Get token accounts for owner |
getCompressedTokenBalancesByOwner |
Get token balances summary |
getValidityProof |
Get ZK proof for account inclusion |
getMultipleCompressedAccounts |
Batch fetch accounts |
getCompressionSignaturesForAccount |
Get transaction history |
Infrastructure
Node Types
| Node | Purpose | Run By |
|---|---|---|
| Photon RPC | Index compressed state, serve queries | Helius (canonical), self-host |
| Prover | Generate validity proofs | Bundled with Photon or standalone |
| Forester | Maintain state trees, empty nullifier queues | Light Protocol, community |
Running Photon Locally
# Install
cargo install photon-indexer
# Run against devnet
photon --rpc-url=https://api.devnet.solana.com
# With Postgres for production
photon --db-url=postgres://postgres@localhost/postgres --rpc-url=<RPC_URL>
# Load from snapshot for faster bootstrap
photon-snapshot-loader --snapshot-dir=~/snapshot --snapshot-server-url=https://photon-devnet-snapshot.helius-rpc.com
Trade-offs
| Consideration | Impact |
|---|---|
| Larger transactions | +128 bytes for proof + account data in payload |
| Higher CU usage | ~100k CU proof verification + ~6k CU per account |
| Per-write cost | Each write nullifies old state, appends new |
| Indexer dependency | Requires Photon RPC (or self-host) for queries |
Break-even analysis: With V2 batched trees, compressed accounts are cost-effective for significantly more writes than V1's ~1000 write threshold due to 250x cheaper state root updates. Exact break-even depends on tree utilization and batch sizes.
Reference Documentation
- compressed-accounts.md - Detailed account model, hashing, addresses
- compressed-tokens.md - Token operations, pools, batch operations
- compressed-pdas.md - Building programs with compressed PDAs
- client-integration.md - TypeScript/Rust client setup, RPC methods
Resources
More from tenequm/skills
swift-macos
Comprehensive macOS app development with Swift 6.2, SwiftUI, SwiftData, Swift Concurrency, Foundation Models, Swift Testing, ScreenCaptureKit, and app distribution. Use when building native Mac apps, implementing windows/scenes/navigation/menus/toolbars, SwiftData models and queries, modern concurrency, on-device AI, testing, screen/audio capture, menu bar apps, AppKit bridges, login items, process monitoring, or App Store and Developer ID distribution. Triggers on macOS app, SwiftUI macOS, SwiftData, Swift concurrency, Foundation Models, Swift Testing, ScreenCaptureKit, screen capture, screen recording, AVFoundation, MenuBarExtra, NSViewRepresentable, notarize, login item, and process monitoring.
56react-typescript
Build React 19 applications with TypeScript. Covers Actions, Activity, use() hook, React Compiler, ref-as-prop, useEffectEvent, and strict TypeScript patterns. Use when creating components, managing state, typing props, handling events, using hooks, or working with React 19 features. Triggers on react, typescript, tsx, component types, hook types, react 19, react compiler, actions, use hook, useEffectEvent, activity, import defer.
45shadcn-tailwind
Build UIs with Tailwind CSS v4 and shadcn/ui. Covers CSS variables with OKLCH colors, component variants with CVA, responsive design, dark mode, and Tailwind v4.2 features. Supports Radix UI and Base UI primitives, CLI 3.0, and visual styles. Use when building interfaces with Tailwind, styling shadcn/ui components, implementing themes, or working with utility-first CSS. Triggers on tailwind, shadcn, utility classes, CSS variables, OKLCH, component styling, theming, dark mode, radix ui.
39python-dev
Opinionated Python development setup with uv + ty + ruff + pytest + just. Use when creating new Python projects, setting up pyproject.toml, configuring linting, type checking, testing, or build tooling. Triggers on "python project", "uv init", "pyproject.toml", "ruff config", "ty check", "pytest setup", "justfile", "python linting", "python formatting", "type checking python".
39privy-integration
Integrates Privy authentication, embedded wallets, and agent payment protocols into web and agentic apps. Covers React SDK (PrivyProvider, hooks, wagmi), Node.js SDK, smart wallets (ERC-4337), x402 and MPP machine payments, Tempo chain, and agentic wallets with policies. Use when setting up Privy auth, creating embedded or agentic wallets, adding x402 or MPP payments, integrating with Tempo, configuring wallet policies, or connecting Privy to MCP/Agent Auth flows.
36biome
Lint and format frontend code with Biome 2.4. Covers type-aware linting, GritQL custom rules, domains, import organizer, and migration from ESLint/Prettier. Use when configuring linting rules, formatting code, writing custom lint rules, or setting up CI checks. Triggers on biome, biome config, biome lint, biome format, biome check, biome ci, gritql, migrate from eslint, migrate from prettier, import sorting, code formatting, lint rules, type-aware linting, noFloatingPromises.
34