Native Programs In Detail
A native program on Solana is a Rust crate that depends on solana-program, targets SBF as a cdylib, and exposes one entrypoint that receives a program id, a slice of AccountInfos, and instruction data. There is no accounts struct macro, no automatic constraint generation, and no built-in IDL. You own the full path from bytes in to state changes out.
This page is the section map. Use it to place entrypoint dispatch, manual validation, (de)serialization, read/write, CPI account creation, logging, and framework trade-offs on one continuum before you follow the focused recipe pages.
Summary
Native Solana programs are ordinary SBF modules. The runtime calls process_instruction once per instruction, hands you only what the transaction declared, and expects Ok(()) or a ProgramError.
Without Anchor, three jobs land in your code. Parse instruction data into a stable enum or opcode layout. Validate every account the handler uses: signers, writability, owners, key equality, PDA seeds, and business relationships. Mutate program-owned account bytes under Solana borrow rules, including System Program CPI when a new account must exist.
Those jobs are also the main risk surface. A missing is_signer check, a wrong owner assumption, or a reinit path on an existing account is a critical bug. Frameworks encode the same rules in macros; native code makes them visible and easy to omit.
Choose native when you need explicit control, smaller binaries, lower baseline CU, unusual layouts, or audit-visible checks. Choose Anchor 0.32.1 (or a thin wrapper like Pinocchio) when velocity, IDL/client generation, and declarative constraints matter more. Many teams prototype in Anchor, then rewrite hot paths as native programs composed via CPI.
Foundations
What "native" means here
Native means you program against the solana-program API without Anchor's #[program] / #[account] layer. You still deploy a .so, pay fees in lamports, declare account metas from clients such as @solana/kit 7.0.0, and build with Agave 4.1.1 / Solana CLI 3.0.10 SBF tooling. Native is a thinner application layer, not a different chain.
The three arguments of every instruction
| Input | Role |
|---|---|
program_id | Your program public key; owner checks and PDA derivation |
accounts: &[AccountInfo] | Accounts the client listed, with runtime-checked signer/writable flags |
instruction_data: &[u8] | Opaque bytes you decode into an instruction enum or opcode |
The runtime does not re-derive intent from your Rust types. Wrong account in slot 2, short data, or garbage layout must fail closed in your handler.
AccountInfo is a capability, not a typed resource
AccountInfo exposes key, lamports, data, owner, executable, is_signer, and is_writable. Those flags reflect what the transaction declared and the runtime verified for signatures. They do not prove "this is the vault for this user" or "this is safe program state." That policy is yours: comparisons, PDA checks, and field inspection inside data.
Where frameworks sit on the same foundation
| Layer | What it adds | What it does not remove |
|---|---|---|
| Raw native | Full control of validation, layout, CPI | Correct checks and client schema discipline |
| Pinocchio / Steel-style | Less boilerplate, near-native CU | Security review of checks |
| Anchor 0.32.1 | Constraints, IDL, client ergonomics | Need to understand the underlying model |
Native literacy makes every option safer, even when you ship Anchor for most product surface.
Minimal crate shape
Typical layout: cdylib, entrypoint!(process_instruction), modules for instruction, processor, state, and error, plus solana-program and a serializer (often Borsh). One entrypoint per binary; multiple instruction kinds live behind one match on parsed data. Native Program Basics walks the skeleton with examples.
Mechanics
One instruction is a fixed pipeline: enter, decode, validate, act (read/write and optional CPI), log carefully, return.
End-to-end instruction pipeline
Client (@solana/kit / CLI / CPI caller)
| AccountMeta list + instruction_data
| sign (or invoke_signed from caller)
v
Runtime: load accounts, check sigs & writable metas
|
v
entrypoint! -> process_instruction(program_id, accounts, data)
|
| 1. deserialize instruction_data
| 2. next_account_info / fixed account order
| 3. validate signers, owners, PDAs, relationships
| 4. borrow/mutate data, or CPI System Program
| 5. optional msg! / sol_log_data
v
Ok(()) commits | Err(ProgramError) aborts instruction
Validate before side effects whenever possible. A failed instruction (or transaction) does not leave partial program effects from that failure path.
1. Entrypoint and dispatch
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResultDeserialize once at the top, then match to handlers. Pass program_id into PDA and owner paths. Map domain errors to stable ProgramError::Custom codes for clients.
2. Instruction data as an external contract
Instruction data is the wire format between off-chain builders and on-chain code. A common pattern is a Borsh enum (first byte variant index, then fields):
#[derive(BorshSerialize, BorshDeserialize)]
pub enum VaultInstruction {
Initialize { bump: u8 },
Deposit { amount: u64 },
}Do not reorder live variants. Cap unbounded strings and vecs. Add a version byte when you must break layout. Mirror the same encoding in TypeScript with @solana/kit 7.0.0 or a shared Codama/schema pipeline so clients cannot drift silently. See Instruction Data (de)Serialization.
3. Manual account validation
Native security is an explicit checklist on every path:
| Check | Pattern |
|---|---|
| Signer | account.is_signer |
| Writable | account.is_writable |
| Owner is your program | account.owner == program_id |
| Owner is System/Token/etc. | equality on expected program id |
| PDA identity | find_program_address + stored bump |
| Relationships | key equality; mint/authority fields in data |
| Sysvars | Sysvar::get() or known keys, not spoofable fakes |
Run cheap flag checks before CPI or heavy deserialize. Document account order; consume with next_account_info. For token accounts, the owner is the Token program; authority and mint live inside data.
if !payer.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if vault.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let (expected, bump) = Pubkey::find_program_address(
&[b"vault", payer.key.as_ref()],
program_id,
);
if expected != *vault.key || bump != stored_bump {
return Err(ProgramError::InvalidSeeds);
}Manual Account Validation covers substitution attacks and reusable checklists.
4. Reading and writing account data
Program-owned state is a byte buffer. Borrow, parse, mutate, write back within account length.
- Prefer
try_borrow_data/try_borrow_mut_data. - Drop borrows before CPI on the same account.
- Guard length; map serialize failures clearly (for example account too small).
- Version layouts; plan migrations.
- Only the owner program may write data (ownership set at create/assign).
let mut data = account.try_borrow_mut_data()?;
let mut state = State::deserialize(&mut &data[..])
.map_err(|_| ProgramError::InvalidAccountData)?;
state.count = state.count.checked_add(1).ok_or(ProgramError::InvalidArgument)?;
state.serialize(&mut &mut data[..])
.map_err(|_| ProgramError::AccountDataTooSmall)?;Detail: Reading & Writing Account Data.
5. Creating accounts via CPI
Programs create accounts by CPI to the System Program (create_account or related allocate/assign). The payer funds rent-exempt lamports; space sizes the buffer; owner becomes your program_id for program state.
PDAs have no private key. Use invoke_signed with exact seeds and bump:
let rent = Rent::get()?;
let lamports = rent.minimum_balance(State::LEN);
invoke_signed(
&system_instruction::create_account(
payer.key, pda.key, lamports, State::LEN as u64, program_id,
),
&[payer.clone(), pda.clone(), system_program.clone()],
&[&[b"vault", owner.key.as_ref(), &[bump]]],
)?;
// initialize data after createValidate the System Program id, empty/new-account assumptions, and seed match. Reinit and "account already exists" paths are classic exploit classes. Full flow: Creating Accounts via CPI.
6. Logging
msg! and related helpers write transaction logs for simulation, explorers, and indexers. Cost scales with volume and string size. Use structured breadcrumbs in development and sparse production logs. Prefer typed custom errors for expected failures. See Emitting Logs.
7. Clients and tests
Native still needs a client story: hand-written builders, a shared crate, or Codama/IDL tooling. Tests should assert validation failures (wrong owner, missing signer, bad PDA), not only happy paths. LiteSVM-style unit tests and local validators under Solana CLI 3.0.10 cover logic speed versus full RPC deploy loops.
Advanced
Trade-offs versus frameworks
| Factor | Native | Anchor 0.32.1 | Thin native (e.g. Pinocchio) |
|---|---|---|---|
| Velocity | Slowest | Fastest for apps | Middle |
| Safety rails | Manual checklists | Declarative constraints | Helpers, still explicit |
| CU / binary size | Best control | Higher baseline | Near-native |
| IDL / clients | Manual or external | Built-in | Varies |
| Audit visibility | Line-by-line | Macro expansion | Thin surface |
| Best fit | Hot paths, primitives, odd layouts | Most app programs | CU-sensitive native style |
Native is not automatically more secure. It is more transparent. Transparency only helps if the checklist is applied every time.
When native is the right product choice
Prefer native when at least two hold: instruction CU is business-critical; layout fights Anchor accounts; binary size is constrained; auditors demand fully explicit validation; or you ship a primitive other programs will CPI into.
Do not choose native only because "Anchor is slow" without profiling. Contention, RPC, and algorithms often dominate macro overhead.
Hybrid architectures
Common shape: Anchor for admin and low-frequency instructions; native for the tight loop (match step, game tick, settlement). Compose with CPI and shared layouts. Document which program id owns which state.
Security review lens
For every instruction ask: who must sign; which accounts are written and with which owner; can a different PDA, mint, or token account be substituted; is initialize re-callable on non-empty data; are seeds/bumps canonical; are borrows released before CPI; does instruction layout match published clients?
Operational discipline
Pin Agave, CLI, and solana-program like application deps. Document custom error codes for @solana/kit consumers. Treat layout changes as migrations with version gates.
Common Misconceptions
Misconception: Native skips security because the runtime already checked accounts.
The runtime checks signatures and writable metas for listed accounts. It does not know your vault seeds, mint bindings, or admin policy.
Misconception: is_writable means the account is safe to treat as your state.
Writable only means the transaction allowed mutation. Still verify owner, identity, and discriminators.
Misconception: Owner equals Token Program is enough for "the user's token account."
Authority, mint, and amount live in data. Validate fields (and often ATA derivation) for the economic action.
Misconception: Instruction enums can be reordered if Rust still compiles.
Variant indices are wire format. Reordering breaks old clients and can map old opcodes to new handlers.
Misconception: Creating a PDA is plain create_account without seeds.
PDA-authorized actions need invoke_signed with the seeds the runtime expects.
Misconception: Logging is free observability.
Logs cost CU and bloat meta. They are not a substitute for errors or off-chain metrics.
Misconception: Native is automatically safer than Anchor.
Both fail under wrong validation. Native moves the default from "macro might omit" to "author might omit."
Misconception: Native programs do not need an IDL or client schema.
Without a published encoding, builders diverge. Ship a schema, shared crate, or Codama pipeline.
FAQs
What is a native Solana program in one sentence?
A solana-program SBF crate with an entrypoint that manually parses instruction data, validates AccountInfos, and updates program-owned state (often with System Program CPI), without Anchor macros.
Do native programs use a different runtime than Anchor programs?
No. Both are SBF under the same Agave runtime. Anchor generates more glue; the chain sees a program id and bytecode either way.
What does the entrypoint receive?
program_id, a slice of AccountInfo for listed accounts, and a byte slice of instruction data.
Why is account order part of the API?
Clients build a parallel AccountMeta list. Handlers typically consume accounts in fixed order with next_account_info. Document that order or generate it from one source of truth.
What must I validate that the runtime does not?
Business identity: which PDA, mint, and authority; whether initialize already ran; relationships between accounts; and owner policy beyond signature presence.
How should I structure instruction data?
Prefer a versioned, documented layout (commonly a Borsh enum). Keep variants stable, bound dynamic sizes, and mirror the layout in clients.
How do I create a program-owned account from native code?
CPI system_instruction::create_account with rent-exempt lamports, desired space, and owner = program_id. Use invoke_signed when the new address is a PDA.
When do I need `invoke_signed`?
Whenever a PDA must authorize an action because no Ed25519 private key exists for that address.
How do I read and write state safely?
Check owner and length, borrow data, deserialize or use a safe view, mutate with checked arithmetic, serialize within capacity, and end borrows before CPI on the same account.
Are `msg!` logs required?
No. Use them for debugging and sparse production breadcrumbs. Prefer clear ProgramError values for expected failures.
How do clients talk to native programs?
Build transactions that list the correct accounts and serialize the same instruction bytes the program expects, using @solana/kit 7.0.0 or other SDKs.
Is native always lower CU than Anchor?
Native has a lower floor and more control, but real CU depends on checks, logs, serialization, and CPI. Measure hot instructions.
Can I mix Anchor and native?
Yes. Separate programs composed with CPI is common: Anchor for admin surfaces, native for performance-sensitive core logic.
What is the biggest security mistake in native programs?
Incomplete validation: missing signer, wrong owner, PDA substitution, or reinitialize-on-existing-account. Treat every instruction as hostile input.
Where should I go next in this section?
Start with Native Program Basics, then deepen validation, instruction data, CPI create, account I/O, and logs via Related below.
Related
- Native Program Basics - crate layout, entrypoint skeleton, and starter examples
- Manual Account Validation - signers, owners, PDAs, and substitution defenses
- Instruction Data (de)Serialization - Borsh enums, opcodes, and client parity
- Reading & Writing Account Data - borrows, serialize loops, and layout care
- Creating Accounts via CPI - System Program create and PDA
invoke_signed - Emitting Logs -
msg!, CU cost, and production logging discipline
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.