How to Burn Compressed Accounts
Guide to burn compressed accounts in Solana programs with full code examples.
Overview
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:


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<()>pub struct BurnInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMetaBurn,
pub current_account: MyCompressedAccount,
}Validity Proof
Define
proofto 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, ...).
Specify input state
Define
account_meta: CompressedAccountMetaBurnto 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.
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(orcurrent_accountin Native Rust).
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()
hashes the current account data as input state and
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,
},
)?;let my_compressed_account = LightAccount::<MyCompressedAccount>::new_burn(
&ID,
&instruction_data.account_meta,
instruction_data.current_account,
)?;Pass these parameters to new_burn():
&program_id: The program's ID that owns the compressed account.&account_meta: TheCompressedAccountMetaBurnfrom instruction data (Step 2) that identifies the existing account for the Light System Program to nullify permanently.Anchor: Pass
&account_metadirectlyNative Rust: Pass
&instruction_data.account_meta
Include the curent account data.
Anchor: Build
MyCompressedAccountwithownerandmessage.Native Rust: Pass
instruction_data.current_accountdirectly.
The SDK creates:
A
LightAccountwrapper that marks the account as burned permanently with no output state.
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 permanently, 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 signerctx.remaining_accounts: Slice with[system_accounts, ...packed_tree_accounts]. The client builds this withPackedAccountsand passes it to the instruction.&LIGHT_CPI_SIGNER: Your program's CPI signer PDA defined in Constants.
let (signer, remaining_accounts) = accounts
.split_first();
let cpi_accounts = CpiAccounts::new(
signer,
remaining_accounts,
LIGHT_CPI_SIGNER
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;Set up CpiAccounts::new():
CpiAccounts::new() parses accounts for the CPI call to Light System Program.
Pass these parameters:
signer: account that signs and pays for the transactionremaining_accounts: Slice with[system_accounts, ...packed_tree_accounts]. The client builds this withPackedAccounts.split_first()extracts the fee payer from the accounts array to separate it from the Light System Program accounts needed for the CPI.
&LIGHT_CPI_SIGNER: Your program's CPI signer PDA defined in Constants.
Build the CPI instruction:
new_cpi()initializes the CPI instruction with theproofto prove the account exists in the state tree - defined in the Instruction Data (Step 2).with_light_accountadds theLightAccountwrapper configured to burn the account - defined in Step 3.invoke(light_cpi_accounts)calls the Light System Program withCpiAccounts.
Full Code Example
The example programs below implement all steps from this guide. Make sure you have your developer environment set up first.
npm -g i @lightprotocol/[email protected]
light init testprogramFor help with debugging, see the Error Cheatsheet.
#![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,
}#![allow(unexpected_cfgs)]
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers;
use borsh::{BorshDeserialize, BorshSerialize};
use light_macros::pubkey;
use light_sdk::{
account::sha::LightAccount,
address::v1::derive_address,
cpi::{
v1::{CpiAccounts, LightSystemProgramCpi},
CpiSigner, InvokeLightSystemProgram, LightCpiInstruction,
},
derive_light_cpi_signer,
error::LightSdkError,
instruction::{account_meta::CompressedAccountMetaBurn, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
use solana_program::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
pub const ID: Pubkey = pubkey!("CFWrQ8za2yT1xH8yBjYvsDUCWnBH7vXtyVJwqoX5FcNg");
pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("CFWrQ8za2yT1xH8yBjYvsDUCWnBH7vXtyVJwqoX5FcNg");
#[cfg(not(feature = "no-entrypoint"))]
entrypoint!(process_instruction);
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub enum InstructionType {
Create,
Burn,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct BurnInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMetaBurn,
pub current_account: MyCompressedAccount,
}
#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
let (instruction_type, rest) = instruction_data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
match InstructionType::try_from_slice(&[*instruction_type])
.map_err(|_| ProgramError::InvalidInstructionData)?
{
InstructionType::Create => create(accounts, rest)?,
InstructionType::Burn => burn(accounts, rest)?,
}
Ok(())
}
fn create(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
CreateInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(
signer,
&accounts[1..],
LIGHT_CPI_SIGNER
);
let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ProgramError::NotEnoughAccountKeys)?,
&ID,
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_packed(address_seed);
let mut my_compressed_account = LightAccount::<MyCompressedAccount>::new_init(
&ID,
Some(address),
instruction_data.output_state_tree_index,
);
my_compressed_account.owner = *signer.key;
my_compressed_account.message = instruction_data.message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
Ok(())
}
fn burn(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
BurnInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let (signer, remaining_accounts) = accounts
.split_first()
.ok_or(ProgramError::InvalidAccountData)?;
let cpi_accounts = CpiAccounts::new(
signer,
remaining_accounts,
LIGHT_CPI_SIGNER
);
let my_compressed_account = LightAccount::<MyCompressedAccount>::new_burn(
&ID, // Now the burn program owns the account since it created it
&instruction_data.account_meta,
instruction_data.current_account,
)?;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
Ok(())
}Next Steps
Build a client for your program or get an overview on all compressed account operations.
Last updated
Was this helpful?