borsh & bytemuck
Borsh (Binary Object Representation Serializer for Hashing) is the default serialization format for Anchor account and instruction data on Agave 4.1.1. bytemuck enables zero-copy casting of plain-old-data account layouts when you control alignment and validation. Pick Borsh for flexibility; pick bytemuck for CU-sensitive hot paths.
Recipe
Quick-reference recipe card - copy-paste ready.
[dependencies]
borsh = "1.5"
bytemuck = { version = "1.16", features = ["derive"] }use borsh::{BorshDeserialize, BorshSerialize};
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Config {
pub bump: u8,
pub fee_bps: u16,
}When to reach for this:
- Encoding instruction arguments and account state in native or Anchor programs.
- Parsing account data in Rust clients and tests.
- Optimizing large fixed-layout accounts with zero-copy reads.
- Sharing schemas with off-chain indexers (discriminator + Borsh tail).
Working Example
use borsh::{BorshDeserialize, BorshSerialize};
use bytemuck::{Pod, Zeroable};
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct UserMeta {
pub authority: [u8; 32],
pub points: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct LedgerHeader {
pub magic: u32,
pub count: u32,
}
pub fn parse_ledger(data: &[u8]) -> Result<(&LedgerHeader, &[u8]), &'static str> {
if data.len() < std::mem::size_of::<LedgerHeader>() {
return Err("too short");
}
let (head_bytes, rest) = data.split_at(std::mem::size_of::<LedgerHeader>());
let header: &LedgerHeader = bytemuck::from_bytes(head_bytes);
if header.magic != 0xC0FFEE {
return Err("bad magic");
}
Ok((header, rest))
}What this demonstrates:
- Borsh derives serialize variable-ish structs for instructions.
Pod+Zeroablemark safe transparent layouts for bytemuck.- Manual validation (
magic, length) precedes zero-copy cast. - Tail bytes can hold Borsh-encoded entries after a fixed header.
Deep Dive
How It Works
- Borsh writes a canonical little-endian layout with length-prefixed slices.
- Anchor prepends 8-byte discriminators to account and instruction payloads.
- bytemuck reinterprets byte slices as
&Twithout copy when alignment rules hold. - Clients (Rust, TS, Python) must mirror the same schema for decoding.
Rust Crate Table
| Crate | Role | On-chain |
|---|---|---|
borsh | Serialize/deserialize structs | Yes |
borsh-derive | Derive macros | Yes |
bytemuck | Zero-copy casts | Yes (with care) |
bytemuck_derive | Pod/Zeroable derives | Yes |
anchor-lang | Integrated Borsh for accounts | Yes (Anchor path) |
TypeScript / NPM Table
| Package | Role | Notes |
|---|---|---|
borsh (npm) | Off-chain decoding in JS | Match Rust layout exactly |
@solana/kit codecs | Binary codecs for tx messages | 7.0.0 |
| Codama-generated codecs | IDL-driven layouts | Preferred for app clients |
Rust Notes
// Anchor account with discriminator
// space = 8 + UserMeta::serialized_size() // or INIT_SPACE in newer Anchor
// Never bytemuck-cast untrusted accounts without owner + length checksGotchas
- bytemuck on packed or bool-heavy structs - undefined behavior risk. Fix:
repr(C)+ explicit padding fields; test on-target. - Schema change without migration - deserializes garbage. Fix: version byte + migration instruction.
- Mixing Serde JSON with on-chain data - different format. Fix: Borsh (or custom) only inside programs.
- Unchecked
try_from_sliceon user data - panics in native code. Fix:try_from_slice->ProgramError::InvalidAccountData. - Endianness assumptions - Borsh is LE; bytemuck follows host for POD - stay LE-only types.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
bincode | Off-chain only tooling | On-chain (not standard) |
Manual Pack trait (legacy SPL style) | Interop with old layouts | New Anchor accounts |
serde JSON | APIs and configs | Account storage |
memmap style off-chain only | Analytics | Program runtime |
FAQs
Does Anchor always use Borsh?
Yes for account/instruction bodies unless you opt into custom serialization.
When is zero-copy worth it?
When profiling shows deserialize dominates CU on large fixed records.
bytemuck with Anchor Account<T>?
Possible for hybrid layouts; many teams use raw AccountInfo for zero-copy tails.
Vector in Borsh on-chain?
Allowed but costly - prefer fixed-capacity arrays in hot accounts.
Python decoding?
borsh-construct or AnchorPy IDL parsers - keep schemas in sync.
Discriminator collisions?
Anchor hashes account/instruction names - do not reuse names across types carelessly.
Endian in TS clients?
Use Codama/@solana/kit codecs to avoid hand-rolled mistakes.
Fuzzing layouts?
Fuzz Borsh decode with arbitrary bytes in LiteSVM 0.6.x tests.
Account resize?
Borsh variable data needs realloc instructions - plan space upfront.
Hashing with Borsh?
Borsh is deterministic - useful for PDA seed preimage design (with domain separation).
Related
- Borsh Serialization - deep Borsh guide
- Zero-Copy with bytemuck - optimization patterns
- Account Data Serialization - section overview
- anchor-lang & anchor-spl - integrated derives
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.