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)]. At compile time it produces the same
instructions you'd write by hand, just without the copy-paste.
Install
[]
= "0.1"
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 powers the on-chain program registry in SHIPyard — a platform for building, deploying, and sharing Solana programs. Every instruction handler in the project registry uses Jiminy's check functions, and SHIPyard's code generator can target Jiminy as a framework option when generating programs from IR.
Real usage in a production program is the best test. If something doesn't hold up, we'll find it.
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.