Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Solana development skill covering @solana/kit v5, Anchor programs, LiteSVM testing, and security patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/programs/pinocchio.md
1---2title: Programs with Pinocchio3description: Build high-performance Solana programs with zero-copy techniques and minimal dependencies, without the solana-program overhead.4---56# Programs with Pinocchio78Pinocchio is a minimalist Rust crate for crafting Solana programs without the heavyweight `solana-program` crate. It delivers significant performance gains through zero-copy techniques and minimal dependencies.910## When to Use Pinocchio1112Use Pinocchio when you need:1314- **Compute efficiency potential**: Can reduce compute units and binary size versus higher-level frameworks, depending on instruction complexity and validation strategy15- **Minimal binary size**: Leaner code paths and smaller deployments16- **Zero external dependencies**: Only Solana SDK types required17- **Fine-grained control**: Direct memory access and byte-level operations18- **no_std environments**: Embedded or constrained contexts1920## Core Architecture2122### Program Structure Validation Checklist2324Before building/deploying, verify lib.rs contains all required components:2526- [ ] `entrypoint!(process_instruction)` macro27- [ ] `pub const ID: Address = Address::new_from_array([...])` with correct program ID28- [ ] `fn process_instruction(program_id: &Address, accounts: &[AccountView], data: &[u8]) -> ProgramResult`29- [ ] Instruction routing logic with proper discriminators30- [ ] `pub mod instructions; pub use instructions::*;`3132### Entrypoint Pattern3334```rust35use pinocchio::{36account::AccountView,37address::Address,38entrypoint,39error::ProgramError,40ProgramResult,41};4243entrypoint!(process_instruction);4445fn process_instruction(46_program_id: &Address,47accounts: &[AccountView],48instruction_data: &[u8],49) -> ProgramResult {50match instruction_data.split_first() {51Some((0, data)) => Deposit::try_from((data, accounts))?.process(),52Some((1, _)) => Withdraw::try_from(accounts)?.process(),53_ => Err(ProgramError::InvalidInstructionData)54}55}56```5758Single-byte discriminators support 255 instructions; use two bytes for up to 65,535 variants.5960### Panic Handler Configuration6162**For std environments (SBF builds):**6364```rust65entrypoint!(process_instruction);66// Remove nostd_panic_handler!() - std provides panic handling67```6869**For no_std environments:**7071```rust72#![no_std]73entrypoint!(process_instruction);74nostd_panic_handler!();75```7677**Critical**: Never include both - causes duplicate lang item error in SBF builds.7879### Program ID Declaration8081```rust82pub const ID: Address = Address::new_from_array([83// Your 32-byte program ID as bytes840xXX, 0xXX, ..., 0xXX,85]);86```8788// Note: Use `Address::new_from_array()` not `Address::new()`8990### Recommended Import Structure9192```rust93use pinocchio::{94account::AccountView,95address::Address,96entrypoint,97error::ProgramError,98ProgramResult,99};100// Add CPI imports only when needed:101// cpi::{invoke_signed, Seed, Signer},102// Add system program imports only when needed:103// pinocchio_system::instructions::Transfer,104```105106107## Utility Macros108109Define in `src/utils/macros.rs`:110111```rust112// Runtime length check — returns InvalidInstructionData113macro_rules! require_len {114($data:expr, $len:expr) => {115if $data.len() < $len {116return Err(ProgramError::InvalidInstructionData);117}118};119}120121// Runtime length check — returns InvalidAccountData122macro_rules! require_account_len {123($data:expr, $len:expr) => {124if $data.len() < $len {125return Err(ProgramError::InvalidAccountData);126}127};128}129130// Validates byte 0 matches expected discriminator131macro_rules! validate_discriminator {132($data:expr, $disc:expr) => {133if $data.is_empty() || $data[0] != $disc {134return Err(ProgramError::InvalidAccountData);135}136};137}138139// Compile-time: asserts struct size matches expected (catches padding bugs)140macro_rules! assert_no_padding {141($t:ty, $expected:expr) => {142const _: () = assert!(143core::mem::size_of::<$t>() == $expected,144"struct size mismatch — check for unexpected padding"145);146};147}148149assert_no_padding!(Config, 65); // usage example150```151152## Traits System153154Define these traits once in a shared module (e.g. `src/traits/`) and implement them on all state/instruction types.155156### Account byte layout157158All PDA accounts follow: `[discriminator: u8 | version: u8 | data...]`159160**Important**: Pinocchio uses a 1-byte discriminator. Anchor uses 8 bytes. Don't conflate them.161162```rust163pub trait Discriminator {164const DISCRIMINATOR: u8; // 1 byte, not 8165}166167pub trait Versioned {168const VERSION: u8;169}170171// DATA_LEN = size of data payload only (excludes disc + version prefix)172// LEN = 1 + 1 + DATA_LEN (total account size)173pub trait AccountSize {174const DATA_LEN: usize;175const LEN: usize = 1 + 1 + Self::DATA_LEN;176}177178// Zero-copy read: validates byte 0 (disc), skips byte 1 (version), casts &data[2..] to &Self179pub trait AccountDeserialize: Sized + Discriminator + AccountSize {180fn from_bytes(data: &[u8]) -> Result<&Self, ProgramError> {181validate_discriminator!(data, Self::DISCRIMINATOR);182require_account_len!(data, Self::LEN);183Ok(unsafe { &*(data[2..].as_ptr() as *const Self) })184}185fn from_bytes_mut(data: &mut [u8]) -> Result<&mut Self, ProgramError> {186validate_discriminator!(data, Self::DISCRIMINATOR);187require_account_len!(data, Self::LEN);188Ok(unsafe { &mut *(data[2..].as_mut_ptr() as *mut Self) })189}190}191192pub trait AccountSerialize: Discriminator + Versioned {193fn to_bytes_inner(&self) -> Vec<u8>;194fn to_bytes(&self) -> Vec<u8> {195let mut bytes = vec![Self::DISCRIMINATOR, Self::VERSION];196bytes.extend(self.to_bytes_inner());197bytes198}199}200201// Marker traits — no methods202pub trait InstructionAccounts<'a> {}203204// Marker trait for data structs; LEN is the expected byte length of instruction data205pub trait InstructionData<'a>: Sized {206const LEN: usize;207// Data structs implement TryFrom<&'a [u8]> separately208}209210pub trait PdaSeeds {211const PREFIX: &'static [u8];212fn seeds(&self) -> Vec<&[u8]>;213// Returns seeds + bump slice, ready for invoke_signed214fn seeds_with_bump<'a>(&'a self, bump: &'a [u8; 1]) -> Vec<Seed<'a>> {215let mut s: Vec<Seed> = self.seeds().into_iter().map(Seed::from).collect();216s.push(Seed::from(bump.as_ref()));217s218}219// Use at initialization to get canonical bump (find loops internally).220fn derive_address(&self, program_id: &Address) -> (Address, u8) {221Address::find_program_address(&self.seeds(), program_id)222}223// Use after initialization when bump is already stored (no bump search loop).224fn derive_address_with_bump(&self, program_id: &Address, bump: u8) -> Result<Address, ProgramError> {225let mut seeds = self.seeds();226let bump_seed = [bump];227seeds.push(&bump_seed);228Address::create_program_address(&seeds, program_id).map_err(|_| ProgramError::InvalidSeeds)229}230fn validate_pda(&self, account: &AccountView, program_id: &Address, bump: u8) -> ProgramResult {231let expected = self.derive_address_with_bump(program_id, bump)?;232if account.address() != &expected {233return Err(ProgramError::InvalidSeeds);234}235Ok(())236}237fn validate_pda_address(&self, account: &AccountView, program_id: &Address) -> Result<u8, ProgramError> {238let (expected, canonical_bump) = self.derive_address(program_id);239if account.address() != &expected {240return Err(ProgramError::InvalidSeeds);241}242Ok(canonical_bump)243}244}245246// For state structs that store their own bump247pub trait PdaAccount: PdaSeeds {248fn bump(&self) -> u8;249fn validate_self(&self, account: &AccountView, program_id: &Address) -> ProgramResult {250self.validate_pda(account, program_id, self.bump())251}252}253```254255## Instruction Directory Structure256257Organize each instruction as its own module:258259```260src/instructions/261├── mod.rs ← re-exports + discriminator enum262├── impl_instructions.rs ← define_instruction! expansions263├── deposit/264│ ├── mod.rs265│ ├── accounts.rs ← DepositAccounts, TryFrom<&'a [AccountView]>266│ ├── data.rs ← DepositData, TryFrom<&'a [u8]>267│ └── processor.rs ← process() business logic268└── withdraw/269└── ...270```271272Use `define_instruction!` to wire accounts + data into an instruction struct without boilerplate:273274```rust275macro_rules! define_instruction {276($name:ident, $accounts:ty, $data:ty) => {277pub struct $name<'a> {278pub accounts: $accounts,279pub data: $data,280}281282impl<'a> From<($accounts, $data)> for $name<'a> {283fn from((accounts, data): ($accounts, $data)) -> Self {284Self { accounts, data }285}286}287288impl<'a> TryFrom<(&'a [u8], &'a [AccountView])> for $name<'a> {289type Error = ProgramError;290fn try_from((data, accounts): (&'a [u8], &'a [AccountView])) -> Result<Self, Self::Error> {291Ok(Self {292accounts: <$accounts>::try_from(accounts)?,293data: <$data>::try_from(data)?,294})295}296}297};298}299300define_instruction!(Deposit, DepositAccounts<'a>, DepositData);301```302303## Account Validation304305Pinocchio requires manual validation. Wrap all checks in `TryFrom` implementations:306307### Account Struct Validation308309```rust310pub struct DepositAccounts<'a> {311pub owner: &'a AccountView,312pub vault: &'a AccountView,313pub system_program: &'a AccountView,314}315316impl<'a> TryFrom<&'a [AccountView]> for DepositAccounts<'a> {317type Error = ProgramError;318319fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {320let [owner, vault, system_program, _remaining @ ..] = accounts else {321return Err(ProgramError::NotEnoughAccountKeys);322};323324// Signer check325if !owner.is_signer() {326return Err(ProgramError::MissingRequiredSignature);327}328329// Owner check330if !vault.owned_by(&pinocchio_system::ID) {331return Err(ProgramError::InvalidAccountOwner);332}333334// Program ID check (prevents arbitrary CPI)335if system_program.address() != &pinocchio_system::ID {336return Err(ProgramError::IncorrectProgramId);337}338339Ok(Self { owner, vault, system_program })340}341}342```343344### Instruction Data Validation345346```rust347pub struct DepositData {348pub amount: u64,349}350351impl<'a> TryFrom<&'a [u8]> for DepositData {352type Error = ProgramError;353354fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {355require_len!(data, core::mem::size_of::<u64>());356357let amount = u64::from_le_bytes(data[..8].try_into().map_err(|_| ProgramError::InvalidInstructionData)?);358359if amount == 0 {360return Err(ProgramError::InvalidInstructionData);361}362363Ok(Self { amount })364}365}366```367368## Token programs369370Use the crates pinocchio-token and pinocchio-token2022371372### SPL Token373374```rust375use pinocchio_token::{instructions::InitializeMint2, state::Mint};376377...378InitializeMint2 {379mint: account,380decimals,381mint_authority,382freeze_authority,383}.invoke()?;384385let mint = Mint::from_account_view(account)?;386```387388### Token2022389390Token2022 provides a similar state struct391392```rust393let mint = Mint::from_account_view(account)?;394```395396## Cross-Program Invocations (CPIs)397398### Basic CPI399400```rust401use pinocchio_system::instructions::Transfer;402403Transfer {404from: self.accounts.owner,405to: self.accounts.vault,406lamports: self.data.amount,407}.invoke()?;408```409410### PDA-Signed CPI411412```rust413use pinocchio::cpi::{Seed, Signer};414415let bump_byte = &[bump];416let seeds = [417Seed::from(b"vault"),418Seed::from(self.accounts.owner.address().as_ref()),419Seed::from(&bump_byte),420];421let signers = [Signer::from(&seeds)];422423Transfer {424from: self.accounts.vault,425to: self.accounts.owner,426lamports: self.accounts.vault.lamports(),427}.invoke_signed(&signers)?;428```429430## Reading and Writing Data431432### Struct Field Ordering433434Order fields from largest to smallest alignment to minimize padding:435436```rust437// Good: 16 bytes total438#[repr(C)]439struct GoodOrder {440big: u64, // 8 bytes, 8-byte aligned441medium: u16, // 2 bytes, 2-byte aligned442small: u8, // 1 byte, 1-byte aligned443// 5 bytes padding444}445446// Bad: 24 bytes due to padding447#[repr(C)]448struct BadOrder {449small: u8, // 1 byte450// 7 bytes padding451big: u64, // 8 bytes452medium: u16, // 2 bytes453// 6 bytes padding454}455```456457### Compile-Time Layout Assertions458459Use `assert_no_padding!(Type, expected_size)` to catch unintended struct padding at compile time. Pass the expected `DATA_LEN` (the payload, excluding the 2-byte disc+version prefix):460461```rust462assert_no_padding!(Config, Config::DATA_LEN);463```464465### Explicit Padding and Versioning466467Reserve bytes for future fields to avoid breaking account layout changes. Remember: the `discriminator` and `version` bytes live in the 2-byte prefix managed by `AccountSerialize`/`AccountDeserialize` — the struct itself contains only the data payload:468469```rust470#[repr(C)]471pub struct Config {472pub bump: u8,473pub authority: [u8; 32],474pub _reserved: [u8; 6], // explicit padding for future fields475}476477impl Discriminator for Config { const DISCRIMINATOR: u8 = 0; }478impl Versioned for Config { const VERSION: u8 = 1; }479impl AccountSize for Config { const DATA_LEN: usize = 39; }480481assert_no_padding!(Config, 39);482```483484### Dangerous Patterns to Avoid485486```rust487// ❌ transmute with unaligned data488let value: u64 = unsafe { core::mem::transmute(bytes_slice) };489490// ❌ Pointer casting to packed structs491#[repr(C, packed)]492pub struct Packed { pub a: u8, pub b: u64 }493let config = unsafe { &*(data.as_ptr() as *const Packed) };494495// ❌ Direct field access on packed structs creates unaligned references496let b_ref = &packed.b;497498// ❌ Assuming alignment without verification499let config = unsafe { &*(data.as_ptr() as *const Config) };500```501502## Error Handling503504Use `thiserror` for descriptive errors (supports `no_std`):505506```rust507use thiserror::Error;508use num_derive::FromPrimitive;509use pinocchio::program_error::ProgramError;510511#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]512pub enum VaultError {513#[error("Lamport balance below rent-exempt threshold")]514NotRentExempt,515#[error("Invalid account owner")]516InvalidOwner,517#[error("Account not initialized")]518NotInitialized,519}520521impl From<VaultError> for ProgramError {522fn from(e: VaultError) -> Self {523ProgramError::Custom(e as u32)524}525}526```527528## Closing Accounts Securely529530Prevent revival attacks by marking closed accounts:531532```rust533pub fn close(account: &AccountView, destination: &AccountView) -> ProgramResult {534// Add lamports535destination.set_lamports(destination.lamports() + account.lamports())?;536537// Close538account.close()539}540```541542## Performance Optimization543544### Feature Flags545546```toml547[features]548default = ["perf"]549perf = []550```551552```rust553#[cfg(not(feature = "perf"))]554solana_program_log::log!("Instruction: Deposit");555```556557### Bitwise Flags for Storage558559Pack up to 8 booleans in one byte:560561```rust562const FLAG_ACTIVE: u8 = 1 << 0;563const FLAG_FROZEN: u8 = 1 << 1;564const FLAG_ADMIN: u8 = 1 << 2;565566// Set flag567flags |= FLAG_ACTIVE;568569// Check flag570if flags & FLAG_ACTIVE != 0 { /* active */ }571572// Clear flag573flags &= !FLAG_ACTIVE;574```575576### Zero-Allocation Architecture577578Use references instead of heap allocations:579580```rust581// Good: references with borrowed lifetimes582pub struct Instruction<'a> {583pub accounts: &'a [AccountView],584pub data: &'a [u8],585}586587// Enforce no heap usage588no_allocator!();589```590591Respect Solana's memory limits: 4KB stack per function, 32KB total heap.592593### Skip Redundant Checks594595If a CPI will fail on incorrect accounts anyway, skip pre-validation:596597```rust598// Instead of validating ATA derivation, compute expected address599let expected_ata = find_program_address(600&[owner.address(), token_program.address(), mint.address()],601&pinocchio_associated_token_account::ID,602).0;603604if account.address() != &expected_ata {605return Err(ProgramError::InvalidAccountData);606}607```608609## Batch Instructions610611Process multiple operations in a single CPI (saves ~1000 CU per batched operation):612613```rust614const IX_HEADER_SIZE: usize = 2; // account_count + data_length615616pub fn process_batch(mut accounts: &[AccountView], mut data: &[u8]) -> ProgramResult {617loop {618if data.len() < IX_HEADER_SIZE {619return Err(ProgramError::InvalidInstructionData);620}621622let account_count = data[0] as usize;623let data_len = data[1] as usize;624let data_offset = IX_HEADER_SIZE + data_len;625626if accounts.len() < account_count || data.len() < data_offset {627return Err(ProgramError::InvalidInstructionData);628}629630let (ix_accounts, ix_data) = (&accounts[..account_count], &data[IX_HEADER_SIZE..data_offset]);631632process_inner_instruction(ix_accounts, ix_data)?;633634if data_offset == data.len() {635break;636}637638accounts = &accounts[account_count..];639data = &data[data_offset..];640}641642Ok(())643}644```645646## Events647648### Simple logging649650For debug output or non-critical events where truncation is acceptable, use `solana-program-log` (pinocchio's own log module is being removed in favour of this crate — see [anza-xyz/pinocchio#261](https://github.com/anza-xyz/pinocchio/pull/261)):651652```rust653use solana_program_log::log;654655log!("deposited {}", amount);656```657658Solana truncates logs beyond ~10KB per transaction. If your event data exceeds this or indexers need to reliably parse it, use the CPI pattern below instead.659660### Event emission via CPI (truncation-safe)661662For production events that indexers must reliably read, emit via CPI into a no-op `EmitEvent` instruction on the program itself. The event data lives in the instruction data field (not logs), which is never truncated.663664Events are validated by an `event_authority` PDA that must sign the CPI:665666```rust667pub const EVENT_AUTHORITY_SEED: &[u8] = b"event_authority";668pub const EVENT_IX_TAG: u64 = 0x1d9acb512ea545e4; // Anchor-compatible event tag669pub const EVENT_IX_TAG_LE: [u8; 8] = EVENT_IX_TAG.to_le_bytes();670671// Event authority PDA (derived at compile time if possible)672pub fn find_event_authority() -> (Address, u8) {673pinocchio_pubkey::find_program_address(&[EVENT_AUTHORITY_SEED], &crate::ID)674}675```676677### Event Struct Pattern678679```rust680pub trait EventDiscriminator {681const DISCRIMINATOR: [u8; 9]; // 8-byte tag + 1-byte event id682}683684pub trait EventSerialize {685fn serialize(&self) -> Vec<u8>;686}687688pub struct DepositEvent {689pub owner: [u8; 32],690pub amount: u64,691}692693impl EventDiscriminator for DepositEvent {694const DISCRIMINATOR: [u8; 9] =695[/* EVENT_IX_TAG_LE bytes */ 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, /* event id */ 0];696}697```698699### Emitting an Event700701```rust702pub fn emit_event<E: EventDiscriminator + EventSerialize>(703event: &E,704event_authority: &AccountView,705program: &AccountView,706) -> ProgramResult {707let mut data = E::DISCRIMINATOR.to_vec();708data.extend(event.serialize());709710// CPI to self with event_authority as signer711pinocchio::program::invoke(712&Instruction { program_id: &crate::ID, accounts: &[...], data: &data },713&[event_authority, program],714)715}716```717718### EmitEvent Processor719720Add a dedicated discriminator (conventionally `228`) that validates the event authority and does nothing else:721722```rust723// In entrypoint routing:724Some((228, _)) => {725if !accounts.iter().any(|a| a.address() == &event_authority && a.is_signer()) {726return Err(ProgramError::MissingRequiredSignature);727}728Ok(()) // no-op, data is read off-chain from instruction data729}730```731732## Testing733734Use Mollusk or LiteSVM for fast Rust-based testing:735736```rust737#[cfg(test)]738pub mod tests;739740// Run with: cargo test-sbf741```742743See [testing.md](../testing.md) for detailed testing patterns with Mollusk and LiteSVM.744745## Build & Deployment746747### Build Validation748749After `cargo build-sbf`:750751- [ ] Check .so file size (>1KB, typically 5-15KB for Pinocchio programs)752- [ ] Verify file type: `file target/deploy/program.so` should show "ELF 64-bit LSB shared object"753- [ ] Test regular compilation: `cargo build` should succeed754- [ ] Run tests: `cargo test` should pass755756### Dependency Compatibility Issues757758**If SBF build fails with "edition2024" errors:**759760```bash761# Downgrade problematic dependencies to compatible versions762cargo update base64ct --precise 1.6.0763cargo update constant_time_eq --precise 0.4.1764cargo update blake3 --precise 1.5.5765```766767**When to apply**: Only when encountering Cargo "edition2024" errors during `cargo build-sbf`. These downgrades resolve toolchain compatibility issues while maintaining functionality.768769**Note**: These specific versions were tested and verified to work with current Solana toolchain. Regular `cargo update` may pull incompatible versions.770771## Security Checklist772773### Account Validation774- [ ] Validate account owners with `verify_owned_by` in `TryFrom`775- [ ] Check signer status with `verify_signer`776- [ ] Enforce writable/read-only with `verify_writable` / `verify_readonly`777- [ ] Validate program IDs before CPIs (prevent arbitrary CPI)778- [ ] Check for duplicate mutable accounts779780### PDA Safety781- [ ] Derive canonical bump with `find_program_address` at init — never trust user-supplied bumps782- [ ] Store canonical bump in account data and validate on every use via `PdaAccount::validate_self`783- [ ] Only transfer the lamport deficit on init — not the full rent amount (lamport griefing)784785### Sysvars (Pinocchio has no implicit validation)786- [ ] Use `Clock::get()?` and `Rent::get()?` — never accept sysvars as passed-in accounts787788### Data & Arithmetic789- [ ] Use `require_len!` before parsing instruction data790- [ ] Use checked math (`checked_add`, `checked_sub`, etc.)791792### Account Lifecycle793- [ ] Close accounts with `account.close()` — this transfers ownership back to the system program794- [ ] Discriminator check on every read prevents type cosplay attacks795