Skip to main content
Compressed accounts are permanently burned via CPI to the Light System Program. Burning a compressed account
  • consumes the existing account hash, and
  • produces no output state.
  • A burned account cannot 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 burns compressed accounts.
Here is the complete flow to burn 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 burn_account<'info>(
    ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
    proof: ValidityProof,
    account_meta: CompressedAccountMetaBurn,
    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
  • Define account_meta: CompressedAccountMetaBurn to reference the existing account for the Light System Program to nullify permanently:
    • tree_info: PackedStateTreeInfo: References the existing account hash in the state tree.
    • address: The account’s derived address.
Burn does not specify an output state tree. CompressedAccountMetaBurn omits output_state_tree_index because no output state is created.
  1. Current account data
  • Define fields to include the current account data passed by the client.
  • This depends on your program logic. This example includes current_message (or current_account in Native Rust).
3

Burn Compressed Account

Burn the compressed account permanently with LightAccount::new_burn(). No account can be reinitialized at this address in the future.
new_burn()
  1. hashes the current account data as input state and
  2. creates no output state to burn the account permanently.
let my_compressed_account = LightAccount::<MyCompressedAccount>::new_burn(
    &crate::ID,
    &account_meta,
    MyCompressedAccount {
        owner: ctx.accounts.signer.key(),
        message: current_message,
    },
)?;
Pass these parameters to new_burn():
  • &program_id: The program’s ID that owns the compressed account.
  • &account_meta: The CompressedAccountMetaBurn from instruction data (Step 2) that identifies the existing account for the Light System Program to nullify permanently.
    • Anchor: Pass &account_meta directly
    • Native Rust: Pass &instruction_data.account_meta
  • Include the curent account data.
    • Anchor: Build MyCompressedAccount with owner and message.
    • Native Rust: Pass instruction_data.current_account directly.
The SDK creates:
  • A LightAccount wrapper that marks the account as permanently burned with no output state.
new_burn() hashes the input state. The Light System Program verifies the input hash and nullifies it in Step 4.
4

Light System Program CPI

The Light System Program CPI burns the compressed account permanently.
The Light System Program
  • validates the account exists in state tree with the validity,
  • nullifies the existing account hash, and
  • creates no output state.
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 account exists in the state tree - defined in the Instruction Data (Step 2).
  • with_light_account adds the LightAccount wrapper configured to burn the account - defined in Step 3.
  • invoke(light_cpi_accounts) calls the Light System Program with CpiAccounts.

Full Code Example

The example programs below implement all steps from this guide.
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
For help with debugging, see the Error Cheatsheet.
Find the source code 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::CompressedAccountMetaBurn, PackedAddressTreeInfo, ValidityProof},
    LightDiscriminator,
};

declare_id!("BJhPWQnD31mdo6739Mac1gLuSsbbwTmpgjHsW6shf6WA");

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

#[program]
pub mod burn {

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

    /// Setup: Creates 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(())
    }

    /// Burns a compressed account permanently
    pub fn burn_account<'info>(
        ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
        proof: ValidityProof,
        account_meta: CompressedAccountMetaBurn,
        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_burn(
            &crate::ID,
            &account_meta,
            MyCompressedAccount {
                owner: ctx.accounts.signer.key(),
                message: current_message,
            },
        )?;

        msg!("Burning compressed account permanently");

        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