Program development
Learn how to use Anchor's program development features.
Account types
Account space and realloc
Custom errors
Emit events
Zero copy
Account types
Reference for the Anchor account types used in the #[derive(Accounts)] struct.
Anchor’s account types are the type wrappers used in fields of a #[derive(Accounts)] struct. Each type performs a specific runtime check on the account before the instruction handler runs, complementing the account constraints declared in the #[account(...)] attribute.
For background on how account types fit into instruction validation, see account validation.
Account<'info, T>#
Account<'info, T> wraps an AccountInfo, deserializes its data into the type T, and verifies that the account is owned by the program declared in declare_id!():
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub account: Account<'info, CustomAccountType>,}
#[account]pub struct CustomAccountType { data: u64,}Signer<'info>#
Signer<'info> verifies that the account signed the transaction. Use it when the handler needs proof of authorization but does not read or write the account’s data:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub signer: Signer<'info>,}SystemAccount<'info>#
SystemAccount<'info> verifies that the account is owned by the System Program:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub account: SystemAccount<'info>,}Program<'info, T>#
Program<'info, T> verifies that the account is the executable program identified by T. Use it for any program invoked through CPI:
use anchor_spl::token::Token;
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>,}Interface<'info, T>#
Interface<'info, T> verifies that the account is one of a known set of programs sharing an interface. The canonical use is accepting either the SPL Token program or Token-2022:
// Token program or Token2022 programuse anchor_spl::token_interface::TokenInterface;
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub program: Interface<'info, TokenInterface>,}InterfaceAccount<'info, T>#
InterfaceAccount<'info, T> deserializes account data into T and verifies ownership against any program in the interface set. Use it for token accounts and mints that may be owned by either SPL Token or Token-2022:
// Token program or Token2022 program Mint/TokenAccountuse anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub mint: InterfaceAccount<'info, Mint>, pub token: InterfaceAccount<'info, TokenAccount>, pub program: Interface<'info, TokenInterface>,}Sysvar<'info, T>#
Sysvar<'info, T> verifies that the account is the Solana sysvar identified by T and deserializes its data:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub rent: Sysvar<'info, Rent>, pub clock: Sysvar<'info, Clock>,}UncheckedAccount<'info>#
UncheckedAccount<'info> is an explicit wrapper around AccountInfo that performs no checks. Every field of this type requires a /// CHECK: doc comment explaining why the lack of validation is safe:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { /// CHECK: No checks are performed pub account: UncheckedAccount<'info>,}AccountInfo<'info>#
AccountInfo<'info> is the raw Solana account type and performs no checks. Prefer UncheckedAccount instead, which makes the lack of validation explicit and requires a /// CHECK: comment:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { /// CHECK: AccountInfo is an unchecked account pub unchecked_account: AccountInfo<'info>,}Box<Account<'info, T>>#
Box moves an account to the heap, freeing space on the BPF stack. Use it when the wrapped type is large enough to risk a stack overflow during instruction execution:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub account: Box<Account<'info, AccountType>>,}Option<Account<'info, T>>#
Wrapping an account type in Option marks it as optional. Clients pass None to omit the account, and the handler accesses it as Option<T>:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub account: Option<Account<'info, AccountType>>,}AccountLoader<'info, T>#
AccountLoader<'info, T> provides on-demand zero-copy deserialization for accounts marked with #[account(zero_copy)]. The handler calls load() or load_mut() to obtain a reference into the account’s bytes without copying:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub account: AccountLoader<'info, ZeroCopyAccountType>,}
#[account(zero_copy)]pub struct ZeroCopyAccountType { data: u64,}Migration<'info, From, To>#
Migration<'info, From, To> handles schema migrations between two account types. The account must deserialize as From on entry and is migrated to To before serialization on exit. Pair it with the realloc constraint to resize the account when the new schema is larger.
The type performs three checks: ownership matches From::owner(), the account is initialized (not a zero-lamport System Program account), and its data deserializes as From:
use anchor_lang::prelude::*;
#[account]pub struct AccountV1 { pub data: u64,}
#[account]pub struct AccountV2 { pub data: u64, pub new_field: u64,}
#[derive(Accounts)]pub struct MigrateAccount<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( mut, realloc = 8 + AccountV2::INIT_SPACE, realloc::payer = payer, realloc::zero = false )] pub my_account: Migration<'info, AccountV1, AccountV2>, pub system_program: Program<'info, System>,}Usage patterns#
Call migrate() for an explicit one-shot migration. Old fields remain accessible through Deref until the call:
let old_data = ctx.accounts.my_account.data;
ctx.accounts.my_account.migrate(AccountV2 { data: old_data, new_field: 42,})?;Call into_inner() for an idempotent migration that returns a reference to the new data, migrating only if needed:
let migrated = ctx.accounts.my_account.into_inner(AccountV2 { data: ctx.accounts.my_account.data, new_field: ctx.accounts.my_account.data * 2,});msg!("New field: {}", migrated.new_field);Call into_inner_mut() for the same idempotent migration with a mutable reference:
let migrated = ctx.accounts.my_account.into_inner_mut(AccountV2 { data: ctx.accounts.my_account.data, new_field: 0,});migrated.new_field = 42;Account space and realloc
Reference for calculating account data size requirements by Rust type.
The space constraint on an init account must reserve enough bytes for the account’s serialized data plus an 8-byte Anchor discriminator. The table below lists the byte size of each Rust type Anchor supports.
This applies to accounts that use Borsh serialization. Accounts annotated with #[account(zero_copy)] use repr(C) with a pointer cast, so the C layout rules apply instead.
Type chart#
Manual calculation#
Add up the field sizes and store the total as a MAX_SIZE constant on the struct, then reference it in the space constraint along with 8 bytes for the discriminator:
#[account]pub struct MyData { pub val: u16, pub state: GameState,
pub players: Vec<Pubkey>,}
impl MyData { pub const MAX_SIZE: usize = 2 + (1 + 32) + (4 + 10 * 32);}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]pub enum GameState { Active, Tie, Won { winner: Pubkey },}
#[derive(Accounts)]pub struct InitializeMyData<'info> { #[account(init, payer = signer, space = 8 + MyData::MAX_SIZE)] pub acc: Account<'info, MyData>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>,}#[derive(InitSpace)]#
The InitSpace derive macro generates an INIT_SPACE associated constant that holds the struct’s serialized size. The struct does not need #[account] for the derive to work:
#[account]#[derive(InitSpace)]pub struct ExampleAccount { pub data: u64, #[max_len(50)] pub string_one: String, #[max_len(10, 5)] pub nested: Vec<Vec<u8>>,}
#[derive(Accounts)]pub struct Initialize<'info> { #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, #[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)] pub data: Account<'info, ExampleAccount>,}InitSpace follows three rules:
- Always add 8 bytes to
spacefor the discriminator. The macro does not include it. #[max_len]declares the maximum number of elements, not bytes. OnVec<u32>,#[max_len(10)]reserves 10 elements × 4 bytes plus a 4-byte length prefix for 44 bytes total.- Nested vectors take one length per dimension.
#[max_len(10, 5)]onVec<Vec<u8>>allows up to 10 outer elements, each holding up to 5 inner.
Custom errors
Learn how to implement custom error handling in Anchor programs.
All instruction handlers in Anchor programs return a custom Result<T> type that allows handling successful execution with Ok(T) and error cases with Err(Error):
pub fn custom_instruction(ctx: Context<CustomInstruction>) -> Result<()> { // --snip-- Ok(())}The Result<T> type in Anchor programs is a type alias that wraps the standard Rust Result<T, E>. T represents the successful return type, while E is Anchor’s custom Error type:
pub type Result<T> = std::result::Result<T, error::Error>;Anchor error#
When an error occurs in an Anchor program, it returns a custom Error type defined as:
#[derive(Debug, PartialEq, Eq)]pub enum Error { AnchorError(Box<AnchorError>), ProgramError(Box<ProgramErrorWithOrigin>),}The Error type has two variants.
ProgramErrorWithOrigin wraps a standard Solana ProgramError from the solana_program crate:
#[derive(Debug)]pub struct ProgramErrorWithOrigin { pub program_error: ProgramError, pub error_origin: Option<ErrorOrigin>, pub compared_values: Option<ComparedValues>,}AnchorError represents errors defined by the Anchor framework:
#[derive(Debug)]pub struct AnchorError { pub error_name: String, pub error_code_number: u32, pub error_msg: String, pub error_origin: Option<ErrorOrigin>, pub compared_values: Option<ComparedValues>,}An AnchorError falls into one of two categories. Internal Anchor errors are built into the framework and defined in the ErrorCode enum. Custom program errors are program-specific errors that developers define for their own validation logic.
The error_code_number from an AnchorError follows this numbering scheme:
Usage#
Anchor provides a convenient way to define custom errors through the #[error_code] attribute. When an enum is annotated with #[error_code], Anchor assigns each variant an error code starting from 6000, generates the error-handling boilerplate, and enables custom error messages via the #[msg(...)] attribute:
#[error_code]pub enum MyError { #[msg("My custom error message")] MyCustomError, #[msg("My second custom error message")] MySecondCustomError,}err!()#
To throw an error, use the err!() macro. Under the hood, err!() uses the error!() macro to construct an AnchorError:
#[program]mod hello_anchor { use super::*; pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> { if data.data >= 100 { return err!(MyError::DataTooLarge); } ctx.accounts.my_account.set_inner(data); Ok(()) }}
#[error_code]pub enum MyError { #[msg("MyAccount may only hold data below 100")] DataTooLarge}require!()#
The require!() macro provides a more concise way to handle error conditions. It combines a condition check with returning an error if the condition is false:
#[program]mod hello_anchor { use super::*; pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> { require!(data.data < 100, MyError::DataTooLarge); ctx.accounts.my_account.set_inner(data); Ok(()) }}
#[error_code]pub enum MyError { #[msg("MyAccount may only hold data below 100")] DataTooLarge}Anchor provides several require macros for different validation needs:
require!()ensures a condition is true, otherwise returns the given error.require_eq!()ensures two non-pubkey values are equal.require_neq!()ensures two non-pubkey values are not equal.require_keys_eq!()ensures two pubkeys are equal.require_keys_neq!()ensures two pubkeys are not equal.require_gt!()ensures the first non-pubkey value is greater than the second.require_gte!()ensures the first non-pubkey value is greater than or equal to the second.
Example#
The program below validates that an input amount falls within an acceptable range. It defines two custom error variants with messages and uses require!() to enforce the bounds:
use anchor_lang::prelude::*;
declare_id!("9oECKMeeyf1fWNPKzyrB2x1AbLjHDFjs139kEyFwBpoV");
#[program]pub mod custom_error { use super::*;
pub fn validate_amount(_ctx: Context<ValidateAmount>, amount: u64) -> Result<()> { require!(amount >= 10, CustomError::AmountTooSmall); require!(amount <= 100, CustomError::AmountTooLarge);
msg!("Amount validated successfully: {}", amount); Ok(()) }}
#[derive(Accounts)]pub struct ValidateAmount {}
#[error_code]pub enum CustomError { #[msg("Amount must be greater than or equal to 10")] AmountTooSmall, #[msg("Amount must be less than or equal to 100")] AmountTooLarge,}import * as anchor from '@anchor-lang/core'import { Program } from '@anchor-lang/core'import { CustomError } from '../target/types/custom_error'import assert from 'assert'
describe('custom-error', () => { anchor.setProvider(anchor.AnchorProvider.env()) const program = anchor.workspace.CustomError as Program<CustomError>
it('Successfully validates amount within range', async () => { const tx = await program.methods.validateAmount(new anchor.BN(50)).rpc()
console.log('Transaction signature:', tx) })
it('Fails with amount too small', async () => { try { await program.methods.validateAmount(new anchor.BN(5)).rpc()
assert.fail('Expected an error to be thrown') } catch (error) { assert.strictEqual(error.error.errorCode.code, 'AmountTooSmall') assert.strictEqual(error.error.errorMessage, 'Amount must be greater than or equal to 10') } })
it('Fails with amount too large', async () => { try { await program.methods.validateAmount(new anchor.BN(150)).rpc()
assert.fail('Expected an error to be thrown') } catch (error) { assert.strictEqual(error.error.errorCode.code, 'AmountTooLarge') assert.strictEqual(error.error.errorMessage, 'Amount must be less than or equal to 100') } })})When a program error occurs, Anchor’s TypeScript client SDK returns a detailed error response containing information about the error:
{ errorLogs: [ 'Program log: AnchorError thrown in programs/custom-error/src/lib.rs:11. Error Code: AmountTooLarge. Error Number: 6001. Error Message: Amount must be less than or equal to 100.' ], logs: [ 'Program 9oECKMeeyf1fWNPKzyrB2x1AbLjHDFjs139kEyFwBpoV invoke [1]', 'Program log: Instruction: ValidateAmount', 'Program log: AnchorError thrown in programs/custom-error/src/lib.rs:11. Error Code: AmountTooLarge. Error Number: 6001. Error Message: Amount must be less than or equal to 100.', 'Program 9oECKMeeyf1fWNPKzyrB2x1AbLjHDFjs139kEyFwBpoV consumed 2153 of 200000 compute units', 'Program 9oECKMeeyf1fWNPKzyrB2x1AbLjHDFjs139kEyFwBpoV failed: custom program error: 0x1771' ], error: { errorCode: { code: 'AmountTooLarge', number: 6001 }, errorMessage: 'Amount must be less than or equal to 100', comparedValues: undefined, origin: { file: 'programs/custom-error/src/lib.rs', line: 11 } }, _programErrorStack: ProgramErrorStack { stack: [ [PublicKey [PublicKey(9oECKMeeyf1fWNPKzyrB2x1AbLjHDFjs139kEyFwBpoV)]] ] }}For a more comprehensive example, see the errors test program in the Anchor repository.
Emit events
Learn how to emit events in Anchor programs using emit!() and emit_cpi!() macros.
Anchor exposes two macros for emitting events from a program. The emit!() macro writes events to program logs, which is the simpler path but can be truncated by some data providers. The emit_cpi!() macro emits events through a Cross Program Invocation (CPI) by including the event data in the inner instruction’s data, which is less likely to be truncated.
Remark (Why two macros exist)
emit_cpi!() was introduced as an alternative to program logs, which can sometimes be truncated by data providers. CPI instruction data is less likely to be truncated, but this approach incurs additional compute costs from the Cross Program Invocation.
Tip (Geyser gRPC for production)
emit!()#
The emit!() macro emits events through program logs. When called, it:
- Uses the
sol_log_data()syscall to write the data to program logs. - Encodes the event data as a base64 string prefixed with
Program data:.
To receive emitted events in a client application, use the addEventListener() method on the Program instance. It parses and decodes event data from the program logs automatically.
Example usage:
use anchor_lang::prelude::*;
declare_id!("8T7MsCZyzxboviPJg5Rc7d8iqEcDReYR2pkQKrmbg7dy");
#[program]pub mod event { use super::*;
pub fn emit_event(_ctx: Context<EmitEvent>, input: String) -> Result<()> { emit!(CustomEvent { message: input }); Ok(()) }}
#[derive(Accounts)]pub struct EmitEvent {}
#[event]pub struct CustomEvent { pub message: String,}import * as anchor from '@anchor-lang/core'import { Program } from '@anchor-lang/core'import { Event } from '../target/types/event'
describe('event', () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env())
const program = anchor.workspace.Event as Program<Event>
it('Emits custom event', async () => { // Set up listener before sending transaction const listenerId = program.addEventListener('customEvent', (event) => { // Do something with the event data console.log('Event Data:', event) })
// Message to be emitted in the event const message = 'Hello, Solana!' // Send transaction await program.methods.emitEvent(message).rpc()
// Remove listener await program.removeEventListener(listenerId) })})The program logs below show the emitted event data, base64-encoded as Zb1eU3aiYdwOAAAASGVsbG8sIFNvbGFuYSE=:
Log Messages: Program 8T7MsCZyzxboviPJg5Rc7d8iqEcDReYR2pkQKrmbg7dy invoke [1] Program log: Instruction: EmitEvent Program data: Zb1eU3aiYdwOAAAASGVsbG8sIFNvbGFuYSE= Program 8T7MsCZyzxboviPJg5Rc7d8iqEcDReYR2pkQKrmbg7dy consumed 1012 of 200000 compute units Program 8T7MsCZyzxboviPJg5Rc7d8iqEcDReYR2pkQKrmbg7dy successDanger (RPC providers may truncate program logs)
Ensure the RPC provider you use does not truncate program logs from the transaction data. A silent truncation will drop emitted events on the floor without surfacing an error to the listener.
emit_cpi!()#
The emit_cpi!() macro emits events through Cross Program Invocations to the program itself. The event data is encoded and included in the CPI’s instruction data instead of the program logs.
To emit events through CPIs, enable the event-cpi feature in Cargo.toml:
[dependencies]anchor-lang = { version = "1.0.1", features = ["event-cpi"] }Example usage:
use anchor_lang::prelude::*;
declare_id!("2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1");
#[program]pub mod event_cpi { use super::*;
pub fn emit_event(ctx: Context<EmitEvent>, input: String) -> Result<()> { emit_cpi!(CustomEvent { message: input }); Ok(()) }}
#[event_cpi]#[derive(Accounts)]pub struct EmitEvent {}
#[event]pub struct CustomEvent { pub message: String,}import * as anchor from '@anchor-lang/core'import { Program } from '@anchor-lang/core'import { EventCpi } from '../target/types/event_cpi'
describe('event-cpi', () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()) const program = anchor.workspace.EventCpi as Program<EventCpi>
it('Emits custom event', async () => { const message = 'Hello, Solana!' const transactionSignature = await program.methods.emitEvent(message).rpc()
// Wait for the transaction to be confirmed await program.provider.connection.confirmTransaction(transactionSignature, 'confirmed')
// Fetch the transaction data const transactionData = await program.provider.connection.getTransaction(transactionSignature, { commitment: 'confirmed', })
// Decode the event data from the CPI instruction data const eventIx = transactionData.meta.innerInstructions[0].instructions[0] const rawData = anchor.utils.bytes.bs58.decode(eventIx.data) const base64Data = anchor.utils.bytes.base64.encode(rawData.subarray(8)) const event = program.coder.events.decode(base64Data) console.log(event) })})The #[event_cpi] attribute must be added to the #[derive(Accounts)] struct for any instruction that emits events with emit_cpi!(). The attribute injects the additional accounts required for the self-CPI:
#[event_cpi]#[derive(Accounts)]pub struct RequiredAccounts { // --snip--}To get the emitted event data in a client application, fetch the transaction by signature and parse the event data from the CPI instruction data:
// 1. Fetch the full transaction data using the transaction signatureconst transactionData = await program.provider.connection.getTransaction(transactionSignature, { commitment: 'confirmed',})
// 2. Extract the CPI (inner instruction) that contains the event dataconst eventIx = transactionData.meta.innerInstructions[0].instructions[0]
// 3. Decode the event dataconst rawData = anchor.utils.bytes.bs58.decode(eventIx.data)const base64Data = anchor.utils.bytes.base64.encode(rawData.subarray(8))const event = program.coder.events.decode(base64Data)console.log(event)The transaction below shows how event data appears in the transaction details. With emit_cpi!(), the event data is encoded into the data field of an inner instruction (CPI). In the example, the encoded event data is "data": "6AJcBqZP8afBKheoif1oA6UAiLAcqYr2RaR33pFnEY1taQp" inside the innerInstructions array.
{ "blockTime": 1735854530, "meta": { "computeUnitsConsumed": 13018, "err": null, "fee": 5000, "innerInstructions": [ { "index": 0, "instructions": [ { "accounts": [1], "data": "6AJcBqZP8afBKheoif1oA6UAiLAcqYr2RaR33pFnEY1taQp", "programIdIndex": 2, "stackHeight": 2 } ] } ], "loadedAddresses": { "readonly": [], "writable": [] }, "logMessages": [ "Program 2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1 invoke [1]", "Program log: Instruction: EmitEvent", "Program 2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1 invoke [2]", "Program 2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1 consumed 5000 of 192103 compute units", "Program 2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1 success", "Program 2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1 consumed 13018 of 200000 compute units", "Program 2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1 success" ], "postBalances": [499999999999995000, 0, 1141440], "postTokenBalances": [], "preBalances": [500000000000000000, 0, 1141440], "preTokenBalances": [], "rewards": [], "status": { "Ok": null } }, "slot": 3, "transaction": { "message": { "header": { "numReadonlySignedAccounts": 0, "numReadonlyUnsignedAccounts": 2, "numRequiredSignatures": 1 }, "accountKeys": [ "4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1", "2brZf9PQqEvv17xtbj5WNhZJULgVZuLZT6FgH1Cqpro2", "2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1" ], "recentBlockhash": "2QtnU35RXTo7uuQEVARYJgWYRYtbqUxWQkK8WywUnVdY", "instructions": [ { "accounts": [1, 2], "data": "3XZZ984toC4WXCLkxBsLimpEGgH75TKXRJnk", "programIdIndex": 2, "stackHeight": null } ], "indexToProgramIds": {} }, "signatures": [ "3gFbKahSSbitRSos4MH3cqeVv2FiTNaLCuWaLPo6R98FEbHnTshoYxopGcx74nFLqt1pbZK9i8dnr4NFXayrMndZ" ] }}Note
Event data emitted through CPIs cannot currently be subscribed to directly. To access this data, fetch the full transaction and manually decode the event from the CPI’s instruction data.
Zero copy
Use Anchor's zero-copy deserialization to handle large account data in Solana programs.
Overview#
Zero-copy deserialization lets programs read and write account data directly from memory without copying or deserializing it. This approach is the practical way to handle large accounts on Solana.
Why use zero-copy?#
Traditional account deserialization with Account<T> copies the account data into a heap-allocated struct. The copied struct is constrained by Solana’s 4KB stack and 32KB heap limits, the deserialization step consumes significant compute units, and the same data ends up duplicated in memory.
AccountLoader<T> casts the raw account bytes directly to the struct type without copying, supports accounts up to 10MB (10,485,760 bytes), reduces compute usage by roughly 90% for large accounts, and modifies account data in place.
Intuition (Why the cast is sound)
The struct annotated with #[account(zero_copy)] is #[repr(C)] and Pod, so its in-memory layout matches the raw bytes byte-for-byte. The “deserialize” step that Account<T> performs is therefore unnecessary work; the bytes already are the struct, and AccountLoader<T> exposes that fact through a checked pointer cast.
Performance comparison#
When to use zero-copy#
Reach for zero-copy when accounts exceed roughly 1KB, when fields are large fixed-size arrays such as orderbooks or event queues, when instructions run at high frequency, or when the program is compute-sensitive.
A regular Account<T> is the better choice for small accounts under 1KB, dynamic data structures (Vec, String, HashMap), schemas that change frequently, and simple state that does not require optimization.
Usage#
Add the bytemuck crate to the program’s dependencies. The min_const_generics feature lets zero-copy types contain arrays of any size:
[dependencies]bytemuck = { version = "1.20.0", features = ["min_const_generics"] }anchor-lang = "1.0.1"Define a zero-copy account#
To define an account type that uses zero-copy, annotate the struct with #[account(zero_copy)]:
#[account(zero_copy)]pub struct Data { // 10240 bytes - 8 bytes account discriminator pub data: [u8; 10232],}The #[account(zero_copy)] attribute automatically implements the traits required for zero-copy deserialization:
#[derive(Copy, Clone)]#[derive(bytemuck::Zeroable)]#[derive(bytemuck::Pod)]#[repr(C)]struct Data { // --snip--}Use AccountLoader<'info, T> for zero-copy accounts#
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:
#[derive(Accounts)]pub struct InstructionAccounts<'info> { pub zero_copy_account: AccountLoader<'info, Data>,}Initialize a zero-copy account#
The init constraint can be used with AccountLoader to create a zero-copy account:
#[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>,}Note
The init constraint is limited to allocating a maximum of 10240 bytes due to CPI limitations. Under the hood, init invokes the System Program to create the account.
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<()> { 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> { #[account(zero)] pub data_account: AccountLoader<'info, Data>,}With the zero constraint, the account must first be created in a separate instruction by directly calling the System Program. This bypasses the CPI allocation limit and supports accounts up to Solana’s maximum size of 10MB (10_485_760 bytes).
Just as before, load_init() returns a mutable reference and sets the account discriminator. Since 8 bytes are reserved for the discriminator, the maximum data size is 10_485_752 bytes (10MB - 8 bytes):
pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let account = &mut ctx.accounts.data_account.load_init()?; account.data = [1; 10_485_752]; Ok(())}Update a zero-copy account#
Use load_mut() to obtain mutable access to an existing zero-copy account:
#[derive(Accounts)]pub struct Update<'info> { #[account(mut)] pub data_account: AccountLoader<'info, Data>,}pub fn update(ctx: Context<Update>) -> Result<()> { let account = &mut ctx.accounts.data_account.load_mut()?; account.data = [2; 10232]; Ok(())}Read a zero-copy account#
Use load() for read-only access to the account data:
#[derive(Accounts)]pub struct ReadOnly<'info> { pub data_account: AccountLoader<'info, Data>,}pub fn read_only(ctx: Context<ReadOnly>) -> Result<()> { let account = &ctx.accounts.data_account.load()?; msg!("First 10 bytes: {:?}", &account.data[..10]); Ok(())}Common patterns#
Nested zero-copy types#
For types used inside 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],}
#[zero_copy]pub struct Order { pub trader: Pubkey, pub price: u64, pub quantity: u64,}Accessor methods for byte arrays#
Zero-copy uses #[repr(packed)], which makes direct field references unsafe. The #[accessor] attribute generates safe getter and setter methods:
#[account(zero_copy)]pub struct Config { pub authority: Pubkey, #[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 with PDAs#
Zero-copy accounts work seamlessly with program-derived addresses:
#[derive(Accounts)]pub struct CreatePdaAccount<'info> { #[account( init, 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>,}Separate types for RPC parameters#
Zero-copy types cannot derive AnchorSerialize or AnchorDeserialize. Define separate types for instruction parameters:
// For zero-copy account#[zero_copy]pub struct Event { pub from: Pubkey, pub data: u64,}
#[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, } }}Common pitfalls#
Forgetting the account discriminator#
Danger (Always reserve 8 bytes for the discriminator)
Skipping the 8-byte discriminator from a space calculation produces an account that the Anchor runtime cannot validate, since the discriminator check expects the first 8 bytes to be the account-type marker. The bug usually surfaces only on first read, where the runtime returns a deserialization error that looks unrelated to the allocation site.
// Wrong - missing discriminatorspace = std::mem::size_of::<Data>()
// Correct - includes discriminatorspace = 8 + std::mem::size_of::<Data>()Using dynamic types#
Danger (Zero-copy fields must be Copy)
Zero-copy requires every field to be a Copy type. Vec<T>, String, and other heap-backed collections store a pointer rather than the data itself, so reading them through a zero-copy cast points at memory the program does not own. The compiler rejects these fields outright; if a derive error mentions Pod or Copy, the fix is to switch to a fixed-size array.
#[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}Choosing between load_init() and load_mut()#
Call load_init() for first-time initialization (which sets the discriminator), and load_mut() for subsequent updates:
// First initializationpub fn initialize(ctx: Context<Initialize>) -> Result<()> { let account = &mut ctx.accounts.data_account.load_init()?; account.data = [1; 10232]; Ok(())}
// Subsequent updatespub fn update(ctx: Context<Update>) -> Result<()> { let account = &mut ctx.accounts.data_account.load_mut()?; account.data = [2; 10232]; Ok(())}Not validating array indices#
Validate array indices before use to prevent panics:
pub fn update_item(ctx: Context<Update>, index: u32, value: u64) -> Result<()> { let account = &mut ctx.accounts.data_account.load_mut()?;
require!( (index as usize) < account.items.len(), ErrorCode::IndexOutOfBounds );
account.items[index as usize] = value; Ok(())}Real-world use cases#
Event queue pattern#
Example (Event queue)
Store large sequences of events efficiently in a single zero-copy account rather than spreading them across many small accounts. Trading protocols, audit logs, and messaging systems all use this pattern.
#[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],}Order book pattern#
Example (Order book)
Efficient storage for trading pairs. DEXs such as Serum and Mango, along with NFT marketplaces, use this layout to keep bid and ask sides resident in a single account.
#[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,}Examples#
The examples below show two approaches for initializing zero-copy accounts:
- Using the
initconstraint to initialize the account in a single instruction. - Using the
zeroconstraint to initialize an account with data greater than 10240 bytes.
Zero copy#
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) })})Initialize a large account#
When initializing an account that requires more than 10,240 bytes of space, split the initialization into two steps:
- Create the account in a separate instruction invoking the System Program.
- Initialize the account data in the program instruction.
The maximum Solana account size is 10MB (10_485_760 bytes), with 8 bytes reserved for the account discriminator.
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)
const createAccountInstruction = anchor.web3.SystemProgram.createAccount({ fromPubkey: program.provider.publicKey, newAccountPubkey: dataAccount.publicKey, space, lamports, programId: program.programId, })
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) })})