jiminy
Pinocchio is the engine. Jiminy keeps it honest.
Writing Solana programs with pinocchio is fast — no allocator, no borsh, full control over every byte in every account. The tradeoff is that safety checks end up scattered through your handlers. Signer check here, owner check there, discriminator byte manually compared somewhere else, and an overflow somewhere you forgot to look at.
Jiminy bundles those checks into composable functions and macros. It doesn't abstract
away pinocchio — you still work directly with AccountView, Address, and raw byte
slices. Jiminy just makes the guard-rail part less repetitive, and adds a few things
that neither pinocchio nor Anchor ever got around to building.
No allocator. No borsh. No proc macros. BPF-safe.
Every function is #[inline(always)]. Designed to inline away in BPF builds;
the benchmark suite shows 3–16 CU of overhead per instruction
with a smaller binary than hand-written Pinocchio.
Install
[]
= "0.2"
Quick Start
use *;
The prelude re-exports all check functions, macros, cursors, header helpers,
math utilities, AccountList, and the pinocchio core types (AccountView,
Address, ProgramResult, ProgramError). One import, everything you need.
No proc macros is both an advantage and a conscious tradeoff. Less surface area = fewer build surprises = fully auditable. The tradeoff: no auto-generated IDL or client code. For the teams that care about CU budgets and binary size, that's the right call.
A real example
use ;
const VAULT_DISC: u8 = 1;
const VAULT_LEN: usize = 41; // 1 disc + 8 balance + 32 authority
What's in the box
Account checks
| Function | Anchor equivalent | What it does |
|---|---|---|
check_signer(account) |
signer |
Must be a transaction signer |
check_writable(account) |
mut |
Must be marked writable |
check_owner(account, program_id) |
owner |
Must be owned by your program |
check_pda(account, expected) |
seeds + bump |
Address must match the derived PDA |
check_system_program(account) |
Program<System> |
Must be the system program |
check_executable(account) |
executable |
Must be an executable program |
check_uninitialized(account) |
init |
Data must be empty — prevents reinit attacks |
check_has_one(stored, account) |
has_one |
Stored address field must match account key |
check_rent_exempt(account) |
rent_exempt |
Must hold enough lamports to be rent-exempt |
check_lamports_gte(account, min) |
constraint |
Must hold at least min lamports |
check_closed(account) |
close |
Must have zero lamports and empty data |
check_size(data, min_len) |
— | Raw slice is at least N bytes |
check_discriminator(data, expected) |
discriminator |
First byte must match type tag |
check_account(account, id, disc, len) |
composite | Owner + size + discriminator in one call |
Macros
| Macro | Anchor equivalent | What it does |
|---|---|---|
require!(cond, err) |
require! |
Return error if condition is false |
require_eq!(a, b, err) |
require_eq! |
a == b (scalars) |
require_neq!(a, b, err) |
require_neq! |
a != b (scalars) |
require_gt!(a, b, err) |
require_gt! |
a > b |
require_gte!(a, b, err) |
require_gte! |
a >= b |
require_lt!(a, b, err) |
require_lt! |
a < b |
require_lte!(a, b, err) |
require_lte! |
a <= b |
require_keys_eq!(a, b, err) |
require_keys_eq! |
Two Address values must be equal |
require_keys_neq!(a, b, err) |
require_keys_neq! |
Two Address values must differ |
require_accounts_ne!(a, b, err) |
— | Two accounts must have different addresses |
require_flag!(byte, n, err) |
— | Bit n must be set in byte |
Math
| Function | What it does |
|---|---|
checked_add(a, b) |
Overflow-safe u64 addition |
checked_sub(a, b) |
Underflow-safe u64 subtraction |
checked_mul(a, b) |
Overflow-safe u64 multiplication |
Account lifecycle
| Function | What it does |
|---|---|
safe_close(account, destination) |
Move all lamports + close atomically |
write_discriminator(data, disc) |
Write type tag byte when initializing |
What Anchor doesn't give you
Anchor is good at what it does. But once you step off the borsh treadmill and go zero-copy, a few things fall off the table. These are the gaps we built Jiminy to fill.
SliceCursor — field reads without the arithmetic
Reading fields from raw account data in pinocchio usually means keeping byte offsets in your head or in constants, then slicing manually. Fine for one or two fields, annoying for five, and a footgun when you change the layout and forget to update an offset three functions away.
SliceCursor tracks the position for you:
let data = account.try_borrow?;
let mut cur = new; // skip discriminator
let balance = cur.read_u64?;
let authority = cur.read_address?;
let is_locked = cur.read_bool?;
let padding = cur.skip?;
No alloc. No schema. If you run off the end of the buffer you get
AccountDataTooSmall, not a panic or silent garbage.
Supported reads: u8, u16, u32, u64, i64, bool, Address, skip.
DataWriter — field writes without the arithmetic
The write-side complement to SliceCursor. Use it when initializing account
data inside a create instruction. Same idea — position-tracked, bounds-checked,
every write little-endian.
let mut raw = new_account.try_borrow_mut?;
write_discriminator?;
let mut w = new;
w.write_u64?; // initial balance
w.write_address?; // 32-byte authority key
The write_discriminator helper is separate so it's explicit that byte zero
is special — it's the type tag that every other check function looks at first.
require_accounts_ne! — source ≠ destination
One of the oldest classes of token program bugs: pass the same account as both
source and destination, end up with corrupted state or a free mint. Anchor doesn't
have a built-in constraint for this. You'd need a custom constraint or an inline
if source.key() == destination.key() { return err; }.
require_accounts_ne!;
One line. Runs before you touch any balances.
check_lamports_gte — collateral and fee floors
// Verify the collateral account holds enough before accepting a position
check_lamports_gte?;
Anchor's constraint system doesn't expose lamport checks directly. You'd write a custom constraint or inline the comparison. Here it's a named function with an obvious error return.
check_closed — verify a previous close actually happened
In CPI-heavy programs you sometimes need to confirm that an account was fully closed by an earlier instruction before you proceed — whether that's reusing the address, completing a multi-step flow, or enforcing ordering guarantees.
// Confirm the escrow was already closed before releasing collateral
check_closed?;
Zero lamports and empty data. If either condition isn't met, you get
InvalidAccountData and stop.
Compared to the alternatives
| Raw pinocchio | Anchor | Jiminy | |
|---|---|---|---|
| Allocator required | No | Yes | No |
| Borsh required | No | Yes | No |
| Proc macros | No | Yes | No |
| Account validation | Manual | #[account(...)] constraints |
Functions + macros |
| Data reads | Manual index arithmetic | Account<'info, T> + borsh |
SliceCursor |
| Data writes | Manual index arithmetic | Automatic via borsh | DataWriter |
| Overflow-safe math | Manual | Built-in | checked_add/sub/mul |
| Source ≠ dest guard | Manual | Not built-in | require_accounts_ne! |
| Lamport floor check | Manual | Not built-in | check_lamports_gte |
| Close verification | Manual | Not built-in | check_closed |
The point isn't that Anchor is bad — it's that once you're working at the pinocchio level, you shouldn't have to give up composable safety primitives to do it.
Used in SHIPyard
Jiminy is being used in SHIPyard — a platform for building, deploying, and sharing Solana programs. The on-chain program registry is built with Jiminy's check functions and layout convention, and the code generator targets Jiminy as a framework option.
Account Layout Convention
Jiminy ships an opinionated Account Layout v1
convention — an 8-byte header with discriminator, version, flags, and
optional data_len. Use write_header / check_header / header_payload
for versioned, evolvable account schemas without proc macros.
See docs/LAYOUT_CONVENTION.md for the full spec and a copy-pasteable layout lint test.
Benchmarks
Comparing a vault program (deposit / withdraw / close) written in raw Pinocchio vs the same logic using Jiminy. Measured via Mollusk SVM on Agave 2.3.
Compute Units
| Instruction | Pinocchio | Jiminy | Delta |
|---|---|---|---|
| Deposit | 146 CU | 149 CU | +3 |
| Withdraw | 253 CU | 266 CU | +13 |
| Close | 214 CU | 230 CU | +16 |
Binary Size (release SBF)
| Program | Size |
|---|---|
| Pinocchio vault | 18.7 KB |
| Jiminy vault | 17.4 KB |
Jiminy adds 3–16 CU of overhead per instruction (a single sol_log costs
~100 CU). The binary is actually 1.3 KB smaller thanks to pattern
deduplication from AccountList and the check functions.
See BENCHMARKS.md for full details and instructions to run them yourself.
Reference Programs
| Program | What it demonstrates |
|---|---|
examples/jiminy-vault |
Init/deposit/withdraw/close with AccountList, cursors, safe_close |
examples/jiminy-escrow |
Two-party escrow, flag-based state, check_closed, ordering guarantees |
Both use the Jiminy Header v1 layout. Fork them as starting templates.
About
Built by MoonManQuark / Bluefoot Labs.
If jiminy has saved you some debugging time, donations are welcome
at SolanaDevDao.sol — it goes toward keeping development going.
License
Apache-2.0. See LICENSE.
pinocchio is also Apache-2.0 — anza-xyz/pinocchio. Apache wrapping Apache, all the way down.