Why Hopper
The compact case for Hopper: segment borrows, policy modes, receipts, and scoped benchmark claims.
The short version
Hopper is a policy-driven zero-copy runtime for Solana. Three things set it apart:
- Segment-level borrow tracking. When one instruction mutates a vault's
balancefield, Hopper locks exactly those 8 bytes. A parallel read ofauthorityon the same account? No conflict. Every other framework locks the whole account. - One access model, five explicit tiers. Generated field accessors /
segment_ref_typedare the default hot path;load::<T>()is validated whole-layout access; const/dynamic segment APIs are advanced;raw_ref/raw_mutare typed escape hatches;unsafe { as_mut_ptr() }is full raw access. Same pipeline, different guarantees. - Policy-driven enforcement.
#[hopper::program(strict)],(sealed), or(raw)at the module level;#[instruction(N, unsafe_memory, skip_token_checks)]per handler. Every safety lever is a compile-time const the user toggles in one line.
Where Hopper sits
| Anchor | Quasar | Pinocchio | Hopper | |
|---|---|---|---|---|
| Raw entrypoint ownership | no | yes | yes | yes |
| Zero-copy account access | AccountLoader |
yes | yes | yes |
no_std / no_alloc |
no | yes | yes | yes |
| Segment-level borrow enforcement | no | no | no | yes |
| Instruction touch maps (per-ix byte footprint) | no | no | no | yes (opt-in touch-map feature; +52 CU measured in the touch-map-enabled smoke crate, zero when off) |
Field-level write policies (strict_writes) |
no | no | no | yes (declared mut ranges enforced at borrow acquire) |
| Migrate-at-bind crank (accounts upgrade as touched) | no | no | no | yes (migrate(from, with)) |
| Compile-time layout fingerprints | no | no | no | yes |
| Versioned + foreign typed loads | no | no | no | yes |
| State receipts | no | no | no | yes |
| Policy-driven safety levers | no | no | no | yes |
| Selective per-instruction unsafe | no | no | no | yes |
| Proc macros optional (not required) | required | yes | no | yes |
| Compile-fail safety proofs | no | no | no | yes (24 compile-fail fixtures + 6 pass guards) |
The three modes you can ship
STRICT
Every lever on. Typed contexts, auto-bind, constraint gauntlet, enforce_token_checks, unsafe allowed but isolated to explicit hopper_unsafe_region! blocks.
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut, has_one = authority)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}
impl<'info> Deposit<'info> {
pub fn deposit(&self, amount: u64) -> ProgramResult {
let mut vault = self.vault.get_mut()?;
vault.balance.checked_add_assign(amount)
}
}
#[hopper::program(strict)]
pub mod vault {
#[instruction(0)]
pub fn deposit(ctx: Ctx<Deposit>, amount: u64) -> ProgramResult {
ctx.accounts.deposit(amount)
}
}
Reach for this by default.
SEALED
Strict + enforce_token_checks + no unsafe anywhere. The program macro emits #[deny(unsafe_code)] on every handler. One handler can still opt back in via #[instruction(N, unsafe_memory)] for a single fast path.
#[hopper::program(sealed)]
pub mod vault {
// Every handler here: no unsafe compiles.
#[instruction(0)]
pub fn deposit(ctx: Ctx<Deposit>, amount: u64) -> ProgramResult {
ctx.accounts.deposit(amount)
}
// Opt-in: this one handler gets raw access back.
#[instruction(1, unsafe_memory)]
pub fn fast_sweep(ctx: Ctx<Sweep>) -> ProgramResult {
ctx.accounts.fast_sweep()
}
}
Reach for this when writing code that goes to external audit.
RAW
Pinocchio parity. Strict off, token checks off, unsafe on. Handlers take &mut Context<'_> directly. Author is responsible for every invariant.
#[hopper::program(raw)]
pub mod vault {
#[instruction(0)]
pub fn deposit(ctx: &mut Context<'_>, amount: u64) -> ProgramResult {
let mut vault = ctx.load_mut::<Vault>(0)?;
vault.balance = WireU64::new(vault.balance.get().checked_add(amount)
.ok_or(ProgramError::ArithmeticOverflow)?);
Ok(())
}
}
Reach for this when every CU counts and the author has already validated the invariants by hand.
Why this matters
Three classes of Solana exploits map directly onto the levers:
| Exploit class | Lever that closes it |
|---|---|
| Missing signer or wrong-authority token move | enforce_token_checks = true + TransferChecked::invoke_strict |
| Layout drift between on-chain program and client | LAYOUT_ID fingerprint enforced in load::<T>() + TS / Kotlin / Rust client assertLayoutId |
| Aliasing bug in a multi-segment write | SegmentBorrowRegistry rejects overlapping mutable borrows at runtime, compile-fail fixture ref_only_rejects_raw_ref.rs proves raw &mut cannot satisfy HopperRefOnly |
Other frameworks rely on the author to remember every check. Hopper makes the check the default and lets you opt out explicitly.
Benchmark, not claims
Current release-facing numbers come from the sibling
hopper-bench parity harness:
8-seed average, Mollusk execution, and one command line for every included
framework. The current snapshot (re-measured 2026-07-09, four-way) includes
Hopper, the benchmark repo's in-tree
Anza Pinocchio target, Quasar's upstream examples/vault target, and a
measured Anchor 0.31.1 comparator. Quasar's
upstream vault implements only deposit and withdraw, so validation-only rows
are n/a for Quasar.
| Instruction | Hopper | Anza Pinocchio | Quasar | Anchor 0.31.1 |
|---|---|---|---|---|
| authorize | 420 CU | 2512 CU | n/a | 5017 CU |
| counter_access | 518 CU | 2539 CU | n/a | 5156 CU |
| deposit | 1653 CU | 3856 CU | 1756 CU | 7150 CU |
| withdraw | 486 CU | 2548 CU | 592 CU | 5108 CU |
| binary size | 7.46 KiB | 7.73 KiB | 5.47 KiB | 190.11 KiB |
Two honest notes on the 2026-07-09 row set. Withdraw moved 442 → 486 CU
versus the previous published table: +44 CU is the mutation-complete
lamport gate actually enforcing on the one lamport-moving instruction, a
measured safety feature no other column carries (the tag-arithmetic
error lowering that bought a 10% .text cut cost +8 here, and the
gate-check fast-out recovered it). Every Quasar-comparable row still
wins. On size, the Hopper vault .so now measures smaller than
Pinocchio's on the identical contract (7.46 vs 7.73 KiB, zero writable
sections); Quasar's 5.47 KiB still wins that row. We do not publish a
size lead we have not measured.
An earlier version of this page published the 2026-05-25 table (431/551/1669/453 CU). Those numbers are retired: they were measured with a pre-gate fast-entrypoint path that only worked in local SVMs (SIMD-0321 is not active on public clusters) and were therefore better than any mainnet deployment could achieve. The table above is the honest, deployable number.
Two more measured results (provenance on the Benchmarks page):
- The safe overlay costs what a raw pointer cast costs. In the 2026-07-09 Mollusk primitive lab, Hopper's validated overlay and a raw unsafe cast both measure 2 CU net, the same number. (The 07-07 run measured both at 1; the equality is the durable claim, never the absolute alone.)
- Router parity, first published three-way: Hopper beats Quasar on every 1-3-hop swap row (−23/−29/−34 CU, re-earned 2026-07-09 after a same-day suspend-bisect-fix cycle) and runs within 1.8 to 2.4% of hand-written Pinocchio, with the smallest binary of the three (10.74 KiB).
That supports a precise claim: Anchor/Quasar-class DX, Hopper-grade safety/state contracts, Pinocchio-class raw control. It does not turn one vault benchmark into a universal "faster than Pinocchio" statement.
Methodology lives in the sibling hopper-bench product repo. Re-run from that checkout:
.\compare-framework-vaults.ps1 -HopperRoot ..\Hopper-Solana-Zero-copy-State-Framework -QuasarRoot <path-to-quasar> -OutDir results\framework-vaults
In-process testing - hopper-svm
Hopper ships its own validator-class harness so tests don't need a live solana-test-validator. Three layered execution modes:
- Default features - inline Rust simulators for the system program plus user-registered builtins. Fast unit tests, no validator dep, full Quasar-parity verb surface (
simulate_instruction,process_instruction_chain,warp_to_slot/warp_to_timestamp, stateful overlay withairdrop/set_token_balance/snapshot_accounts/restore_accounts). bpf-execution- directsolana-sbpfinterpretation of.sobytes when you need real BPF execution but want the lean dep tree.agave-runtime- validator-runtime execution. Replaces inline simulators with the Agave validator stack (solana-program-runtime+solana-bpf-loader-program+solana-system-program). AfterHopperSvm::new().with_agave_runtime(),process_instructionroutes throughInvokeContext::process_instructionagainst Agave's program cache.
let svm = HopperSvm::new().with_agave_runtime();
let result = svm.process_instruction(&transfer_ix, &[alice, bob]);
result.assert_success();
// Agave's system program reports its real CU baseline.
assert!(result.compute_units_consumed() >= 150);
The hopper-svm crate is the harness layer; it ships standalone so any Solana program (Hopper or otherwise) can pull it in as a dev-dependency. See the sibling hopper-svm repo for the full surface and its programs/README.md for sourcing SPL Token / Token-2022 / ATA .so bytes when you need real CPI tests.
Where to start
- Read MEMORY_ACCESS.md for the access-tier doctrine.
- Read POLICY_GUARANTEES.md for what each lever guarantees and drops.
- Read
examples/hopper-policy-vault/src/lib.rsfor the three modes side by side. - Run
cargo run -p hopper-cli -- verify --package hopper-policy-vaultto see the LAYOUT_ID fingerprint scan on a shipping.so. - In the
hopper-svmrepo, runcargo test --features agave-runtime process_instruction_routes_through_agave_runtimeto see the harness execute a system transfer through Agave's real runtime.
What Hopper doesn't promise
- Not an Anchor replacement for every workflow. Teams already on Anchor with a working IDL pipeline should weigh the migration cost against what Hopper adds.
- Not a serialization library. Hopper maps structs directly onto account bytes. If your account format uses Borsh, Hopper's zero-copy layer is not useful; stick with the Borsh pipeline.
- Not a host-side framework. The
hoppercrate isno_stdand targets SBF. The schema / CLI / client-gen crates are host-side; programs are not.
