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.
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.
Summary
- Solana programs are sBPF executables written in a
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. - Insight: Desktop habits (
unwrap, unboundedVecgrowth,stddeps, silent overflow, heavy logging) either fail to compile forcargo build-sbfor pass tests then break under CU, rent, and audit pressure on mainnet-beta. - Key Concepts: sBPF / SVM, no_std / alloc, entrypoint, AccountInfo, Borsh, checked arithmetic, ProgramError, compute units (CU), program size.
- When to Use This Model: Writing or reviewing any on-chain crate (native, Anchor 0.32.1, or Pinocchio); onboarding Rust engineers who have not shipped BPF/SBF code; deciding what to learn next in this section.
- Limitations/Trade-offs: You trade host ergonomics for determinism, parallel scheduling, and meterable execution. Features like async runtimes, threads, and unconstrained heap patterns do not apply.
- Related Topics: Rust for Solana Basics, no_std & the Program Environment, Borsh Serialization, Safe Arithmetic, Error Handling On-Chain, Program Size & CU Discipline.
Foundations
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."
Mechanics & Interactions
From Rust source to sBPF
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.
Entrypoint and dispatch
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:
- Decode a Borsh instruction enum (or raw tag + payload)
- Walk accounts with
next_account_infoor index checks - Verify owners, signers, PDAs, and data lengths
- Apply checked state updates
- Return
Ok(())orErr(...)
Anchor wraps this in #[program] modules and 8-byte discriminators. The runtime model is unchanged: one entrypoint, declared accounts, byte data.
no_std and what you can import
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.
Borsh as the wire format
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.
Safe arithmetic
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.
Errors clients can decode
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 and program size
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:
- Deserializing huge accounts every instruction
- Unbounded loops over client-supplied lists
- Verbose
msg!in tight loops - Deep CPI stacks without simulation
Binary size sinks:
- Heavy dependency graphs
- Monolithic "one crate does everything" programs
- Debug-only code left in release builds
Discipline means early returns, bounded iteration, lean deps, and profiling with simulation before mainnet-beta traffic.
Advanced Considerations & Applications
Choosing a Rust surface
| 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.
Mapping host Rust intuition to on-chain work
| 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 |
Patterns and testing
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).
Common Misconceptions
- "If I know Rust, Solana programs are just another crate." The target, entrypoint, account model, and metering change which crates and patterns are valid.
- "I can use the standard library like a backend service." On-chain is
no_stdwith optionalalloc; moststdand async stacks will not build for sBPF. - "Anchor removes the need to understand Borsh and accounts." Anchor generates code over the same bytes, owners, and discriminators; bad layouts still brick migrations.
- "Release builds make integer overflow safe automatically." Financial logic must use checked or saturating APIs deliberately.
- "
unwrapis fine if clients are honest." Clients are adversarial; panics waste CU and produce poorer client UX than typed errors. - "More logging always helps production." Excess
msg!can push paths over the CU budget; prefer errors and off-chain indexing for high-volume events. - "Larger dependencies only hurt compile time." They can inflate deploy size and pull more code into hot paths; audit deps as production risk.
- "Zero-copy is always better than Borsh." Zero-copy needs strict
repr(C)/Podrules; variable-length fields often favor Borsh. - "Program upgrades rewrite account data for free." Upgrades swap bytecode; account bytes stay until you migrate them.
FAQs
What does Rust compile to for Solana programs?
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.
Why is there no `main` function?
The BPF/SBF loader calls the registered entrypoint for each instruction. Frameworks generate or wrap that entrypoint for you.
What is the practical difference between no_std and "limited alloc"?
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.
Is Anchor still "real" on-chain Rust?
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.
Why is Borsh so common?
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 should I switch from Borsh to bytemuck?
When accounts are large, fixed-size, and hot enough that full deserialize/serialize dominates CU. Keep alignment and padding rules under review.
Do I need checked math if I use u64 token amounts only?
Yes. Addition, multiplication for fees, and share math can still overflow u64. Checked ops turn that into a clean instruction failure.
How should clients read program failures?
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.
What is a good first optimization pass for CU?
Remove hot-path logs, bound loops, avoid cloning large byte buffers, deserialize only needed fields, and simulate with realistic account sizes.
How do I know if a crate is safe to depend on?
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.
Can I use floating point on-chain?
Prefer integer fixed-point for balances, fees, and shares. Avoid floats for anything financial.
How does this section relate to Sealevel runtime docs?
Sealevel pages cover locks, parallelism, and metering. This section covers the Rust subset you write inside that runtime.
What versions should my team pin?
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.
Where do I start if I am new to on-chain Rust?
This overview, then Rust for Solana Basics, then no_std, Borsh, arithmetic, errors, and CU pages as you build.
Does Pinocchio change the model?
It thins deps and entrypoint style. You still reason about accounts, bytes, errors, and CU the same way.
Related
- Rust for Solana Basics - short examples of entrypoint, no_std, Borsh, and checked ops
- no_std & the Program Environment - what the sandbox allows and forbids
- Borsh Serialization - instruction and account encoding in depth
- Safe Arithmetic - checked and saturating patterns for balances and fees
- Error Handling On-Chain - ProgramError, custom codes, and
? - Program Size & CU Discipline - binary size and compute budgets
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.