Skip to main content
Compressed accounts are closed via CPI to the Light System Program. Closing a compressed account
  • consumes the existing account hash, and
  • produces a new account hash with zero values to mark it as closed.
  • A closed compressed account can be reinitialized.
Find full code examples at the end for Anchor and native Rust.

Implementation Guide

This guide will cover the components of a Solana program that closes compressed accounts. Here is the complete flow to close compressed accounts:
1

Program Setup

DependenciesAdd dependencies to your program.
[dependencies]
light-sdk = "0.16.0"
anchor_lang = "0.31.1"
  • The light-sdk provides macros, wrappers and CPI interface to create and interact with compressed accounts.
  • Add the serialization library (borsh for native Rust, or use AnchorSerialize).
ConstantsSet program address and derive the CPI authority PDA to call the Light System program.
declare_id!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");

pub const LIGHT_CPI_SIGNER: CpiSigner =
    derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
CPISigner is the configuration struct for CPI’s to the Light System Program.
  • CPIs to the Light System program must be signed with a PDA derived by your program with the seed b"authority"
  • derive_light_cpi_signer! derives the CPI signer PDA for you at compile time.
Compressed AccountDefine your compressed account struct.
#[event] // declared as event so that it is part of the idl.
#[derive(
    Clone,
    Debug,
    Default,
    LightDiscriminator
)]
pub struct MyCompressedAccount {
    pub owner: Pubkey,
    pub message: String,
}
You derive
  • the standard traits (Clone, Debug, Default),
  • borsh or AnchorSerialize to serialize account data, and
  • LightDiscriminator to implements a unique type ID (8 bytes) to distinguish account types. The default compressed account layout enforces a discriminator in its own field, .
The traits listed above are required for LightAccount. LightAccount wraps MyCompressedAccount in Step 3 to set the discriminator and create the compressed account’s data.
2

Instruction Data

Define the instruction data with the following parameters:
pub fn close_account<'info>(
    ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
    proof: ValidityProof,
    account_meta: CompressedAccountMeta,
    current_message: String,
) -> Result<()>
  1. Validity Proof
  • Define proof to include the proof that the account exists in the state tree.
  • Clients fetch a validity proof with getValidityProof() from an RPC provider that supports ZK Compression (Helius, Triton, …).
  1. Specify input state and output state tree (stores closed account hash)
  • Define account_meta: CompressedAccountMeta to reference the existing account and specify the state tree to store the new hash with zero values:
    • tree_info: PackedStateTreeInfo: References the existing account hash in the state tree.
    • address: The account’s derived address.
    • output_state_tree_index points to the state tree that will store the updated hash with a zero-byte hash to mark the account as closed.
Clients fetch the current account with getCompressedAccount() and populate CompressedAccountMeta with the account’s metadata.
  1. Current data
  • Define fields to include the current account data passed by the client.
  • This depends on your program logic. This example includes the current_message field.
3

Close Compressed Account

Load the compressed account and mark it as closed with LightAccount::new_close().
new_close()
  1. hashes the current account data as input state and
  2. marks the account for closure for the Light System Program.
let my_compressed_account = LightAccount::<MyCompressedAccount>::new_close(
    &crate::ID,
    &account_meta,
    MyCompressedAccount {
        owner: ctx.accounts.signer.key(),
        message: current_message,
    },
)?;
Pass these parameters to new_close():
  • &program_id: The program’s ID that owns the compressed account.
  • &account_meta: The CompressedAccountMeta from instruction data (Step 2) that identifies the existing account and specifies the output state tree.
  • Current account data: The existing account data. The SDK hashes this input state for verification by the Light System Program.
    • Anchor: Construct MyCompressedAccount with ctx.accounts.signer.key() and current_message
    • Native: Construct MyCompressedAccount with data from instruction_data
The SDK creates:
  • A LightAccount wrapper similar to Anchor’s Account that marks the account for closure.
new_close() hashes the input state and marks the account for closure. The Light System Program creates output state with zero values:
  • a zero discriminator (0u8; 8) removes type identification of the account,
  • the output contains zeroes as data hash that indicates no data content, and
  • the data field contains an empty vector, instead of serialized account fields.
4

Light System Program CPI

Invoke the Light System Program to close the compressed account. This empty account can be reinitialized with LightAccount::new_empty().
The Light System Program
  • validates the account exists in state tree,
  • nullifies the existing account hash, and
  • appends the new account hash with zero values to the state tree to mark it as closed.
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(my_compressed_account)?
    .invoke(light_cpi_accounts)?;
Set up CpiAccounts::new():CpiAccounts::new() parses accounts for the CPI call to Light System Program.Pass these parameters:
  • ctx.accounts.signer.as_ref(): the transaction signer
  • ctx.remaining_accounts: Slice with [system_accounts, ...packed_tree_accounts]. The client builds this with PackedAccounts and passes it to the instruction.
  • &LIGHT_CPI_SIGNER: Your program’s CPI signer PDA defined in Constants.
1Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts.
2CPI Signer
  • PDA to sign CPI calls from your program to the Light System Program.
  • Verified by the Light System Program during CPI.
  • Derived from your program ID.
3Registered Program PDAProvides access control to the Account Compression Program.
4Signs CPI calls from the Light System Program to the Account Compression Program.
5
  • Writes to state and address tree accounts.
  • Clients and the Account Compression Program do not interact directly — handled internally.
6Solana System Program used to transfer lamports.
Build the CPI instruction:
  • new_cpi() initializes the CPI instruction with the proof to prove the compressed account exists in the state tree - defined in the Instruction Data (Step 2).
  • with_light_account adds the LightAccount wrapper configured to close the account with the zero values - defined in Step 3.
  • invoke() calls the Light System Program with CpiAccounts.

Full Code Example

The example programs below implement all steps from this guide. Make sure you have your developer environment set up first.
Install Solana CLI:
sh -c "$(curl -sSfL https://release.solana.com/v2.2.15/install)"
Install Anchor CLI:
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
Install the Light CLI:
npm install -g @lightprotocol/zk-compression-cli@0.27.1-alpha.2
Verify installation:
light --version
Find the source code for this example here.
#![allow(unexpected_cfgs)]
#![allow(deprecated)]

use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize};
use light_sdk::{
    account::LightAccount,
    address::v1::derive_address,
    cpi::{v1::CpiAccounts, CpiSigner},
    derive_light_cpi_signer,
    instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
    LightDiscriminator,
};

declare_id!("DzQ3za3DVCpXkXhmZVSrNchwbbSsJXmi9MBc8v5tvZuQ");

pub const LIGHT_CPI_SIGNER: CpiSigner =
    derive_light_cpi_signer!("DzQ3za3DVCpXkXhmZVSrNchwbbSsJXmi9MBc8v5tvZuQ");

#[program]
pub mod close {

    use super::*;
    use light_sdk::cpi::{
        v1::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction,
    };

    /// Setup: Create a compressed account
    pub fn create_account<'info>(
        ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
        proof: ValidityProof,
        address_tree_info: PackedAddressTreeInfo,
        output_state_tree_index: u8,
        message: String,
    ) -> 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"message", ctx.accounts.signer.key().as_ref()],
            &address_tree_info
                .get_tree_pubkey(&light_cpi_accounts)
                .map_err(|_| ErrorCode::AccountNotEnoughKeys)?,
            &crate::ID,
        );

        let mut my_compressed_account = LightAccount::<MyCompressedAccount>::new_init(
            &crate::ID,
            Some(address),
            output_state_tree_index,
        );

        my_compressed_account.owner = ctx.accounts.signer.key();
        my_compressed_account.message = message.clone();

        msg!(
            "Created compressed account with message: {}",
            my_compressed_account.message
        );

        LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
            .with_light_account(my_compressed_account)?
            .with_new_addresses(&[address_tree_info.into_new_address_params_packed(address_seed)])
            .invoke(light_cpi_accounts)?;

        Ok(())
    }

    /// Close compressed account
    pub fn close_account<'info>(
        ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
        proof: ValidityProof,
        account_meta: CompressedAccountMeta,
        current_message: String,
    ) -> Result<()> {
        let light_cpi_accounts = CpiAccounts::new(
            ctx.accounts.signer.as_ref(),
            ctx.remaining_accounts,
            crate::LIGHT_CPI_SIGNER,
        );

        let my_compressed_account = LightAccount::<MyCompressedAccount>::new_close(
            &crate::ID,
            &account_meta,
            MyCompressedAccount {
                owner: ctx.accounts.signer.key(),
                message: current_message,
            },
        )?;

        msg!("Close compressed account.");

        LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
            .with_light_account(my_compressed_account)?
            .invoke(light_cpi_accounts)?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct GenericAnchorAccounts<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
}

#[event]
#[derive(Clone, Debug, Default, LightDiscriminator)]
pub struct MyCompressedAccount {
    pub owner: Pubkey,
    pub message: String,
}

Next Steps