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


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<()>pub struct CloseInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
}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 and output state tree (stores closed account hash)
Define
account_meta: CompressedAccountMetato 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_indexpoints to the state tree that will store the updated hash with a zero-byte hash to mark the account as closed.
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_messagefield.
Close Compressed Account
Load the compressed account and mark it as closed with LightAccount::new_close().
new_close()
hashes the current account data as input state and
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,
},
)?;let my_compressed_account = LightAccount::<MyCompressedAccount>::new_close(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;Pass these parameters to new_close():
&program_id: The program's ID that owns the compressed account.&account_meta: TheCompressedAccountMetafrom 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
MyCompressedAccountwithctx.accounts.signer.key()andcurrent_messageNative: Construct
MyCompressedAccountwith data frominstruction_data
The SDK creates:
A
LightAccountwrapper similar to Anchor'sAccountthat marks the account for closure.
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 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 compressed account exists in the state tree - defined in the Instruction Data (Step 2).with_light_accountadds theLightAccountwrapper configured to close the account with the zero values - defined in Step 3.invoke()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, or simply run:
npm -g i @lightprotocol/[email protected]
light init testprogram#![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,
}#![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::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
use solana_program::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
pub const ID: Pubkey = pubkey!("NLusgr6vsEjYDvF6nDxpdrhMUxUC19s4XoyshSrGFVN");
pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("NLusgr6vsEjYDvF6nDxpdrhMUxUC19s4XoyshSrGFVN");
#[cfg(not(feature = "no-entrypoint"))]
entrypoint!(process_instruction);
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub enum InstructionType {
Create,
Close,
}
#[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 CloseInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
}
#[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::Close => close(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 close(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
CloseInstructionData::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_close(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
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 learn how to reinitialize compressed accounts.
Last updated
Was this helpful?