Get the FREE Ultimate OpenClaw Setup Guide →

solana-compression

Scanned
npx machina-cli add skill tenequm/claude-plugins/solana-compression --openclaw
Files (1)
SKILL.md
10.9 KB

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:

AspectRegular AccountCompressed Account
StorageAccountsDB (disk)Ledger (call data)
RentRequired (~0.002 SOL per 100 bytes)None
IdentificationPubkeyHash (changes on write) or Address
State validationRuntime checksZK 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

  1. Build: Client fetches compressed accounts and validity proofs via RPC
  2. Submit: Transaction includes account data + proof in payload
  3. Validate: Light System Program verifies proof and account integrity
  4. Update: New state appended to tree, old state nullified
  5. 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):

MethodDescription
getCompressedAccountGet account by hash or address
getCompressedAccountsByOwnerGet all accounts owned by pubkey
getCompressedTokenAccountsByOwnerGet token accounts for owner
getCompressedTokenBalancesByOwnerGet token balances summary
getValidityProofGet ZK proof for account inclusion
getMultipleCompressedAccountsBatch fetch accounts
getCompressionSignaturesForAccountGet transaction history

Infrastructure

Node Types

NodePurposeRun By
Photon RPCIndex compressed state, serve queriesHelius (canonical), self-host
ProverGenerate validity proofsBundled with Photon or standalone
ForesterMaintain state trees, empty nullifier queuesLight 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

ConsiderationImpact
Larger transactions+128 bytes for proof + account data in payload
Higher CU usage~100k CU proof verification + ~6k CU per account
Per-write costEach write nullifies old state, appends new
Indexer dependencyRequires 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

Resources

Source

git clone https://github.com/tenequm/claude-plugins/blob/main/solana/skills/solana-compression/SKILL.mdView on GitHub

Overview

ZK Compression enables rent-free state on Solana by storing state on-chain and validating transitions with zero-knowledge proofs. Built by Light Protocol and indexed by Helius Photon, it enables compressed tokens and PDAs at scale while reducing rent costs.

How This Skill Works

ZK Compression uses a state-tree model where state is stored on-chain and proofs validate transitions. It leverages Light Protocol tooling and PackedAddressTreeInfo to create compressed accounts and PDAs, while clients query via Helius Photon RPC for validation and data access.

When to Use It

  • Creating millions of token accounts (≈5000x cheaper than regular accounts)
  • Minting to many recipients (airdrops, loyalty programs, gaming assets)
  • Apps with many user accounts that are infrequently updated
  • Reducing rent costs for PDAs with low update frequency
  • Compute budget is critical or updates are frequent—consider regular accounts if compression overhead is a concern

Quick Start

  1. Step 1: Install dependencies (TypeScript client and tooling): npm install @lightprotocol/stateless.js @lightprotocol/compressed-token; cargo add light-sdk; npm install -g @lightprotocol/zk-compression-cli
  2. Step 2: Start local development: light test-validator and light init my-program
  3. Step 3: Mint compressed tokens and query: createMint, mintTo, and getCompressedTokenAccountsByOwner via RPC

Best Practices

  • Map data to compressed accounts and a state tree to maximize rent savings
  • Compare update frequency to determine compression viability vs regular accounts
  • Leverage Light Protocol SDKs and Helius/Photon RPC for client integration
  • Run local validator with compression support for testing
  • Benchmark compute usage, since compression adds ~100k CU overhead

Example Use Cases

  • Mass airdrops of compressed tokens to millions of recipients
  • Gaming assets distributed as compressed tokens to large player bases
  • Loyalty programs with millions of user accounts using compressed PDAs
  • PDAs stored as compressed accounts to reduce rent across many addresses
  • Applications querying state via Helius Photon RPC for validation

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers