How to Update Compressed Accounts
Guide to update compressed accounts in Solana programs with full code examples.
Overview
Compressed accounts are updated via CPI to the Light System Program.
The update of a compressed account follows a UTXO pattern, unlike regular Solana accounts that overwrite data in place. Each update of a compressed account
consumes the existing account hash and
produces a new account hash with updated data.
The existing account hash is nullified to prevent double spending.
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 updates compressed accounts. Here is the complete flow:


Instruction Data
Define the instruction data with the following parameters:
Anchor handles instruction deserialization automatically. Pass the parameters directly to the instruction function:
pub fn update_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
current_account: MyCompressedAccount,
account_meta: CompressedAccountMeta,
new_message: String,
) -> Result<()>pub struct UpdateInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
pub new_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 updated account hash)
Define
account_meta: CompressedAccountMetato reference the existing account and specify the state tree to store the updated account hash:tree_info: PackedStateTreeInfo: References the existing account hash in the state tree.address: The account's derived address.output_state_tree_index: References the state tree account that will store the updated account hash.
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 Anchor) andnew_messagefields.new_messagecontains the new data that will replace the message field of the compressed account after the update.
Update Compressed Account
Load the compressed account and update it with LightAccount::new_mut().
new_mut():
hashes the current account data as input state and
lets your program define the output state.
let mut my_compressed_account = LightAccount::<MyCompressedAccount>::new_mut(
&crate::ID,
&account_meta,
current_account,
)?;
my_compressed_account.message = new_message.clone();let mut my_compressed_account = LightAccount::<MyCompressedAccount>::new_mut(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
my_compressed_account.account.message = instruction_data.new_message;Pass these parameters to new_mut():
&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.Include the curent account data.
Anchor: Pass
current_accountdirectlyNative: Construct
MyCompressedAccountwith data frominstruction_data
The SDK creates:
A
LightAccountwrapper similar to Anchor'sAccount.new_mut()lets the program modify the output state. This example setsmessagetonew_message.
Light System Program CPI
Invoke the Light System Program to update the compressed account.
The Light System Program
validates the account exists in state tree,
nullifies the existing account hash in the state tree, and
appends the updated account hash to the state tree.
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 = accounts.first();
let light_cpi_accounts = CpiAccounts::new(
signer,
&accounts[1..],
LIGHT_CPI_SIGNER
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.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:
signer: account that signs and pays for the transaction&accounts[1..]: Slice with[system_accounts, ...packed_tree_accounts]. The client builds this withPackedAccounts.&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 that the account exists in the specified state tree - in the Instruction Data (Step 2).with_light_accountadds theLightAccountwith the modified compressed account data - defined in Step 3invoke(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, or simply run:
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::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
declare_id!("Cj3DxyqB7wJh511VKexsjKt7Hx1kvPvCBMrbLuL8grKc");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("Cj3DxyqB7wJh511VKexsjKt7Hx1kvPvCBMrbLuL8grKc");
#[program]
pub mod update {
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(())
}
/// Updates an existing compressed account's message
pub fn update_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
current_account: MyCompressedAccount,
account_meta: CompressedAccountMeta,
new_message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let mut my_compressed_account = LightAccount::<MyCompressedAccount>::new_mut(
&crate::ID,
&account_meta,
current_account,
)?;
my_compressed_account.message = new_message.clone();
msg!(
"Updated compressed account message to: {}",
my_compressed_account.message
);
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!("2m6LXA7E6kMSkK6QHq2WCznD6kvhDcVFqEKpETKAQxYe");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("2m6LXA7E6kMSkK6QHq2WCznD6kvhDcVFqEKpETKAQxYe");
entrypoint!(process_instruction);
#[repr(u8)]
#[derive(Debug)]
pub enum InstructionType {
Create = 0,
Update = 1,
}
impl TryFrom<u8> for InstructionType {
type Error = LightSdkError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(InstructionType::Create),
1 => Ok(InstructionType::Update),
_ => panic!("Invalid instruction discriminator."),
}
}
}
#[derive(
Debug, Default, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator,
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UpdateInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
pub new_message: String,
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
if program_id != &ID {
return Err(ProgramError::IncorrectProgramId);
}
if instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
let discriminator = InstructionType::try_from(instruction_data[0])
.map_err(|_| ProgramError::InvalidInstructionData)?;
match discriminator {
InstructionType::Create => {
let instruction_data =
CreateInstructionData::try_from_slice(&instruction_data[1..])
.map_err(|_| ProgramError::InvalidInstructionData)?;
create(accounts, instruction_data)
}
InstructionType::Update => {
let instruction_data =
UpdateInstructionData::try_from_slice(&instruction_data[1..])
.map_err(|_| ProgramError::InvalidInstructionData)?;
update(accounts, instruction_data)
}
}
}
pub fn create(
accounts: &[AccountInfo],
instruction_data: CreateInstructionData,
) -> Result<(), ProgramError> {
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(())
}
pub fn update(
accounts: &[AccountInfo],
instruction_data: UpdateInstructionData,
) -> Result<(), ProgramError> {
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(
signer,
&accounts[1..],
LIGHT_CPI_SIGNER
);
let mut my_compressed_account = LightAccount::<MyCompressedAccount>::new_mut(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
// Update the account data with new message
my_compressed_account.account.message = instruction_data.new_message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
Ok(())
}Next Steps
Build a client for your program or learn how to close compressed accounts.
Last updated
Was this helpful?