| title | Zero Copy |
|---|---|
| description | Learn how to use Anchor's zero-copy deserialization feature to handle large account data in Solana programs. |
Zero-copy deserialization allows programs to read and write account data directly from memory without copying or deserializing it. This is essential for handling large accounts efficiently on Solana.
Traditional account deserialization (Account<T>) copies data from the account
into a heap-allocated struct. This has limitations:
- Size Constraints: Stack (4KB) and heap (32KB) limits restrict account sizes
- Compute Cost: Deserialization consumes significant compute units
- Memory Overhead: Data is duplicated in memory
Zero-copy (AccountLoader<T>) instead:
- Direct Access: Casts raw account bytes to the struct type (no copying)
- Larger Accounts: Supports accounts up to 10MB (10,485,760 bytes)
- Lower Compute: ~90% reduction in CU usage for large accounts
- In-Place Updates: Modifies account data directly
| Account Size | Account<T> | AccountLoader<T> | Improvement |
|---|---|---|---|
| 1 KB | ~8,000 CU | ~1,500 CU | 81% faster |
| 10 KB | ~50,000 CU | ~5,000 CU | 90% faster |
| 100 KB | Too large | ~12,000 CU | Possible |
| 1 MB | Impossible | ~25,000 CU | Possible |
Use zero-copy for:
- Accounts larger than 1KB
- Arrays with many elements (orderbooks, event queues)
- High-frequency operations
- Compute-sensitive programs
Use regular Account<T> for:
- Small accounts (< 1KB)
- Dynamic data structures (Vec, String, HashMap)
- Frequently changing schemas
- Simple state that doesn't need optimization
Zero copy is a deserialization feature that allows programs to read account data directly from memory without copying it. This is particularly useful when working with large accounts.
To use zero-copy add the bytemuck crate to your dependencies. Add the
min_const_generics feature to allow working with arrays of any size in your
zero-copy types.
[dependencies]
bytemuck = { version = "1.20.0", features = ["min_const_generics"] }
anchor-lang = "1.0.2"To define an account type that uses zero-copy, annotate the struct with
#[account(zero_copy)].
// [!code highlight]
#[account(zero_copy)]
pub struct Data {
// 10240 bytes - 8 bytes account discriminator
pub data: [u8; 10232],
}The #[account(zero_copy)] attribute automatically implements several traits
required for zero-copy deserialization:
// [!code highlight:4]
#[derive(Copy, Clone)]
#[derive(bytemuck::Zeroable)]
#[derive(bytemuck::Pod)]
#[repr(C)]
struct Data {
// --snip--
}To deserialize a zero-copy account, use
AccountLoader<'info, T>,
where T is the zero-copy account type defined with the #[account(zero_copy)]
attribute.
For example:
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
// [!code word:AccountLoader]
// [!code highlight]
pub zero_copy_account: AccountLoader<'info, Data>,
}The init constraint can be used with the AccountLoader type to create a
zero-copy account.
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
// [!code word:init:1]
// [!code highlight:4]
init,
// 10240 bytes is max space to allocate with init constraint
space = 8 + 10232,
payer = payer,
)]
pub data_account: AccountLoader<'info, Data>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}When initializing a zero-copy account for the first time, use
load_init
to get a mutable reference to the account data. The load_init method also sets
the account discriminator.
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// [!code word:load_init]
// [!code highlight]
let account = &mut ctx.accounts.data_account.load_init()?;
account.data = [1; 10232];
Ok(())
}For accounts that require more than 10240 bytes, use the
zero
constraint instead of init. The zero constraint verifies the account has not
been initialized by checking that its discriminator has not been set.
#[derive(Accounts)]
pub struct Initialize<'info> {
// [!code word:zero]
// [!code highlight]
#[account(zero)]
pub data_account: AccountLoader<'info, Data>,
}With the zero constraint, you'll need to first create the account in a
separate instruction by directly calling the System Program. This allows you to
create accounts up to Solana's maximum account size of 10MB (10_485_760 bytes),
bypassing the CPI limitation.
Just as before, use load_init to get a mutable reference to the account data
and set the account discriminator. Since 8 bytes are reserved for the account
discriminator, the maximum data size is 10_485_752 bytes (10MB - 8 bytes).
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// [!code word:load_init]
// [!code highlight]
let account = &mut ctx.accounts.data_account.load_init()?;
account.data = [1; 10_485_752];
Ok(())
}Use
load_mut
when you need mutable access to update an existing zero-copy account:
#[derive(Accounts)]
pub struct Update<'info> {
// [!code highlight]
#[account(mut)]
pub data_account: AccountLoader<'info, Data>,
}pub fn update(ctx: Context<Update>) -> Result<()> {
// [!code word:load_mut]
// [!code highlight]
let account = &mut ctx.accounts.data_account.load_mut()?;
account.data = [2; 10232];
Ok(())
}Use
load
to only read the account data.
#[derive(Accounts)]
pub struct ReadOnly<'info> {
pub data_account: AccountLoader<'info, Data>,
}pub fn read_only(ctx: Context<ReadOnly>) -> Result<()> {
// [!code word:load]
// [!code highlight]
let account = &ctx.accounts.data_account.load()?;
msg!("First 10 bytes: {:?}", &account.data[..10]);
Ok(())
}For types used within zero-copy accounts, use #[zero_copy] (without account):
#[account(zero_copy)]
pub struct OrderBook {
pub market: Pubkey,
pub bids: [Order; 1000],
pub asks: [Order; 1000],
}
// [!code word:#[zero_copy]]
#[zero_copy]
pub struct Order {
pub trader: Pubkey,
pub price: u64,
pub quantity: u64,
}Zero-copy uses #[repr(packed)], making field references unsafe. Use the
#[accessor] attribute for safe getter/setter methods:
#[account(zero_copy)]
pub struct Config {
pub authority: Pubkey,
// [!code word:accessor]
#[accessor(Pubkey)]
pub secondary_authority: [u8; 32],
}
// Usage:
let config = &mut ctx.accounts.config.load_mut()?;
let secondary = config.get_secondary_authority();
config.set_secondary_authority(&new_authority);Zero-copy accounts work seamlessly with program-derived addresses:
#[derive(Accounts)]
pub struct CreatePdaAccount<'info> {
#[account(
init,
// [!code word:seeds]
seeds = [b"data", authority.key().as_ref()],
bump,
payer = authority,
space = 8 + std::mem::size_of::<Data>(),
)]
pub data_account: AccountLoader<'info, Data>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}Zero-copy types cannot derive AnchorSerialize/AnchorDeserialize. Use
separate types for instruction parameters:
// For zero-copy account
#[zero_copy]
pub struct Event {
pub from: Pubkey,
pub data: u64,
}
// For RPC/instruction parameters
// [!code word:AnchorSerialize]
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct EventParams {
pub from: Pubkey,
pub data: u64,
}
impl From<EventParams> for Event {
fn from(params: EventParams) -> Self {
Event {
from: params.from,
data: params.data,
}
}
}Always add 8 bytes for the account discriminator when calculating space:
// Wrong - missing discriminator
space = std::mem::size_of::<Data>()
// Correct - includes discriminator
// [!code highlight]
space = 8 + std::mem::size_of::<Data>()Zero-copy requires all fields to be Copy types:
#[account(zero_copy)]
pub struct InvalidData {
pub items: Vec<u64>, // Vec is not Copy
pub name: String, // String is not Copy
}
#[account(zero_copy)]
pub struct ValidData {
pub items: [u64; 100], // Fixed-size array
pub name: [u8; 32], // Fixed-size bytes
}Use load_init() for first-time initialization (sets discriminator):
// First initialization
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// [!code highlight]
let account = &mut ctx.accounts.data_account.load_init()?;
account.data = [1; 10232];
Ok(())
}
// Subsequent updates
pub fn update(ctx: Context<Update>) -> Result<()> {
// [!code highlight]
let account = &mut ctx.accounts.data_account.load_mut()?;
account.data = [2; 10232];
Ok(())
}Always validate array indices to prevent panics:
pub fn update_item(
ctx: Context<Update>,
index: u32,
value: u64
) -> Result<()> {
let account = &mut ctx.accounts.data_account.load_mut()?;
// [!code highlight:4]
require!(
(index as usize) < account.items.len(),
ErrorCode::IndexOutOfBounds
);
account.items[index as usize] = value;
Ok(())
}Store large sequences of events efficiently:
#[account(zero_copy)]
pub struct EventQueue {
pub head: u64,
pub count: u64,
pub events: [Event; 10000],
}
#[zero_copy]
pub struct Event {
pub timestamp: i64,
pub user: Pubkey,
pub event_type: u8,
pub data: [u8; 32],
}Used by: Trading protocols, audit logs, messaging systems
Efficient storage for trading pairs:
#[account(zero_copy)]
pub struct OrderBook {
pub market: Pubkey,
pub bid_count: u32,
pub ask_count: u32,
pub bids: [Order; 1000],
pub asks: [Order; 1000],
}
#[zero_copy]
pub struct Order {
pub trader: Pubkey,
pub price: u64,
pub size: u64,
pub timestamp: i64,
}Used by: DEXs (Serum, Mango), NFT marketplaces
The examples below demonstrate two approaches for initializing zero-copy accounts in Anchor:
- Using the
initconstraint to initialize the account in a single instruction - Using the
zeroconstraint to initialize an account with data greater than 10240 bytes
<Tabs items={["Program", "Client"]}>
use anchor_lang::prelude::*;
declare_id!("8B7XpDXjPWodpDUWDSzv4q9k73jB5WdNQXZxNBj1hqw1");
#[program]
pub mod zero_copy {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account = &mut ctx.accounts.data_account.load_init()?;
account.data = [1; 10232];
Ok(())
}
pub fn update(ctx: Context<Update>) -> Result<()> {
let account = &mut ctx.accounts.data_account.load_mut()?;
account.data = [2; 10232];
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
// 10240 bytes is max space to allocate with init constraint
space = 8 + 10232,
payer = payer,
)]
pub data_account: AccountLoader<'info, Data>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Update<'info> {
#[account(mut)]
pub data_account: AccountLoader<'info, Data>,
}
#[account(zero_copy)]
pub struct Data {
// 10240 bytes - 8 bytes account discriminator
pub data: [u8; 10232],
}import * as anchor from "@anchor-lang/core";
import { Program } from "@anchor-lang/core";
import { ZeroCopy } from "../target/types/zero_copy";
describe("zero-copy", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ZeroCopy as Program<ZeroCopy>;
const dataAccount = anchor.web3.Keypair.generate();
it("Is initialized!", async () => {
const tx = await program.methods
.initialize()
.accounts({
dataAccount: dataAccount.publicKey,
})
.signers([dataAccount])
.rpc();
console.log("Your transaction signature", tx);
const account = await program.account.data.fetch(dataAccount.publicKey);
console.log("Account", account);
});
it("Update!", async () => {
const tx = await program.methods
.update()
.accounts({
dataAccount: dataAccount.publicKey,
})
.rpc();
console.log("Your transaction signature", tx);
const account = await program.account.data.fetch(dataAccount.publicKey);
console.log("Account", account);
});
});When initializing an account that requires more than 10,240 bytes of space, you must split the initialization into two steps:
- Create the account in a separate instruction invoking the System Program
- Initialize the account data in your program instruction
Note that the maximum Solana account size is 10MB (10_485_760 bytes), 8 bytes are reserved for the account discriminator.
<Tabs items={["Program", "Client"]}>
use anchor_lang::prelude::*;
declare_id!("CZgWhy3FYPFgKE5v9atSGaiQzbSB7cM38ofwX1XxeCFH");
#[program]
pub mod zero_copy_two {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account = &mut ctx.accounts.data_account.load_init()?;
account.data = [1; 10_485_752];
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(zero)]
pub data_account: AccountLoader<'info, Data>,
}
#[account(zero_copy)]
pub struct Data {
// 10240 bytes - 8 bytes account discriminator
pub data: [u8; 10_485_752],
}import * as anchor from "@anchor-lang/core";
import { Program } from "@anchor-lang/core";
import { ZeroCopyTwo } from "../target/types/zero_copy_two";
describe("zero-copy-two", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ZeroCopyTwo as Program<ZeroCopyTwo>;
const dataAccount = anchor.web3.Keypair.generate();
it("Is initialized!", async () => {
const space = 10_485_760; // 10MB max account size
const lamports =
await program.provider.connection.getMinimumBalanceForRentExemption(
space,
);
// [!code highlight:7]
const createAccountInstruction = anchor.web3.SystemProgram.createAccount({
fromPubkey: program.provider.publicKey,
newAccountPubkey: dataAccount.publicKey,
space,
lamports,
programId: program.programId,
});
// [!code highlight:6]
const initializeInstruction = await program.methods
.initialize()
.accounts({
dataAccount: dataAccount.publicKey,
})
.instruction();
const transaction = new anchor.web3.Transaction().add(
createAccountInstruction,
initializeInstruction,
);
const tx = await program.provider.sendAndConfirm(transaction, [
dataAccount,
]);
console.log("Your transaction signature", tx);
const account = await program.account.data.fetch(dataAccount.publicKey);
console.log("Account", account);
});
});