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.

Implementation Guide

This guide will cover the components of a Solana program that updates compressed accounts. Here is the complete flow:

1

Program Setup

Dependencies, Constants, Compressed Account

Dependencies

Add dependencies to your program.

[dependencies]
light-sdk = "0.16.0"
anchor_lang = "0.31.1"
[dependencies]
light-sdk = "0.16.0"
borsh = "0.10.0"
solana-program = "2.2"
  • 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).

Constants

Set 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 Account

Define 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,}#[derive( Debug, Default, Clone, BorshSerialize, BorshDeserialize, 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:

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<()>
  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 updated account hash)

  • Define account_meta: CompressedAccountMeta to 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.

Clients fetch the current account with getCompressedAccount() and populate CompressedAccountMeta with the account's metadata.

  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 Anchor) and new_message fields.

    • new_message contains the new data that will replace the message field of the compressed account after the update.

3

Update Compressed Account

Load the compressed account and update it with LightAccount::new_mut().

let mut my_compressed_account = LightAccount::<MyCompressedAccount>::new_mut(
    &crate::ID,
    &account_meta,
    current_account,
)?;

my_compressed_account.message = new_message.clone();

Pass these parameters to new_mut():

  • &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.

  • Include the curent account data.

    • Anchor: Pass current_account directly

    • Native: Construct MyCompressedAccount with data from instruction_data

The SDK creates:

  • A LightAccount wrapper similar to Anchor's Account.

  • new_mut() lets the program modify the output state. This example sets message to new_message.

new_mut() only hashes the input state. The Light System Program verifies that input hash exists in a state tree and creates the output hash in Step 4.

4

Light System Program CPI

Invoke the Light System Program to update the compressed account.

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.

System Accounts List

1

Verifies validity proofs, compressed account ownership checks, cpis the account compression program to update tree accounts

2

CPI Signer

- PDA to sign CPI calls from your program to Light System Program - Verified by Light System Program during CPI - Derived from your program ID

3

Registered Program PDA

- Access control to the Account Compression Program

4

- Logs compressed account state to Solana ledger. Only used in v1. - Indexers parse transaction logs to reconstruct compressed account state

5

Signs CPI calls from Light System Program to Account Compression Program

6

- Writes to state and address tree accounts - Client and the account compression program do not interact directly.

7

Invoking Program

Your program's ID, used by Light System Program to: - Derive the CPI Signer PDA - Verify the CPI Signer matches your program ID - Set the owner of created compressed accounts

8

Solana System Program to transfer lamports

Build the CPI instruction:

  • new_cpi() initializes the CPI instruction with the proof to prove that the account exists in the specified state tree - in the Instruction Data (Step 2).

  • with_light_account adds the LightAccount with the modified compressed account data - 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. Make sure you have your developer environment set up first, or simply run:

npm -g i @lightprotocol/[email protected]
light init testprogram

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::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,
}

Next Steps

Build a client for your program or learn how to close compressed accounts.

Last updated

Was this helpful?