Advanced Anchor Key Points
Anchor's defaults (Borsh accounts, fixed #[derive(Accounts)] lists, InitSpace) get most programs shipping. Production systems hit edges: large books, variable hop routes, schema growth, foreign callees, and hard CU or deploy-size ceilings.
This page is the conceptual map for Advanced Anchor - zero-copy, remaining_accounts, realloc, custom account types, feature flags and cfg, CU and binary size, and interop with native or Pinocchio programs on Agave 4.1.1.
Summary
- Advanced Anchor keeps the same runtime contract (declared accounts, ownership, rent, CU) but opts out of conveniences that burn compute, freeze layout, or hide variable metas, replacing them with loaders, tails, resizes, and CPI bridges.
- Insight: Wrong zero-copy layout, unvalidated remaining accounts, sloppy realloc, or feature-gated IDL drift turns framework speed into corruption, rent griefing, or client breakage under mainnet load.
- Key Concepts:
#[account(zero_copy)]/AccountLoader,ctx.remaining_accounts,realloc+realloc::payer/realloc::zero, manual account traits, Cargo features /#[cfg], compute units and.sosize, native / Pinocchio CPI (invoke,declare_program!). - When to Use This Model: Large fixed state, routers and multi-oracle paths, growing schemas, mixed-framework DeFi, CU or deploy size pressure, or dev-only instructions that must never ship.
- Limitations/Trade-offs: More power means more manual validation; zero-copy and realloc fight schema churn; remaining accounts leave the IDL; cfg can fork IDL and layout; interop trusts the callee's rules, not Anchor constraints on the other side.
- Related Topics: Zero-Copy Accounts, realloc in Anchor, remaining_accounts, Custom Account Types, CU & Size Optimization, Feature Flags & cfg, Interacting with Native/Pinocchio Programs.
Foundations
Anchor sits on Solana's account model: programs are stateless executables; durable state lives in program-owned data accounts; every account an instruction needs must appear in the transaction with correct signer and writable flags.
The usual path is: name accounts in #[derive(Accounts)] with constraints; decode bodies with Borsh (Account<'info, T>); fix space at init with space = / InitSpace; expose a stable IDL for clients (@solana/kit 7.0.0, Codama, Anchor TS).
Advanced Anchor starts when that path is wrong for the workload:
| Pressure | Default that breaks down | Advanced response |
|---|---|---|
| Large fixed tables (books, ticks, bitmaps) | Full Borsh deserialize/serialize each ix | Zero-copy + AccountLoader |
| Variable account count (routes, hooks, oracles) | Fixed named fields only | ctx.remaining_accounts |
| Schema or collection growth after init | Fixed SPACE forever | realloc (or migrate to a new account) |
| Foreign or exotic layouts | Account<T> + #[account] only | Custom types or UncheckedAccount + manual checks |
| Dev diagnostics / cluster-only ix | Same binary everywhere | Features + cfg (or separate crates) |
| CU ceiling or deploy size | Verbose logs, re-find PDAs, fat deps | Measure, then cut hot-path cost and binary weight |
| Compose with non-Anchor programs | Anchor-only CPI helpers | Raw invoke / declare_program! interop |
Nothing here changes Sealevel locks or ownership: you still cannot invent accounts at runtime, write foreign data without CPI into the owner, or spend unbounded CU. You only change how Anchor maps those rules into types and macros.
Mechanics & Interactions
Zero-copy accounts
For small configs, Account<'info, T> is fine: Anchor checks owner and discriminator, Borsh-decodes, and writes back on drop when mutable. When T is large and mostly fixed (order books, tick arrays, slabs), full parse/serialize burns CU with size. Zero-copy keeps bytes in place and mutates through a typed view.
In Anchor 0.32.1 the usual shape is:
#[account(zero_copy)]
#[repr(C)]
pub struct Market {
pub head: u64,
pub bids: [u64; 1024],
}
#[derive(Accounts)]
pub struct Touch<'info> {
#[account(mut)]
pub market: AccountLoader<'info, Market>,
}
pub fn touch(ctx: Context<Touch>) -> Result<()> {
let mut market = ctx.accounts.market.load_mut()?;
market.head = market.head.saturating_add(1);
Ok(())
}Prefer #[repr(C)], fixed-size fields, and bytemuck/Pod-friendly layout (no naive String/Vec). Discriminators still apply. Use AccountLoader, not Account, and do not mix Borsh and zero-copy on the same live account. Plan capacity up front; mark mut for load_mut. Profile first: on tiny accounts zero-copy often costs more than it saves.
remaining_accounts
The typed Accounts struct is a prefix of the instruction's account list. Anything after those named fields is ctx.remaining_accounts: raw AccountInfo with no automatic owner, signer, or discriminator checks.
Typical uses: multi-hop routers, optional oracle sets or hooks, and forwarding open-ended metas into CPI.
pub fn route(ctx: Context<Route>, hops: u8) -> Result<()> {
require!(
ctx.remaining_accounts.len() == hops as usize * 2,
RouterError::BadMetas
);
require!(ctx.remaining_accounts.len() <= MAX_REMAINING, RouterError::TooMany);
for (i, acc) in ctx.remaining_accounts.iter().enumerate() {
if i % 2 == 0 {
require_keys_eq!(*acc.owner, token_program::ID, RouterError::BadOwner);
}
}
Ok(())
}Remaining accounts are not named in the IDL; document the protocol for clients and indexers, or ship typed instructions per common route. Clients append metas in agreed order with correct is_signer / is_writable. Cap length. Validate every entry before read, write, or CPI.
realloc in Anchor
Init freezes data_len and the rent-exempt minimum for that size. When schemas or collections grow, realloc changes the buffer under program ownership inside an instruction:
#[account(
mut,
realloc = 8 + Config::INIT_SPACE + extra,
realloc::payer = payer,
realloc::zero = false,
)]
pub config: Account<'info, Config>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,realloc = N: targetdata_len(include the 8-byte discriminator when usingINIT_SPACEpatterns).realloc::payer: funds the rent delta on growth; must be mutable.realloc::zero: whether new bytes are zeroed. Zero when new bytes become new fields; stale bytes are a bug class if you leave them dirty.- System program must be present for rent transfers.
Prefer forward growth and migrate instructions for large redesigns. Do not realloc on every hot path: grow in chunks or preallocate when the ceiling is known. Zero-copy plus frequent realloc is a poor mix.
Custom account types
Most programs should stay on Account<'info, T>, InterfaceAccount / token wrappers, AccountLoader, and constrained UncheckedAccount / SystemAccount / Signer.
Custom account types implement Anchor traits by hand (AccountDeserialize, ownership checks) when foreign layouts or specialized validation do not map to derives. They raise audit surface: lifetimes, try_deserialize_unchecked misuse, missing owner checks. Prefer wrapping built-ins or a foreign IDL, with LiteSVM fixtures if you must go custom.
Feature flags and cfg
Cargo features and #[cfg(feature = "...")] compile code in or out:
[features]
default = []
debug-log = []
devnet-only = []#[cfg(feature = "debug-log")]
msg!("debug path");Good uses: optional diagnostics, heavier test asserts, cluster constants from build scripts. Dangerous uses: splitting #[program] so mainnet IDL differs from clients; gating account field layouts across builds of the same program ID; leaving devnet-only instructions in release. Prefer empty defaults, CI that forbids dangerous mainnet feature combos, runtime admin gates, or a separate dev crate when instructions must never exist on mainnet.
CU and binary size
Compute units meter work inside a finite transaction budget (plan around a default on the order of ~1,400,000 CU unless Compute Budget instructions raise the limit within protocol caps). Deserialization, constraints, logs, PDA derivation, and CPI all draw from the same pool.
High-leverage habits: store canonical bumps; use zero-copy only where Borsh dominates on large state; trim msg! and prefer events; fewer accounts and shallower CPI; right-size client CU limits from simulation with headroom; release profiles and splitting rare admin surfaces can shrink the .so. Never drop validation to save CU. Measure first; when framework tax still dominates, consider a Pinocchio or native hot path via CPI.
Interop with native and Pinocchio
Anchor can call non-Anchor programs in the same transaction:
declare_program!when the callee publishes a compatible IDL (typed CPI helpers at compile time).- Raw
invoke/invoke_signedwith hand-builtInstructiondata andAccountMetalists when there is no IDL or the layout is hand-packed.
invoke(
&Instruction {
program_id: native_program.key(),
accounts: metas,
data: ix_data, // discriminator + args per callee spec
},
account_infos,
)?;Anchor constraints apply only to your Accounts struct. Validate program IDs, account order, writable/signer flags, and post-CPI state. Clients with @solana/kit 7.0.0 must encode the same discriminators and metas the callee expects.
Advanced Considerations & Applications
Treat these tools as a stack of escape hatches, not a checklist to enable all at once.
| Tool | Wins | Costs | Fits |
|---|---|---|---|
| Zero-copy | Low CU on large fixed state | Rigid layout, harder migrations | Books, slabs, tick arrays |
| remaining_accounts | Variable composition | No IDL names; full manual validation | Routers, hooks, multi-oracle |
| realloc | Pay rent when needed | Payer, zeroing, CU, max caps | Additive growth, bounded lists |
| New account + migrate | Clean V2 layout | UX and dual-read period | Breaking redesigns |
| Custom account types | Foreign layout fit | Audit and trait burden | Last-resort interop |
| cfg features | Smaller or safer release binaries | IDL/layout fork risk | Debug logs, never-ship dev ix |
| CU/size tuning | Landing under budget, deploy fit | Complexity if unmeasured | Profiled hot paths |
| Native/Pinocchio CPI | Specialized performance or shared protocol | Byte-level contract risk | Mixed DeFi stacks |
Migration: prefer append-only Borsh fields and a version byte when possible; realloc for bounded growth; dedicated migrate when layout breaks; dual-read during rollout.
Clients and indexers: every hatch that leaves the IDL (remaining accounts, manual discriminators, cfg-gated instructions) needs a published protocol: constants, route docs, Codama/kit codecs, and migration events.
Security review: remaining-account authentication, realloc rent griefing and max size, zero-copy alignment and type confusion, unchecked custom deserialize, feature-flag attack surface, CPI program spoofing.
On an Agave 4.1.1-aligned stack (CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, LiteSVM 0.6.x, Surfpool 0.12.0), exercise growth, adversarial remaining accounts, and CU profiles before mainnet traffic does.
Common Misconceptions
- "Zero-copy is always faster, so every account should use it." Small accounts rarely justify Pod rigidity. Profile Borsh cost first.
- "remaining_accounts are already validated by Anchor." Only the typed struct gets constraint macros. The tail is raw until you check it.
- "realloc zeros and initializes new fields for me." You choose zeroing policy and must init semantic fields.
- "I can shrink and grow casually like a heap." Account size is a rent and owner operation; design forward growth and migrate for large changes.
- "Custom account types are a normal style choice." They are a last resort. Prefer built-ins plus constraints.
- "Feature flags let me keep one codebase with different mainnet IDLs." Clients and verifiable builds need a single release product.
- "Cutting constraints is a valid CU optimization." Security regressions are not free CU. Optimize layout, logs, bumps, and CPI instead.
- "If Anchor CPI succeeds, the native program did what I meant." Success means the callee returned
Ok; still verify amounts and authorities. - "IDL completeness is optional if the program is correct." Remaining accounts and manual CPI bytes are invisible to naive clients.
- "Binary size does not matter once deploy succeeds." Larger programs raise deploy cost and operational friction.
FAQs
What counts as "Advanced Anchor" versus everyday Anchor?
Everyday Anchor: Borsh Account<T>, fixed account structs, init with known space, standard CPI helpers. Advanced Anchor: zero-copy loaders, remaining account tails, realloc growth, hand-rolled account types, compile-time feature splits, aggressive CU/size work, and native/Pinocchio interop.
When should I use zero-copy accounts?
When account data is large and mostly fixed, and profiling shows Borsh deserialize/serialize dominating CU. Use #[account(zero_copy)] with #[repr(C)] and AccountLoader. See Zero-Copy Accounts.
Does zero-copy still use a discriminator?
Yes. Type tags still protect against reading one layout as another. Init and load paths must write and check the expected prefix.
What is ctx.remaining_accounts?
Accounts listed on the instruction after the fields of your #[derive(Accounts)] struct. They are not named in the IDL and receive no automatic constraint checks. See remaining_accounts.
When do I need realloc in Anchor?
When an existing program-owned account must grow for new fields or more entries (or carefully shrink). Use realloc, realloc::payer, realloc::zero, and the system program. See realloc in Anchor.
Is realloc a substitute for account versioning?
No. Realloc changes byte length; versioning decides how you parse old and new layouts. Combine a stable discriminator, a version field, dual-read, and migrate or realloc as needed.
Should I implement custom account types?
Only when built-ins cannot express the layout or validation. Prefer Account, InterfaceAccount, AccountLoader, and constraints. See Custom Account Types.
How do feature flags interact with the IDL?
Anything that changes instructions or account shapes between builds produces different IDLs. Keep a single mainnet release profile. See Feature Flags & cfg.
How do I reduce CU without weakening security?
Store bumps, cut verbose logs, prefer zero-copy only on large hot state, minimize accounts and CPI depth, and set client CU limits from simulation. Never remove ownership or signer checks for CU. See CU & Size Optimization.
How do I measure CU and binary size?
Use compute unit consumption in LiteSVM/Surfpool tests and RPC simulation on devnet; inspect release .so size after anchor build with production features.
Can an Anchor program CPI into a Pinocchio or native program?
Yes. Prefer declare_program! when an IDL exists; otherwise build instruction data and metas and invoke / invoke_signed. Validate the program ID and every account. See Interacting with Native/Pinocchio Programs.
What does declare_program! need?
A callee IDL JSON available at compile time so Anchor can generate typed CPI helpers. Without an IDL, use raw instruction construction.
How should @solana/kit clients treat these advanced patterns?
Generate or hand-write codecs that match discriminators, fixed accounts, remaining-account protocols, and any non-Anchor CPI bytes. Target @solana/kit 7.0.0 aligned with this stack.
When should I leave Anchor for Pinocchio on a hot path?
When measured CU or binary size still fails product goals after Anchor-side optimizations, and you can isolate a small surface with a clear CPI boundary.
Related
- Zero-Copy Accounts -
AccountLoader, Pod layout, large state - realloc in Anchor - grow space, payer, zeroing policy
- remaining_accounts - variable account tails and validation
- Custom Account Types - manual account traits and foreign layouts
- CU & Size Optimization - compute units and deploy size
- Feature Flags & cfg - conditional compilation and IDL stability
- Interacting with Native/Pinocchio Programs - mixed-framework CPI
- Advanced Anchor Best Practices - checklist for production edges
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.