How to Create Compressed Accounts
Guide to create compressed accounts in Solana programs with full code examples.
How to Create Compressed Accounts
Overview
Compressed accounts and addresses are created via CPI to the Light System Program.
Compressed and regular Solana accounts share the same functionality and are fully composable.
A compressed account has two identifiers: the account hash and its address (optional). In comparison, regular Solana accounts are identified by their address.
The account hash is not persistent and changes with every write to the account.
For Solana PDA like behavior your compressed account needs an address as persistent identifier. Fungible state like compressed token accounts do not need addresses.
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 creates compressed accounts. Here is the complete flow:


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-sdkprovides macros, wrappers and CPI interface to create and interact with compressed accounts.Add the serialization library (
borshfor native Rust, or useAnchorSerialize).
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");pub const ID: Pubkey = pubkey!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");CPISigner is the configuration struct for CPIs to the Light System Program.
CPI 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
#[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(
Clone,
Debug,
Default,
BorshSerialize,
BorshDeserialize,
LightDiscriminator
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}Define your compressed account struct and derive
the standard traits (
Clone,Debug,Default),borshorAnchorSerializeto serialize account data, andLightDiscriminatorto implements a unique type ID (8 bytes) to distinguish account types. The default compressed account layout enforces a discriminator in its own field, .
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 create_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
message: String,
) -> Result<()>Define an instruction data struct that will be deserialized from the instruction data:
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}Validity Proof
Define
proofto include the proof that the address does not exist yet in the specified address tree.Clients fetch a validity proof with
getValidityProof()from an RPC provider that supports ZK Compression (Helius, Triton, ...).
Specify Merkle trees to store address and account hash
Define
address_tree_info: PackedAddressTreeInfoto reference the address tree account used to derive the address in the next step.Define
output_state_tree_indexto reference the state tree account that stores the compressed account hash.
Initial account data
Define fields for your program logic. Clients pass the initial values.
This example includes the
messagefield to define the initial state of the account.
Derive Address
Derive the address as a persistent unique identifier for the compressed account.
let (address, address_seed) = derive_address(
&[b"message", ctx.accounts.signer.key().as_ref()],
&address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
&crate::ID,
);let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
&ID,
);Pass these parameters to derive_address():
&custom_seeds: Arbitrary byte slices that uniquely identify the account. This example usesb"message"and the signer's pubkey.&address_tree_pubkey: The pubkey of the address tree where the address will be created.Retrieved by calling
get_tree_pubkey()onaddress_tree_info, which unpacks the index from the accounts array.This parameter ensures an address is unique to an address tree. Different trees produce different addresses from identical seeds.
&program_id: Your program's ID.
The SDK returns:
address: The derived address for the compressed account.address_seed: Pass this to the Light System Program CPI in Step 8 to create the address.
Address Tree Check (optional)
Ensure global uniqueness of an address by verifying that the address tree pubkey matches the program's tree constant.
let address_tree = light_cpi_accounts.tree_pubkeys().unwrap()
[address_tree_info.address_merkle_tree_pubkey_index as usize];
if address_tree != light_sdk::constants::ADDRESS_TREE_V2 {
return Err(ProgramError::InvalidAccountData.into());
}Initialize Compressed Account
Initialize the compressed account struct with LightAccount::new_init().
new_init() creates a LightAccount instance similar to anchor Account and lets your program define the initial account data.
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();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;Pass these parameters to new_init():
&owner: The program's ID that owns the compressed account.Some(address): The derived address from Step 5. PassNonefor accounts without addresses.output_state_tree_index: References the state tree account that will store the updated account hash, defined in instruction data (Step 4)
The SDK creates:
A
LightAccountwrapper similar to Anchor'sAccount.new_init()lets the program set the initial data. This example sets:ownerto the signer's pubkeymessageto an arbitrary string
Light System Program CPI
Invoke the Light System Program to create the compressed account and its address.
The Light System Program
verifies the validity proof against the address tree's Merkle root,
inserts the address into the address tree, and
appends the new account hash to the state tree.
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let new_address_params = address_tree_info
.into_new_address_params_packed(address_seed);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.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
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_packed(address_seed);
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)?;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 the proof to prove that an address does not exist yet in the specified address tree - defined in the Instruction Data (Step 4).
with_light_accountadds theLightAccountwith the initial compressed account data to the CPI instruction - defined in Step 7.with_new_addressesadds theaddress_seedand metadata to the CPI instruction data - returned byderive_addressin Step 5.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, or simply run:
npm -g i @lightprotocol/[email protected]
light init testprogramFor help with debugging, see the Error Cheatsheet.
Next Steps
Build a client for your program or learn how to update compressed accounts.
Last updated
Was this helpful?