The Entrypoint & Dispatch
The entrypoint is the single RPC surface of your program. Dispatch parses instruction_data, validates context, and routes to handlers with consistent error semantics.
Recipe
entrypoint!(process_instruction);
let ix = MyInstruction::try_from_slice(data)?;
match ix { /* ... */ }When to reach for this:
- Building native programs without Anchor dispatch.
- Adding a new instruction variant to an existing program.
- Auditing instruction parsing for ambiguity.
Working Example
use borsh::BorshDeserialize;
use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey};
entrypoint!(process_instruction);
#[derive(BorshDeserialize)]
pub enum MyInstruction { Init, Increment }
pub fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
if data.is_empty() { return Err(ProgramError::InvalidInstructionData); }
let ix = MyInstruction::deserialize(&mut &data[..]).map_err(|_| ProgramError::InvalidInstructionData)?;
match ix {
MyInstruction::Init => init(program_id, accounts),
MyInstruction::Increment => increment(program_id, accounts),
}
}What this demonstrates:
- Empty data guard before deserialize.
- Map parse errors to
InvalidInstructionData. - Handlers receive
program_idfor PDA derivation.
Deep Dive
How It Works
- Runtime passes raw bytes from the transaction instruction.
- Enum discriminant is the first serialized byte unless using manual tags.
- Each handler owns account iteration for its subset.
Dispatch Styles
| Style | Pros |
|---|---|
| Borsh enum | Idiomatic, compact |
| Manual opcode byte | C-like, explicit versioning |
Anchor #[program] | IDL + constraints auto |
Rust Notes
// Keep handlers thin; push shared checks to validators.Gotchas
- Ambiguous empty data - Treated as valid empty enum in some layouts.. Fix: Reject
data.is_empty()when no default instruction exists. - Variant reorder - Breaks clients silently.. Fix: Append variants only; version breaking changes.
- Fat match arms - Large handlers obscure audit trail.. Fix: One function per instruction.
- Missing program_id - Handlers cannot verify PDAs.. Fix: Thread
program_idthrough all paths. - Unchecked unwrap on slice - Bad length panics.. Fix: Use
deserializeResult mapping.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Anchor dispatch | IDL + account validation | Minimal binary |
| Pinocchio entrypoint | Lowest overhead | Macro ecosystem |
| Opcode tables | Firmware-style programs | Simple enums |
FAQs
Can I have multiple entrypoints?
No - one per program binary.
Where to parse accounts?
Inside each handler after shared header parse.
Version prefix byte?
Common pattern: first byte is schema version.
Share code with client?
Publish instruction crate for @solana/kit builders.
Test dispatch?
Serialize ix bytes in LiteSVM tests.
Unknown variant?
Borsh returns error - map to InvalidInstructionData.
Optional accounts?
Document and use sentinel or separate instructions.
Instruction data max size?
Bounded by transaction size (~1232 bytes).
entrypoint_no_alloc?
Advanced pattern to avoid allocator on entry.
Cross-program parse?
Never trust callee to parse your data.
Logging in dispatch?
OK for dev; avoid in production hot path.
Refactor match to table?
Use fn pointers only if binary size wins matter.
Related
- Native Program Basics - Structure
- Instruction Data (de)Serialization - Layouts
- CPI Basics - Calling others
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.