Rust for Solana Programs
On-chain Solana code is Rust, but it is not laptop Rust with a different main. You compile to sBPF, run inside the SVM, and only touch chain state through accounts and syscalls.
Search across all documentation pages
On-chain Solana code is Rust, but it is not laptop Rust with a different main. You compile to sBPF, run inside the SVM, and only touch chain state through accounts and syscalls.
That subset - no_std, constrained allocation, a fixed entrypoint, Borsh (or zero-copy) layouts, checked math, typed errors, and compute/size budgets - is the language of every native, Anchor, and lightweight program on Agave 4.1.1.
no_std-friendly Rust subset: one entrypoint, byte-level instruction and account data (usually Borsh), safe arithmetic, fail-fast errors, and hard limits on compute units and binary size.unwrap, unbounded Vec growth, std deps, silent overflow, heavy logging) either fail to compile for cargo build-sbf or pass tests then break under CU, rent, and audit pressure on mainnet-beta.Solana program source is Rust 1.91.1 (this site's pin), but the target is not a host triple.
cargo build-sbf (via Agave platform tools / Anchor build) cross-compiles to sBPF (Solana Bytecode Format). Validators load the .so and the SVM executes it under Sealevel locks and compute metering.
There is no process main. The loader invokes a single entrypoint the crate registers (native entrypoint!, or Anchor's generated dispatcher). That function receives program_id, a slice of AccountInfo, and instruction_data bytes. Instruction variants, account layouts, PDAs, and CPIs all build on that triple.
The environment is no_std: no filesystem, sockets, threads, or full standard library. Optional alloc gives Vec / String / Box, but heap use costs compute. Production code prefers fixed-size accounts, stack-friendly parsing, and avoiding format! on hot paths.
Durable state does not live in the program binary. App records sit in accounts the program owns (or other programs own, mutated only via CPI). On-chain Rust structs are schemas over account bytes.
Borsh is the de facto encoding for instructions and most account structs (Anchor 0.32.1 default). Large fixed layouts often use bytemuck zero-copy instead. Money and counters use u64 (or wider fixed integers) with checked ops. Failures return ProgramError (or mapped custom codes); prefer ? over unwrap. Execution is metered in compute units, and binary size affects deploy cost. "It compiles" is not "it fits CU and size budgets."
Native crates depend on solana-program (or Pinocchio). Anchor projects use anchor-lang = "0.32.1" for entrypoint generation and account validation.
Build flow: compile to sBPF ELF with platform tools, deploy with Solana CLI 3.0.10 / Anchor, store bytecode in ProgramData under the upgradeable loader, then on each instruction the SVM maps accounts, calls the entrypoint, meters CU, and commits or rolls back.
Host unit tests (#[cfg(test)]) run on the workstation; integration tests use LiteSVM 0.6.x, Surfpool 0.12.0, or a local validator.
Native style:
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// decode instruction_data, validate accounts, mutate owned state
Ok(())
}Inside the handler you usually:
next_account_info or index checksOk(()) or Err(...)Anchor wraps this in #[program] modules and 8-byte discriminators. The runtime model is unchanged: one entrypoint, declared accounts, byte data.
On-chain you get core, selected no_std crates, solana_program syscalls (log, CPI, sysvars), optional alloc, and serializers such as Borsh / bytemuck that build for sBPF.
You do not get full std, async runtimes, HTTP clients, or filesystem access. If cargo build-sbf fails on a dependency, audit cargo tree for transitive std and swap in program-safe crates.
Instruction data and account bodies are bytes. Borsh is deterministic and little-endian: fixed integers, 1-byte bool, 32-byte Pubkey, u32-prefixed Vec/String, and enum discriminants.
Size accounts at creation so serialize cannot write past account.data.len(). Schema changes need versioning or migrations; reordering enum variants silently changes tags. Zero-copy (Pod + Zeroable + #[repr(C)]) reinterprets large fixed accounts without a full copy when CU matters.
Token amounts, fees in basis points, share math, and reward indexes should use:
let next = balance
.checked_add(amount)
.ok_or(ProgramError::InvalidArgument)?;Prefer checked_* when overflow is an error, saturating_* when a cap is intentional, and explicit bounds checks for business rules (max supply, max bps). Do not rely on debug-only overflow panics; treat wraparound as a design failure.
ProgramResult is Result<(), ProgramError>. Map domain failures to custom codes (native From impls, Anchor #[error_code], or similar) so TypeScript clients with @solana/kit 7.0.0 can branch on error numbers instead of log scraping alone.
Use msg! sparingly for diagnostics. Logs cost CU and are not a substitute for structured errors on expected failure paths (unauthorized, slippage, expired).
CU draws from a transaction-level budget (on modern Solana including Agave 4.1.1, plan around a default near 1,400,000 CU unless Compute Budget instructions change the limit within protocol caps).
Common CU sinks:
msg! in tight loopsBinary size sinks:
Discipline means early returns, bounded iteration, lean deps, and profiling with simulation before mainnet-beta traffic.
| Approach | Rust experience | CU / size | When it fits |
|---|---|---|---|
| Anchor 0.32.1 | Macros, accounts, IDL | Higher baseline | Product velocity, typed clients |
Native solana-program | Explicit entrypoint and checks | Lower ceiling if careful | Learning, custom loaders of control |
| Pinocchio / thin stacks | Systems-oriented | Often leanest | Hot paths, infrastructure programs |
| Hybrid | Anchor app layer + thin core | Split by CPI | Mature systems with measured hotspots |
All three still obey no_std constraints, Borsh-or-bytes layouts, checked math, and CU metering. Framework choice changes macros and validation ergonomics, not the SVM rules.
| Host Rust habit | On-chain substitute |
|---|---|
main + CLI args | Entrypoint + instruction bytes + account list |
| Files / DB rows | Program-owned accounts with fixed or versioned layouts |
anyhow + stack traces | Compact ProgramError codes + selective logs |
Unbounded Vec growth | Fixed capacity, account-backed storage, max counts |
| Threads / async | Single-threaded instruction; parallelization is Sealevel across txs |
println! | msg! (metered) |
| Floating money | Integer lamports / token base units + checked ops |
Use versioned account headers, discriminators (manual or Anchor's 8-byte tags), PDA seeds as part of the API, and CPI helpers that still require declared accounts. Keep host-only tests behind cfg so they do not bloat the sBPF binary.
Unit-test pure math and packing on the host. Run full instructions under LiteSVM 0.6.x or Surfpool 0.12.0. Measure binary size after build and computeUnitsConsumed from simulation (Kit RPC meta is enough for client-side checks).
no_std with optional alloc; most std and async stacks will not build for sBPF.unwrap is fine if clients are honest." Clients are adversarial; panics waste CU and produce poorer client UX than typed errors.msg! can push paths over the CU budget; prefer errors and off-chain indexing for high-volume events.repr(C)/Pod rules; variable-length fields often favor Borsh.sBPF bytecode (ELF .so) executed by the SVM on Agave validators. You build with cargo build-sbf or Anchor's build, not a normal host target alone.
The BPF/SBF loader calls the registered entrypoint for each instruction. Frameworks generate or wrap that entrypoint for you.
no_std removes the standard library. alloc optionally restores heap types (Vec, String) via the program allocator, still without OS I/O or threads. Prefer fixed layouts when possible.
Yes. Anchor 0.32.1 emits Rust that targets the same runtime. Macros reduce boilerplate for accounts, errors, and IDL; they do not change Sealevel rules.
It is deterministic, compact, and shared across native programs, Anchor defaults, and many client coders. Cross-language stability matters more than using whatever serde format you like on the host.
When accounts are large, fixed-size, and hot enough that full deserialize/serialize dominates CU. Keep alignment and padding rules under review.
Yes. Addition, multiplication for fees, and share math can still overflow u64. Checked ops turn that into a clean instruction failure.
Map custom error codes in the program and decode them off-chain (IDL/error tables with Anchor, or documented codes for native). Use @solana/kit 7.0.0 transaction meta for logs and err fields.
Remove hot-path logs, bound loops, avoid cloning large byte buffers, deserialize only needed fields, and simulate with realistic account sizes.
It must build under cargo build-sbf, preferably as no_std. Check transitive deps for std-only features and prefer crates maintained for Solana programs.
Prefer integer fixed-point for balances, fees, and shares. Avoid floats for anything financial.
Sealevel pages cover locks, parallelism, and metering. This section covers the Rust subset you write inside that runtime.
Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, LiteSVM 0.6.x.
This overview, then Rust for Solana Basics, then no_std, Borsh, arithmetic, errors, and CU pages as you build.
It thins deps and entrypoint style. You still reason about accounts, bytes, errors, and CU the same way.
?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.
Reviewed by Chris St. John·Last updated Jul 15, 2026