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.
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.
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.
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.
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.
Deserialize 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.
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):
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.
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.
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 create
Validate 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.
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.
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.
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.
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.
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?
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.
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.
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.