Skip to main content

Overview

The Light Protocol system program (SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7) is the core program for compressed account operations. It handles account creation, updates, and deletion using Merkle trees and zero-knowledge proofs. Program ID: SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7
Documentation: System Program

Instruction Types

Invoke

Direct compressed account operations

InvokeCpi

Cross-program invocation context

CPI Context

Account for storing CPI state

Discriminators

InstructionDiscriminator (8 bytes)Description
InitializeCpiContextAccount[163, 223, 82, 38, 39, 61, 233, 233]Initialize CPI context account
Invoke[163, 223, 82, 38, 39, 61, 233, 233]Execute compressed account ops
InvokeCpi[230, 224, 109, 130, 81, 205, 191, 65]CPI from another program
InvokeCpiWithReadOnlyCustomCPI with readonly accounts
InvokeCpiWithAccountInfoCustomCPI with account info mode

Invoke

Discriminator: [163, 223, 82, 38, 39, 61, 233, 233] Direct invocation for compressed account operations.

Instruction Data

pub struct InvokeInstructionData {
    pub input_compressed_accounts_with_merkle_context: 
        Vec<PackedCompressedAccountWithMerkleContext>,
    pub output_compressed_accounts: Vec<OutputCompressedAccountData>,
    pub relay_fee: Option<u64>,
    pub new_address_params: Vec<NewAddressParams>,
    pub compress_or_decompress_lamports: Option<u64>,
    pub is_compress: bool,
    pub proof: Option<CompressedProof>,
}
input_compressed_accounts_with_merkle_context
Vec<PackedCompressedAccountWithMerkleContext>
Compressed accounts to spend (with Merkle proofs and context)
output_compressed_accounts
Vec<OutputCompressedAccountData>
New compressed accounts to create
new_address_params
Vec<NewAddressParams>
Parameters for creating new addresses in address Merkle tree
compress_or_decompress_lamports
Option<u64>
Lamports to compress (move into Merkle tree) or decompress (move to Solana account)
is_compress
bool
True for compress operation, false for decompress
proof
Option<CompressedProof>
ZK proof for validity of operation (Groth16 proof, 128 bytes)

Operations

Create new compressed accounts in Merkle tree.Steps:
  1. Derive addresses for output accounts (if needed)
  2. Verify address non-inclusion proofs
  3. Hash output accounts
  4. Insert hashes into state tree output queue
  5. Emit event with account data
Requirements:
  • Authority must sign
  • Addresses must not exist (proven by ZK proof)
  • Sum check: input lamports = output lamports + fees
Spend existing compressed accounts and create new ones.Steps:
  1. Verify inclusion proofs for input accounts
  2. Compute and insert nullifiers (prevent double-spend)
  3. Create output accounts
  4. Verify sum check
Requirements:
  • Valid inclusion proofs for inputs
  • Authority signature
  • Nullifiers not already used
  • Sum check passes
Spend compressed accounts without creating outputs.Steps:
  1. Verify inclusion proofs
  2. Insert nullifiers
  3. Decompress lamports to Solana account
Use case: Close compressed accounts and reclaim lamports
Move lamports from Solana account into compressed account.Steps:
  1. Transfer lamports from fee payer
  2. Create compressed account with lamports
  3. Insert into Merkle tree
Benefit: Reduces rent costs (compressed accounts don’t pay rent)
Move lamports from compressed account to Solana account.Steps:
  1. Verify inclusion proof for compressed account
  2. Insert nullifier (spend account)
  3. Transfer lamports to destination Solana account
Use case: Access lamports for non-Light Protocol operations

Example

// Create new compressed account
let output = OutputCompressedAccountData {
    owner: program_id,
    lamports: 1_000_000,
    data: Some(account_data),
    address: Some(derived_address),
};

let ix_data = InvokeInstructionData {
    input_compressed_accounts_with_merkle_context: vec![],
    output_compressed_accounts: vec![output],
    new_address_params: vec![address_params],
    proof: Some(address_proof),
    compress_or_decompress_lamports: Some(1_000_000),
    is_compress: true,
    relay_fee: None,
};

InvokeCpi

Discriminator: [230, 224, 109, 130, 81, 205, 191, 65] Cross-program invocation for compressed account operations.

Instruction Data

pub struct InvokeCpiInstructionData {
    pub inputs: InvokeInstructionData,
    pub cpi_context: Option<CpiContext>,
}

pub struct CpiContext {
    pub invoking_program: Pubkey,
    pub cpi_context_account_index: u8,
    pub execute: bool,
}
inputs
InvokeInstructionData
Same data structure as Invoke instruction
cpi_context
Option<CpiContext>
CPI context for tracking cross-program state:
  • invoking_program: Program making the CPI
  • cpi_context_account_index: Index of CPI context account
  • execute: Whether to execute (true) or just validate (false)

CPI Context Account

Accounts called via CPI must use a CPI context account to store intermediate state:
pub struct CpiContextAccount {
    pub discriminator: [u8; 8],
    pub fee_payer: Pubkey,
    pub associated_merkle_tree: Pubkey,
    pub cpi_signature_account_index: u8,
    pub execute: bool,
}
Purpose: Store state between CPI setup and execution

CPI Flow

1

Initialize CPI Context

Calling program initializes CPI context account with InitializeCpiContextAccount
2

Setup Phase

Call InvokeCpi with execute: false to validate and prepare state
3

Execution Phase

Call InvokeCpi with execute: true to perform the operation
4

Cleanup

CPI context account can be reused for next operation

Example

// In your program making CPI to system program

// 1. Initialize CPI context (once)
let cpi_context_ix = create_initialize_cpi_context_account_instruction(
    &payer.pubkey(),
    &cpi_context_account.pubkey(),
);

// 2. Setup phase
let cpi_data = InvokeCpiInstructionData {
    inputs: /* ... */,
    cpi_context: Some(CpiContext {
        invoking_program: my_program_id,
        cpi_context_account_index: cpi_context_index,
        execute: false,  // Setup only
    }),
};

invoke_cpi_instruction(/* ... */)?;

// 3. Execute phase  
cpi_data.cpi_context.as_mut().unwrap().execute = true;
invoke_cpi_instruction(/* ... */)?;

InvokeCpiWithReadOnly

Variant of InvokeCpi that supports readonly input accounts. Use case: Read compressed account data without spending it
pub struct InvokeCpiInstructionDataWithReadOnly {
    pub inputs: InvokeInstructionData,
    pub cpi_context: CpiContext,
    pub readonly_indices: Vec<u8>,
}

InitializeCpiContextAccount

Initialize CPI context account for cross-program calls. Accounts:
  1. Fee payer (mut, signer)
  2. CPI context account (mut)
  3. System program
Account size: 200 bytes (expandable)

Accounts

Input Accounts

authority
Signer
required
Authority for input compressed accounts (must sign)
fee_payer
Signer, Writable
required
Pays transaction fees and provides lamports for compression
registered_program_pda
Account
PDA proving program is registered to use Merkle trees
noop_program
Program
Noop program for emitting events (SPL Noop)
account_compression_program
Program
Account compression program managing Merkle trees
merkle_tree_accounts
[Account]
State and address Merkle tree accounts (dynamic count)
nullifier_queue_accounts
[Account, Writable]
Nullifier queue accounts for spent account tracking
output_queue_accounts
[Account, Writable]
Output queue accounts for new compressed accounts

Error Codes

Common error codes (11000+ range):
CodeErrorDescription
11001InvalidAuthorityAuthority check failed
11002InvalidProofZK proof verification failed
11003SumCheckFailedInput/output lamports don’t match
11004InvalidMerkleTreeWrong Merkle tree account
11005InvalidNullifierQueueWrong nullifier queue
11006DuplicateNullifierNullifier already exists
11007InvalidAddressAddress derivation failed
11008AddressAlreadyExistsAddress already in tree
11009InvalidCpiContextCPI context validation failed
11010InvalidProgramOwnerProgram not registered

Best Practices

Inclusion and non-inclusion proofs must be fresh and correspond to current Merkle roots. Get proofs from Photon indexer.
Client-side verification: ensure total input lamports = total output lamports + fees. On-chain verification will reject mismatches.
When calling via CPI, always initialize CPI context account first, then use two-phase (setup + execute) pattern.
Each compressed account can only be spent once. Track nullifiers to prevent double-spend attempts.
Programs that use compressed accounts must be registered with the account compression program.

Resources

Program Source

View on GitHub

SDK

TypeScript SDK (stateless.js)

Rust SDK

Rust program SDK

Examples

Program examples

Build docs developers (and LLMs) love