Data Accounts & Layout
Program state is a byte buffer in an account's data field. Anchor 0.32.1 uses Borsh serialization with an 8-byte discriminator prefix. Correct layout sizing prevents init failures and deserialization exploits.
Recipe
#[account]
#[derive(InitSpace)]
pub struct Market {
pub authority: Pubkey,
pub price: u64,
pub name: String, // max 32 in InitSpace via #[max_len(32)]
}
// space = 8 + Market::INIT_SPACEWhen to reach for this:
- Calculating
initspace for new account types - Versioning account schemas for upgrades
- Choosing Borsh vs zero-copy for CU and size trade-offs
- Parsing unknown accounts from RPC in clients
Working Example
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct UserStats {
pub authority: Pubkey,
#[max_len(16)]
pub nickname: String,
pub wins: u32,
pub losses: u32,
}
#[derive(Accounts)]
pub struct InitStats<'info> {
#[account(
init,
payer = authority,
space = 8 + UserStats::INIT_SPACE,
seeds = [b"stats", authority.key().as_ref()],
bump,
)]
pub stats: Account<'info, UserStats>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[program]
pub mod stats {
use super::*;
pub fn init(ctx: Context<InitStats>, nickname: String) -> Result<()> {
require!(nickname.len() <= 16, StatsError::NameTooLong);
ctx.accounts.stats.nickname = nickname;
ctx.accounts.stats.wins = 0;
ctx.accounts.stats.losses = 0;
Ok(())
}
}What this demonstrates:
InitSpacecomputes Borsh size includingmax_lenstrings- 8-byte discriminator is prepended by Anchor automatically
- Runtime validates string length against allocated space
Deep Dive
Layout Structure
[ 8 bytes discriminator ][ Borsh field 1 ][ field 2 ]...
Sizing Rules
- Fixed-size types:
Pubkey32,u648,bool1 String/Vec: 4-byte length + max content bytes- Enums: 1-byte variant index + largest variant size
Versioning
- Add
version: u8as first field after discriminator - Or use new account type with new discriminator
- Never shrink accounts without migration
Gotchas
- Omitting max_len on String/Vec - compile error or unbounded size. Fix: always set
#[max_len(N)]in Anchor 0.32.1. - Manual space math errors - off-by-one on enums. Fix: prefer
InitSpacederive. - Reordering fields - breaks existing accounts. Fix: only append new fields with versioning.
- Deserializing untrusted accounts - attacker crafts bytes. Fix: validate owner, discriminator, and field ranges.
- Huge accounts - rent and CU deserialize cost. Fix: zero-copy or split across accounts.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Borsh + Anchor | Default program state | Multi-MB datasets |
Zero-copy (AccountLoader) | Large structs, CU savings | Small simple structs |
bytemuck POD | Fixed-size packed layouts | Variable-length strings |
| External compression | NFT-scale data | Hot mutable fields |
FAQs
What is the 8-byte discriminator?
First 8 bytes of SHA256("account:StructName") in Anchor - identifies account type.
How do I compute space manually?
Sum Borsh sizes + 8. For String(max N): 4 + N bytes.
Can I change field order in an upgrade?
No - breaks deserialization of existing accounts.
What is InitSpace?
Anchor 0.32.1 derive macro calculating INIT_SPACE constant for space attribute.
How do clients decode account data?
Use Codama/@solana/kit clients from IDL, or manual buffer reads matching Borsh layout.
What is the max account size?
10 MB per account in Agave 4.1.1 - practical limits lower due to rent/CU.
Can I store JSON in accounts?
Wasteful and unbounded. Use typed binary layouts.
How do enums affect space?
Borsh uses 1-byte variant tag + size of largest variant payload.
What is zero-copy layout?
#[account(zero_copy)] maps struct directly to account bytes without deserialize alloc.
Should I pack fields manually?
Only for extreme CU optimization. Borsh default layout is fine for most programs.
How do I handle optional fields?
Use Option<T> in Borsh (1 byte tag + T) or sentinel values.
Can two structs share a discriminator?
No - each Anchor account type has a unique discriminator.
Related
- Rent & Rent-Exemption - space drives rent
- Program-Owned State - where layouts live
- Creating & Closing Accounts - realloc for growth
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.