Skip to main content

Overview

The light-compressed-account crate provides the core compressed account struct and utility types for Light Protocol. It defines the fundamental data structures used throughout the protocol for state compression. Crate: light-compressed-account
Location: program-libs/compressed-account/
Error Codes: 12001-12025

Key Types

CompressedAccount

Core struct representing compressed state

Address

Deterministic address derivation

Nullifier

Spend tracking for compressed accounts

Instruction Data

ZK proof structures and CPI contexts

CompressedAccount

Structure

#[derive(Debug, Clone, PartialEq)]
pub struct CompressedAccount {
    pub owner: [u8; 32],
    pub lamports: u64,
    pub address: Option<[u8; 32]>,
    pub data: Option<CompressedAccountData>,
}

pub struct CompressedAccountData {
    pub discriminator: [u8; 8],
    pub data: Vec<u8>,
    pub data_hash: [u8; 32],
}

Fields

owner
[u8; 32]
required
Program that owns this compressed account (similar to Solana account owner)
lamports
u64
required
Lamport balance stored in this account
address
Option<[u8; 32]>
Optional deterministic address. Required for accounts with data. Derived from seeds using derive_address().
data
Option<CompressedAccountData>
Optional account data:
  • discriminator: 8-byte type identifier
  • data: Raw account data (variable length)
  • data_hash: Poseidon hash of discriminator || data

Hashing

Compressed accounts are hashed before insertion into Merkle trees:
use light_hasher::{Poseidon, Hasher};

pub fn hash_compressed_account(
    account: &CompressedAccount,
) -> Result<[u8; 32], HasherError> {
    let data_hash = account.data
        .as_ref()
        .map(|d| d.data_hash)
        .unwrap_or([0u8; 32]);
    
    let address = account.address.unwrap_or([0u8; 32]);
    
    Poseidon::hashv(&[
        &account.owner,
        &account.lamports.to_le_bytes(),
        &address,
        &data_hash,
    ])
}
Hash inputs (in order):
  1. Owner (32 bytes)
  2. Lamports (8 bytes, little-endian)
  3. Address or zero (32 bytes)
  4. Data hash or zero (32 bytes)

Address Derivation

derive_address

Deterministically derive address from seeds:
use light_compressed_account::address::derive_address;

pub fn derive_address(
    seeds: &[[u8]],
    program_id: &[u8; 32],
) -> Result<[u8; 32], CompressedAccountError>
Example:
let seeds = &[
    b"token-account",
    owner_pubkey.as_ref(),
    mint_pubkey.as_ref(),
];

let address = derive_address(seeds, &program_id)?;

Address Seeds

Structured seed representation:
pub struct AddressSeeds {
    pub seeds: Vec<Vec<u8>>,
    pub program_id: [u8; 32],
}

impl AddressSeeds {
    pub fn derive_address(&self) -> Result<[u8; 32], CompressedAccountError> {
        derive_address(
            &self.seeds.iter().map(|s| s.as_slice()).collect::<Vec<_>>(),
            &self.program_id,
        )
    }
}

Nullifier Computation

Nullifiers prevent double-spending of compressed accounts:
use light_compressed_account::nullifier::derive_nullifier;

pub fn derive_nullifier(
    compressed_account_hash: &[u8; 32],
    leaf_index: u64,
    merkle_tree_pubkey: &[u8; 32],
) -> Result<[u8; 32], HasherError>
Hash inputs:
  1. Compressed account hash
  2. Leaf index (8 bytes, little-endian)
  3. Merkle tree pubkey
Example:
let nullifier = derive_nullifier(
    &account_hash,
    leaf_index,
    &tree_pubkey,
)?;

// Insert nullifier into tree's input queue
tree.insert_nullifier_into_queue(nullifier)?;

Instruction Data

CompressedProof

Gro th16 ZK proof (128 bytes):
pub struct CompressedProof {
    pub a: [u8; 32],     // G1 point (compressed)
    pub b: [u8; 64],     // G2 point (compressed)
    pub c: [u8; 32],     // G1 point (compressed)
}

PackedCompressedAccountWithMerkleContext

Compressed account with proof data:
pub struct PackedCompressedAccountWithMerkleContext {
    pub compressed_account: CompressedAccount,
    pub merkle_context: PackedMerkleContext,
    pub root_index: u16,
    pub read_only: bool,
}

pub struct PackedMerkleContext {
    pub merkle_tree_pubkey_index: u8,
    pub nullifier_queue_pubkey_index: u8,
    pub leaf_index: u32,
    pub queue_index: Option<u16>,
}
Purpose: Used in transaction inputs to prove account existence

NewAddressParams

Parameters for creating new addresses:
pub struct NewAddressParams {
    pub seed: [u8; 32],
    pub address_queue_pubkey: [u8; 32],
    pub address_merkle_tree_pubkey: [u8; 32],
    pub address_merkle_tree_root_index: u16,
}

CpiContext

Cross-program invocation context:
pub struct CpiContext {
    pub invoking_program: [u8; 32],
    pub cpi_context_account_index: u8,
    pub execute: bool,
}

Queue Types

Enum for queue variants:
pub enum QueueType {
    NullifierV1 = 1,
    AddressV1 = 2,
    InputStateV2 = 3,
    AddressV2 = 4,
    OutputStateV2 = 5,
}

Tree Types

Enum for Merkle tree variants:
pub enum TreeType {
    StateV1 = 1,
    AddressV1 = 2,
    StateV2 = 3,
    AddressV2 = 4,
    Unknown = 255,
}

Error Codes

CodeErrorDescription
12001InputTooLargeInput exceeds maximum size
12002InvalidChunkSizeChunk size validation failed
12003InvalidSeedsInvalid seeds for address derivation
12004InvalidRolloverThresholdInvalid tree rollover threshold
12005InvalidInputLengthInput length doesn’t match expected
12010InvalidAccountSizeAccount size mismatch
12011AccountMutableAccount mutability error
12012AlreadyInitializedAccount already initialized
12013InvalidAccountBalanceInvalid lamport balance
12014FailedBorrowRentSysvarFailed to access rent sysvar
12015DeriveAddressErrorAddress derivation failed
12016InvalidArgumentInvalid function argument
12017ZeroCopyExpectedAddressExpected address in zero-copy data
12018InstructionDataExpectedAddressExpected address in instruction
12019CompressedAccountDataNotInitializedAccount data not initialized
12020ExpectedDiscriminatorMissing discriminator
12021InstructionDataExpectedProofMissing proof in instruction
12022ZeroCopyExpectedProofExpected proof in zero-copy data
12023ExpectedDataHashMissing data hash
12024InvalidCpiContextInvalid CPI context configuration
12025InvalidProofSizeProof size != 128 bytes

Usage Examples

Creating Compressed Accounts

use light_compressed_account::{
    compressed_account::{CompressedAccount, CompressedAccountData},
    address::derive_address,
};
use light_hasher::{Poseidon, Hasher, DataHasher};

// Derive address
let seeds = &[b"my-account", owner.as_ref()];
let address = derive_address(seeds, &program_id)?;

// Create account data
let data = MyAccountData {
    value: 42,
}.try_to_vec()?;

let data_hash = MyAccountData::hash_with_discriminator(&data)?;

// Create compressed account
let account = CompressedAccount {
    owner: program_id,
    lamports: 1_000_000,
    address: Some(address),
    data: Some(CompressedAccountData {
        discriminator: MyAccountData::DISCRIMINATOR,
        data,
        data_hash,
    }),
};

// Hash for Merkle tree insertion
let account_hash = hash_compressed_account(&account)?;

Computing Nullifiers

use light_compressed_account::nullifier::derive_nullifier;

// When spending a compressed account
let nullifier = derive_nullifier(
    &account_hash,
    leaf_index,
    &merkle_tree_pubkey,
)?;

// Insert into nullifier queue
tree.insert_nullifier_into_queue(nullifier)?;

Validating Proofs

use light_compressed_account::instruction_data::compressed_proof::CompressedProof;
use light_verifier::verify_inclusion_proof;

// Verify account inclusion
let proof = CompressedProof {
    a: /* ... */,
    b: /* ... */,
    c: /* ... */,
};

verify_inclusion_proof(
    &[root],
    &[account_hash],
    &proof,
)?;

Constants

Program IDs and discriminators:
// Program IDs
pub const SYSTEM_PROGRAM_ID: [u8; 32] = /* SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7 */;
pub const ACCOUNT_COMPRESSION_PROGRAM_ID: [u8; 32] = /* compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq */;
pub const COMPRESSED_TOKEN_PROGRAM_ID: [u8; 32] = /* cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m */;

// Instruction discriminators
pub const INVOKE_DISCRIMINATOR: [u8; 8] = [163, 223, 82, 38, 39, 61, 233, 233];
pub const INVOKE_CPI_DISCRIMINATOR: [u8; 8] = [230, 224, 109, 130, 81, 205, 191, 65];

Zero-Copy Types

For efficient on-chain processing:
use light_zero_copy::ZeroCopy;

// Zero-copy compressed account
pub struct ZCompressedAccount {
    // Direct memory access without deserialization
}

impl ZeroCopy for ZCompressedAccount {
    fn zero_copy_at(data: &[u8]) -> Result<(&Self, &[u8]), Error>;
}

Best Practices

Accounts with data MUST have deterministic addresses. This ensures uniqueness and enables lookups.
Always use discriminator || data when computing data hashes. The discriminator must be included.
Always verify ZK proofs before modifying any state or transferring lamports.
Store and check nullifiers to prevent double-spending of compressed accounts.

Feature Flags

std
feature
Enables standard library features
anchor
feature
Enables Anchor framework integration
solana
feature
Enables Solana SDK integration
pinocchio
feature
Enables Pinocchio SDK integration
alloc
feature
Enables allocation without std

Resources

Source Code

View on GitHub

API Docs

Rust documentation

System Program

System program using compressed accounts

Light Protocol Paper

Technical whitepaper

Build docs developers (and LLMs) love