Benchmark policy
How Hopper reports same-provenance performance data without overclaiming.
Compute-unit measurements for individual Hopper primitives and for cross-framework parity workloads on Solana.
How Benchmarks Work
Two measurement methods have appeared on this page, and they are not interchangeable:
- Mollusk net-of-logging (current). The primitive lab runs
under
mollusk-svm(validator-free). Each primitive is dispatched between twosol_log_compute_units()syscalls; the runner records the whole instruction'scompute_units_consumed(whole-ix) and the bracketed delta minus the empty-bracket overhead measured by a dedicated probe instruction (net), the closest estimate of the primitive alone. - Validator-log deltas (historical, April 2026). An earlier version of
this page published raw log-delta figures from
solana-test-validator 2.1(check_keys_eq ~40 CU, overlay~8 CU, receipt~50 CU, and the rest). Those numbers are retired. They were captured with a different bracketing method and are not comparable to the net column. Do not quote them.
The primitive benchmark program lives in the sibling
hopper-bench product repo.
From the framework workspace, hopper profile bench still knows how to run
the primitive lab; cross-framework orchestration, Docker runners, baselines,
and raw artifacts are owned by the benchmark repo so release docs never drift
from the executable harness.
All CU numbers are toolchain- and runtime-relative: identical Anchor code has
been observed to move 571 → 685 CU between Solana 2.1 and 2.3, and Mollusk CU
parity with mainnet is governed by the configured SVMFeatureSet. Compare
numbers only within one provenance block. A refresh on the agave-4.0 Mollusk
stack (0.13.x, SIMD-0339 active) is queued.
Release-Facing Benchmark Policy
Release-facing comparison tables must come from one hopper-bench run that
uses the same lockfile, SBF toolchain, Mollusk version, seed set, feature
flags, release profile, and command line for every included framework.
The current vault snapshot includes Hopper, an in-tree Anza Pinocchio target,
Quasar's upstream examples/vault target, and an in-tree Anchor 0.31.1
comparator. Quasar's upstream vault exposes only deposit and withdraw, so
validation-only rows are shown as n/a for Quasar instead of being
synthesized by the harness.
Primitive CU Results (Mollusk, 2026-07-09)
Measured with the primitive-bench Mollusk runner (mollusk-svm 0.10.3),
re-baselined 2026-07-09 at framework HEAD (prior 07-07 artifacts preserved
in the bench repo's results/primitive-bench/; fresh run in
results/primitive-bench-2026-07-09/).
whole-ix includes dispatch, fixture checks, and the logging brackets;
net subtracts the measured empty-bracket overhead (101 CU, probe disc 21).
The April 2026 column is the superseded validator-log figure each row
replaces (different method, see above).
| Disc | Operation | Whole-ix CU | Net CU | April 2026 (superseded) | Category |
|---|---|---|---|---|---|
| 0 | check_signer |
461 | 3 | ~20 | Validation |
| 1 | check_writable |
462 | 3 | ~20 | Validation |
| 2 | check_owner |
472 | 14 | ~50 | Validation |
| 3 | Vault::load() (T1 full check) |
495 | 33 | ~120 | Account loading |
| 4 | check_keys_eq |
491 | 15 | ~40 | Validation |
| 5 | Vault::overlay() (57 bytes) |
475 | 2 | ~8 | Memory access (Tier A) |
| 6 | write_header |
483 | 6 | ~30 | Account init |
| 7 | zero_init (57 bytes) |
498 | 21 | ~15 | Account init |
| 8 | check_account_fast |
462 | 5 | ~12 | Validation (fast path) |
| 9 | emit_event (32-byte payload) |
688 | 240 | ~100 | Events |
| 10 | TrustProfile::load (Strict) |
493 | 29 | ~130 | Trust loading |
| 11 | pod_from_bytes (57 bytes) |
475 | 2 | ~6 | Memory access (Tier B) |
| 12 | StateReceipt::begin + commit |
2395 | 1915 | ~50 | Receipts |
| 13 | read_layout_id + compare |
479 | 6 | ~15 | Fingerprint check |
| 14 | StateSnapshot::capture + diff |
476 | 0* | ~30 | State tracking |
| 15 | overlay_mut + field write |
482 | 4 | ~10 | Memory access (Tier A mut) |
| 16 | raw_cast_baseline (unsafe ptr) |
475 | 2 | ~4 | Competitor baseline |
| 17 | StateReceipt (enriched fields) |
2396 | 1917 | ~80 | Receipt (all fields) |
| 18 | receipt + emit (64B log) |
2711 | 2231 | ~150 | Receipt + event |
| 19 | proc_macro_typed_dispatch |
651 | 183 | n/a | Macro dispatch |
Note on the key-compare rows: all 32-byte key compares were rerouted to
4×u64 word-compare PartialEq on 2026-07-07 (the G1 pass). The check_keys_eq
15 CU and check_owner 14 CU rows above are measured post-G1; the April
~40 / ~50 figures are pre-G1 and retired.
Note on the receipt rows: the 2026-07-09 re-baseline measured the receipt
engine ~31% cheaper than 07-07 (begin+commit 2,784 → 1,915 net;
+emit 3,141 → 2,231). The same panic-formatting elimination that removed
~5 KiB of core::fmt also un-pessimized the fingerprint/diff hot loops.
*Disc 14's 0 is a measurement artifact, not a win: the probe discards its
result on an unchanged account and the compiler now dead-code-eliminates
the sequence; budget snapshot+diff from the receipt rows instead.
Memory Access Tier Comparison
Net CU, Mollusk 2026-07-09 run:
| Tier | Operation | Net CU | What you get |
|---|---|---|---|
| Raw (unsafe) | raw ptr cast |
2 | Size check + pointer cast only. Competitor baseline |
| B (pod) | pod_from_bytes |
2 | Bounds-checked typed view |
| A (safe) | Vault::overlay() |
2 | Header + layout_id + bounds check |
| A (mut) | overlay_mut + field set |
4 | Mutable overlay + write |
| Full load | Vault::load() |
33 | Owner + disc + version + layout_id + size |
| Strict trust | TrustProfile::load |
29 | Full cross-program trust validation |
The Performance Story
Hopper's safe overlay costs what a raw pointer cast costs.
This is a measured claim, not a rounding argument: in the 2026-07-09
Mollusk lab, the raw unsafe cast baseline and Hopper's safe, validated
overlay 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.) The bounds check, header validation, and layout-fingerprint
verification disappear into the same measured cost as
*const u8 as *const T.
For hot paths where accounts are already validated, use Tier A overlay. For
cold paths, use Vault::load() at 33 CU net for full protocol-grade
validation (owner + disc + version + layout_id + size). The cost of safety
scales with how much safety you need, and at the overlay tier it is
measured at zero premium.
Validation Cost Breakdown
Net CU, Mollusk 2026-07-09 run:
| Check | Net CU | Purpose |
|---|---|---|
check_signer |
3 | Verify account is a signer |
check_account_fast |
5 | Fused fast-path account check |
check_writable |
3 | Verify account is writable |
check_owner |
14 | Compare owner against program_id (post-G1 word compare) |
check_keys_eq |
15 | Compare two account keys (post-G1 word compare) |
| Full T1 load | 33 | All checks: owner + disc + version + layout_id + size |
| Strict trust load | 29 | TrustProfile with all validations |
Receipt and Tracking Overhead
Net CU, Mollusk 2026-07-09 run:
| Operation | Net CU | Notes |
|---|---|---|
StateSnapshot::capture + diff |
0* | *DCE artifact on a discarded, unchanged diff, see the receipt-row note |
read_layout_id + compare |
6 | 8-byte fingerprint verification |
StateReceipt::begin + commit |
1,915 | Full snapshot + diff + encode cycle (−31% vs 07-07) |
StateReceipt (enriched) |
1,917 | + phase, compat_impact, validation, migration |
receipt + emit |
2,231 | Full cycle: begin + set + commit + emit |
emit_event (32 bytes) |
240 | Log-based event emission |
A complete audit trail of every state mutation (full enriched receipt plus emission) measures ~2,231 CU net, about 1.1% of a 200,000 CU instruction budget (down ~31% from the 07-07 run). Receipts remain a reasonable default for audit-sensitive state changes; CU-critical one-shot programs can use the snapshot/diff core alone.
The April validator-log figures for receipts (~50/~80/~150 CU) are retired: they were captured with a bracketing method that under-measured the encode path, and they should not be quoted.
Competitor-Shaped Baselines
Net CU, Mollusk 2026-07-09 run:
| Framework Style | Equivalent Net CU | What It Does |
|---|---|---|
| Quasar / raw-cast | 2 | ptr as *const T, no validation |
| Steel / podded | 2 | Bounds-checked Pod cast |
| Hopper overlay | 2 | Header + layout_id + bounds |
| Anchor / borsh | ~500-2000 | Deserialization + clone |
The safe overlay and the raw cast are measured at the same net cost. The validation Hopper adds at this tier is free at measurement resolution; the difference against Anchor-style deserialization remains orders of magnitude.
Framework Parity Benchmark (Vault, 8-seed average)
Measured with the sibling hopper-bench Mollusk parity harness on
2026-07-09 (post-G1 key-compare lowering, the fused single-pass
entrypoint walk, the mutation-complete lamport gate, and the
tag-arithmetic error lowering). Every included framework used the same
deterministic user seed set, SBF toolchain, runner, and command line.
n/a means the upstream comparator does not implement that benchmark
instruction.
A version note on the Anchor column: it is measured against
anchor-lang 0.31.1, the comparator this table was locked to (rebuilt from
the same pinned lockfile for the 2026-07-09 run; its rows reproduced the
07-02 values exactly, confirming runner stability). anchor-lang 1.1.2 is
the current stable release (a 1.1.2 re-run is queued), and an unreleased,
Pinocchio-based Anchor v2 alpha exists whose in-repo benchmarks land at
Quasar-level CU. Read the Anchor multiples below as measurements of shipped
Anchor 0.31.1/1.x, with the shelf life that implies.
| Scenario | Hopper | Anza Pinocchio | Quasar | Anchor 0.31.1 |
|---|---|---|---|---|
| Authorize | 420 CU | 2512 CU (+2092) | n/a | 5017 CU (+4597) |
| Auth-fail (missing sig) | 66 CU | 41 CU (−25) | n/a | 2284 CU (+2218) |
| Counter (segment-safe) | 518 CU | 2539 CU (+2021) | n/a | 5156 CU (+4638) |
| Deposit | 1653 CU | 3856 CU (+2203) | 1756 CU (+103) | 7150 CU (+5497) |
| Withdraw | 486 CU | 2548 CU (+2062) | 592 CU (+106) | 5108 CU (+4622) |
| Unsigned withdraw | rejected | rejected | rejected | rejected |
Binary size (.so) |
7.46 KiB | 7.73 KiB | 5.47 KiB | 190.11 KiB |
Honest notes on this row set, each delta a priced decision:
- Withdraw 442 → 486 vs the previous published table: +44 CU is the
lamport gate (mutation-complete write-sets) actually enforcing on the
one lamport-moving instruction, a measured safety feature no other column
has. The error-lowering's +8 was recovered by the gate-check fast-out;
Deposit's +3 and Auth-fail's +5 remain, the price of the 10%
.textcut. Every Quasar-comparable row still wins. - Size: a P0 was found this cycle by loading, not building. The
gate's
static mutmade everylamports(...)program fail the SBF loader's no-writable-sections rule; it now lives in the reserved, zero-initialized VM heap and Hopper programs carry zero writable sections. With ~5 KiB of accidentally-linkedcore::fmtalso eliminated, the vault.sois now smaller than Pinocchio's on the identical contract (7.46 vs 7.73 KiB; Hopper.textis 6,016 B of the 7.46 KiB file). Quasar's 5.47 KiB still wins this row. The remaining delta is increasingly paid-for structure (receipts, the byte-range ledger, the lamport gate; outlining experiments that "saved" bytes cost +44..+73 CU and were rejected). We do not publish a size lead we have not measured.
The Pinocchio column is built in-tree from the benchmark repo's Anza
Pinocchio target, not borrowed from Quasar's reference sample or an older
"Pinocchio-style" proxy number. The Anchor column is the benchmark repo's
in-tree anchor-vault implementing the identical instruction contract.
An earlier version of this page published the 2026-05-25 table (431/72/551/1669/453 CU at 7.53 KiB). Those numbers are retired. See the bisect section below for why they were un-deployable.
Router Parity Lab: first three-way numbers
This is, to our knowledge, the first published router-class head-to-head
between zero-copy Solana frameworks. The workload (contract:
hopper-bench/ROUTER_CONTRACT.md v1) is a multi-hop swap router over a
shared mock-AMM CPI target: 1 to 3 hops, measured amount forwarding (hop i+1
input is the router's measured user-lamport delta from hop i, never the
venue-reported figure), and a min-out safety gate exercised at its boundary
on every success row. A framework that lets a min-out violation through is
disqualified from publication; all three passed.
| Row | Hopper | Quasar | Pinocchio (hand-written) |
|---|---|---|---|
| swap_1hop | 1,559 CU | 1,582 CU (+23) | 1,523 CU (−36) |
| swap_2hop | 3,035 CU | 3,064 CU (+29) | 2,975 CU (−60) |
| swap_3hop | 4,512 CU | 4,546 CU (+34) | 4,431 CU (−81) |
| Binary size | 10.74 KiB | 11.05 KiB | 10.98 KiB |
| min-out gate | rejected | rejected | rejected |
Rows re-measured 2026-07-09 after a same-day suspend-bisect-fix cycle: a routine re-run caught a regression (+52 CU/hop: gate-machinery calls reachable from the per-meta CPI closure forced spill-heavy codegen), the claim was suspended in public, a per-commit bisect attributed every CU, and the once-per-CPI delegation-sweep fix landed the rows BETTER than the 07-07 originals (1,564/3,044/4,525).
Reading it honestly:
- Hand-written Pinocchio wins the CU rows, as it should. It carries no framework surface. Hopper lands within 1.8 to 2.4% of it (+36/+60/+81 CU across the hops) while carrying full framework validation, state contracts, and tooling, and ships the smallest binary of the three.
- Hopper beats Quasar on every row (−23/−29/−34 CU) with a smaller binary. The 07-07 morning run had Hopper trailing Quasar by +34/+56/+79 CU; the fused single-pass account walk (an instruction-level audit finding, fixed and re-measured the same day) flipped every row. Both snapshots are published so the delta is checkable.
- Every framework's measured CU includes one identical mock-AMM invocation per hop, so the deltas isolate router-side framework overhead.
Results: hopper-bench/results/router-parity-2026-07-07-post-ep/ (current)
and router-parity-2026-07-07-threeway/ (same-day pre-fix snapshot).
Lazy vs Eager Dispatch Lab (R3, 2026-07-10): Hopper vs Hopper
This is a one-framework lab: the same eight-instruction dispatch vault
built twice, once with the standard eager fast_entrypoint! and once with
hopper_lazy_entrypoint! (on-demand account parsing). It is NOT a
cross-framework comparison. First measured run: 2026-07-10, Mollusk 0.10.3,
whole-instruction CU, identical 8-account fixtures for every cell, debug=0
artifacts, surprising rows confirmed by a second identical run
(deterministic). Harness: lazy-dispatch-bench in the sibling
hopper-bench repo.
| Disc | Instruction | Touched | Eager CU | Lazy CU | Delta | Delta% |
|---|---|---|---|---|---|---|
| 0 | ping | 0/8 | 119 | 98 | −21 | −17.6% |
| 1 | get_balance | 1/8 | 124 | 111 | −13 | −10.5% |
| 2 | authorize | 2/8 | 128 | 116 | −12 | −9.4% |
| 3 | counter | 2/8 | 195 | 201 | +6 | +3.1% |
| 4 | deposit | 3/8 | 123 | 158 | +35 | +28.5% |
| 5 | withdraw | 2/8 | 129 | 117 | −12 | −9.3% |
| 6 | sweep | 8/8 | 123 | 323 | +200 | +162.6% |
| 7 | flush | 8/8 | 124 | 324 | +200 | +161.3% |
Binary size: eager 3,496 B (3.41 KiB), lazy 9,960 B (9.73 KiB). Lazy is 2.8× larger (the 254-slot resolved-array machinery).
Numbers above are the 2026-07-10 re-run AFTER the runtime-typed lazy
bridge landed (the DX fix that made hopper_lazy_entrypoint! hand out
runtime types like the eager macro always did): the bridge costs the
lazy side +1 to 2 CU per instruction and +40 B versus the first run
earlier the same day, while the eager column is bit-identical. Measured,
disclosed, and worth it. Lazy handlers now compile against
hopper::prelude::LazyContext with zero hand-written substrate glue.
The honest headline is about the EAGER path. The measured bound on
Hopper's fused eager parse is ~2.75 CU per account (eager ping at 119 vs
lazy ping at 97 brackets the entire 8-account parse at ~22 CU), while
lazy's on-demand next_account() costs ~28 CU per touched account,
roughly 10× the eager batch rate. Consequences, measured:
- Lazy wins only on 0-2-account dispatch paths (−12 to −21 CU) and is already a loss at 3 touched accounts (deposit +35). Break-even sits around 2 to 3 touched accounts, LOWER than this lab's own pre-measurement doc comment guessed, which is why we measure.
- Touch-everything variants pay ~2.6× (sweep/flush +200 CU).
- Verdict for users: the lazy entrypoint is a niche tool for ping-like admin/probe instructions in accounts-heavy programs. For everything else, Hopper's eager fused parse is already near-free. That cheapness is the durable result of this lab.
Benchmark provenance
Shared toolchain for the release-facing runs:
| Field | Value |
|---|---|
| Vault four-way | re-measured 2026-07-09, hopper-bench results (framework-vaults) |
| Router three-way | re-measured 2026-07-09, hopper-bench/results/router-parity-2026-07-07-post-ep/ (pre-fix snapshot kept alongside) |
| Primitive lab | 2026-07-09, hopper-bench/results/primitive-bench-2026-07-09/ |
| Lazy-dispatch lab (R3) | 2026-07-10, lazy-dispatch-bench harness |
| Quasar checkout | 37e8a6b clean (upstream 2026-06-28) |
| Anchor | anchor-lang 0.31.1 (crates.io, locked), in-tree anchor-vault comparator |
| Rust | rustc 1.96.0 (workspace pin) |
| SBF toolchain | cargo-build-sbf 4.0.0, platform-tools v1.53 |
| SVM harness | mollusk-svm 0.10.3 |
| Samples | 8 deterministic user seed cases per parity row |
Every parity result published from hopper-bench records the Hopper
framework commit, the benchmark repo commit, competitor source commits or
release tags, Rust/SBF/Mollusk versions, exact feature flags and release
profile, and the exact reproduction command and seed count.
Performance observations
- Anchor, measured (2026-07-09 vault): Hopper is ~11.9× cheaper on
authorize, ~4.3× ondeposit, ~10.5× onwithdraw, and the artifact is 26× smaller (7.46 vs 190.11 KiB). Anchor's failure path is also expensive: a missing signer costs 2284 CU (8-byte discriminator hash + fulltry_accountsbefore the signer check) vs Hopper's 66 CU. Shelf-life caveat: these multiples apply to shipped Anchor 0.31.1/1.x; the unreleased Anchor v2 alpha benchmarks at Quasar-level CU in its own repo, so when it ships, "10× cheaper than Anchor" stops being a durable headline for any framework. The durable ground is winning within the zero-copy cluster (the router lab above) plus the state/safety/tooling surface no Pinocchio-derived framework has. - Hopper beats Quasar on both upstream Quasar workloads (deposit −103 CU, withdraw −106 CU) while carrying its full state-contract surface. Quasar publishes no comparative CU benchmark of its own; this pinned, provenance-checked matrix is currently the only published cross-framework table that includes it.
- Hopper is lower-CU than the in-tree Anza Pinocchio parity target on the measured PDA-bearing success paths in this vault contract. Treat that as a result for this benchmark, not a universal "faster than Pinocchio" claim. The router lab is the fairer overhead measurement, and there hand-written Pinocchio wins by 1.8 to 2.4%.
The +13…+44 CU delta vs 2026-05-25, bisected and resolved
Hopper's rows moved between the 2026-05-25 and 2026-07-02 runs. An automated
git bisect run (build the parity vault at each candidate, measure with the
pinned runner; the runner itself reproduces the May numbers bit-for-bit on
the May commit) pinned the entire delta to one commit: the change that
feature-gated the r2 fast entrypoint behind simd-0321.
This is not a regression. It is the removal of an unsound
optimization. Before that commit, fast_entrypoint! unconditionally read
the instruction-data pointer from the SVM's second entrypoint register.
SIMD-0321 (the proposal that populates that register) is not activated
on any public cluster; the fast path only worked in local SVMs that happen
to pass r2. The May numbers were therefore ~30 to 40 CU better than any
mainnet deployment could actually achieve. Today's table is the honest,
deployable number; the delta comes back the day the SIMD-0321 gate
activates (rebuild with the feature).
Two corollaries the bisect proved along the way:
- Fused signer/writable validation, touch maps, and write policies cost 0 CU in the parity vault. It measures identically at the pre-feature commit and at head, across all five scenarios and binary size.
- The widened auth-fail gap to Pinocchio is the same entrypoint story: their 41 CU rejection is measured with their scanning entrypoint too, so the honest comparison is 66 vs 41 (as of 2026-07-09), the scanning pass plus Hopper's dispatch reaching the fused check. Still the only vault row Pinocchio wins.
Reading the Pinocchio deltas honestly
The large CU gaps on the vault rows are mostly a PDA-strategy difference,
not a substrate difference. The in-tree Pinocchio target uses idiomatic
find_program_address (a bump search that can cost ~1,500 to 2,500 CU),
whereas the Hopper parity vault verifies a stored canonical bump with a
single create_program_address (~200 CU). A Pinocchio program that also
stored its bump would land close to Hopper on these rows. The router lab,
where the Pinocchio comparator is hand-optimized, shows exactly that shape.
So the accurate claim is "Hopper is fast by default": the cheap PDA path is the one the macros and conventions steer you toward. It is not "raw Pinocchio is slower."
Where Pinocchio is still the right choice
Use raw Pinocchio directly when the target program should remain a minimal manual substrate with no framework-owned account lifecycle, schema, CLI, or validation layer. Hopper is the framework-layer option when those surfaces are worth carrying.
The parity vault source is at
examples/hopper-parity-vault.
The cross-framework runners live in the sibling hopper-bench repo.
Deploy-cost economics
Rent-exempt deploy cost follows (elf_bytes + 128) × 6,960 lamports
(formula verified against a live mainnet program-account balance). Applied
to the 2026-07-09 vault matrix and the counter example:
| Artifact | Size | Rent at deploy |
|---|---|---|
| Hopper counter (2026-07-09 build) | 3,736 B | ≈ 0.027 SOL |
| Hopper counter (as deployed to devnet, 2026-07-07) | 4,688 B | ≈ 0.034 SOL |
| Hopper vault (2026-07-09) | 7.46 KiB | ≈ 0.054 SOL |
| Quasar vault | 5.47 KiB | ≈ 0.040 SOL |
| Pinocchio vault | 7.73 KiB | ≈ 0.056 SOL |
| Anchor 0.31.1 vault | 190.11 KiB | ≈ 1.356 SOL (~25× Hopper) |
Unlike CU tables, deploy rent does not churn with runtime versions: it is a durable cost axis, and the Anchor-class artifact is the outlier on it.
Devnet deployment evidence
Built with cargo build-sbf on the Anza toolchain and deployed to devnet
from authority HoppRy1HbNcHus9rmubDdXejDqAmhi55AURiCrq6tvxT. Bytecode
sizes are the on-disk .so artifacts that were deployed.
| Example | SBF .so bytes |
Devnet program id |
|---|---|---|
| counter | 4 688 | D8UGWDX5QRwEkKs2J9Sweabf4zd6hzdLqv7CB11SF91F |
| escrow | 18 736 | 5Ficb6k1Lv8tV8pThmQLU9H4MAYGbArwGRH2vrTHoPuN |
| versioned-state (migration) | 25 664 | EuDECNLNwPAptWC5NmenBBfjSuhZtmpPwpMQ7Z1P2GMt |
| orderbook | 18 408 | CK3XYYsbFducx9UEEWWLGAVnSAhGkMtM1TKLe8PDP6dJ |
| virtual-state | 23 240 | 6MmtjcdZuGZyceETKB2pstfSZ8Pv5r72U7dZrBCzgehz |
The 4,688-byte counter was the headline size claim as deployed; the same
example rebuilt at HEAD (2026-07-09, after the writable-section P0 fix and
the core::fmt/error-lowering size work) measures 3,736 bytes ≈ 0.027
SOL, a complete, deployable zero-copy program (#[account] layout,
#[derive(Accounts)] context with a has_one constraint, single-byte
dispatch, and a checked mutation) in under 4 KiB of bytecode. The devnet
program id above still runs the 4,688-byte 07-07 artifact until redeployed.
Measured on-chain compute
The escrow make instruction (self-initializes a fresh Escrow account
via the init lifecycle, then writes four typed fields) consumed
1,761 CU on devnet, decoded from the confirmed transaction by
hopper explain against the checked-in escrow manifest.
Live self-describing transactions (devnet, 2026-07-10)
The upgraded hopper-smoke (2YPBvKJ8h37bUEFBrmytzNuKfUJ5Q2o2tkTiqRCZdjme,
now carrying the event_cpi and whole-account-touch instructions) ran the
full loop live. Every instruction was fired with hopper tx send (the
no-Node generic instruction sender) and decoded back with
hopper tx explain, pure Rust on both sides, no JS toolchain anywhere:
| Instruction | Live CU | Signature (devnet) |
|---|---|---|
| initialize | 1,839 | 4R2WDJzr9ftZzJL2Qj2tjr4Ke1DRwZb1jE8CLwKHAFJzRQsFKyicKQ5ZvzfMmyYTtqc1NqMzDWaJqxroeJb3QKyS |
| deposit | 2,221 | 4Jnp43Hg7B5V1dUajjURJrriVcoNY3PdgTDfjdkt4vVhRcAth9WMEgDt3BcfL3fchd5XZ38MYrbdbEwKmQ6tXGAV |
| withdraw | 892 | 4uc6v9hCR6R2tRxGFJYgFbyPxnhWQZV4cL6VCfLnWNbPe2qAK79aX7HgkhuVfJaS4w6FjSoV7hDit6bn51SzKnfP |
| bump_whole_vault | 762 | 4yTv8gKBktSAg4sNiZgCwEnh6CkgHA89NA1r2ZrEAcNamak1xsu8u4veTYJ88wMQMaPmwsyx6jJnnmce9R2oyXMB |
| emit_receipt (event_cpi) | 3,586 | 3XbEB9QqajTjtM5tgNQAAQCYUsDa245YkzMuvc9gkBoi3eB9yWXq6LnU74TmygQsKuhKSfAez3dJuWc69Rn2zqrT |
Three decode receipts from those confirmed transactions:
- Field-level touch map, live: the withdraw decodes to
W slot 1 (vault) [48..56) -> Vault.balance. The transaction names the exact field it wrote. - Whole-account wrapper capture, live:
bump_whole_vault(aget_mutwrite, the path that was the touch map's blind spot until 2026-07-10) decodes toW slot 1 [0..76). The ambient-log capture works on a real cluster, not just in Mollusk. - Named event from inner-instruction metadata, live: the
emit_receipttransaction decodes toevent: DepositReceipt (tag 0x02) { balance: 1500000, deposit_count: 3 }with the sink-authentication provenance line, and both values cross-check against the known state sequence (2,000,000 deposited − 500,000 withdrawn = 1,500,000; count = deposit + bump + receipt = 3).
The live emit_receipt cost (3,586 CU) matches the Mollusk
measurement on the CU costs page exactly, which is
the strongest possible validation of the in-process numbers this page is
built on.
Live lazy migration (devnet, 2026-07-11)
The migrate-at-bind crank ran against a real account. Instruction 6
created a version-1 Note (B2ZyGAUw9CVqdL2tiv9yG32HwuPbpeaofTBig6UkFQrL,
header verified live: disc 8, version 1, tag 0xBEEF as a u32); the
first instruction-7 touch migrated it in place and the second bound it
steady-state:
| Step | Live CU | Signature (devnet) |
|---|---|---|
| init_note (creates V1) | 1,656 | 4iiEejiGZxvEnTuJsabnN2w7y7A4j3jCGV2zUq5P1cHpxrpHEDM8GApg6TP644BjA2z4z6iMVehJUvim35rZX8Mm |
| touch_note: MIGRATING touch | 280 | 4HTk16eVSby5r8r68A7AaCr1ZKGRThZgS9tAbVFiQzvnHX82Jvpqz5T1imEsGPHt1osUa968J9xXoNWJajSHfcaH |
| touch_note: steady-state | 251 | 4ggJLgKPhRZZ4xbwpXuAv4Mi3TsqFr2uoNkQmaVNtWaXpNWrdQoh4X9YMPPueGKVQp4FxS3JANukASi6vPPN9GMG |
Post-state read back live: version 2, tag widened to u64 with the
value preserved (0xBEEF), touches = 2. The one-time migration
premium is 29 CU, the full typed in-place upgrade (V1 identity
probe, transform, header re-stamp), and BOTH live numbers match the
Mollusk e2e (examples/hopper-smoke/tests/note_migration_sbf_e2e.rs)
exactly: the second consecutive lab-to-cluster exact match in this
document. The demo layouts also model the recommended forward-compat
pattern: V1 carries reserved padding that V2 claims, so both versions
fit one allocation and no realloc is ever needed.
Live IDL publish (devnet, 2026-07-12)
hopper publish-idl battle-tested on a real cluster for the first
time: examples/hopper-smoke's manifest-generated Anchor IDL (the same
hopper.manifest.json the manifest tooling checked in) sent to the
canonical SPL Program Metadata PDA in one signed transaction, zero
Node/JS anywhere in the path.
| Field | Value |
|---|---|
| Program | 2YPBvKJ8h37bUEFBrmytzNuKfUJ5Q2o2tkTiqRCZdjme |
| Metadata PDA | 5EQGsgPAkY4Sf6v89GDhDxXSzHve1SfkEJEDcqXZ9j2x (bump 254) |
| Payload | 4,476 raw → 727 zlib bytes (Utf8/Zlib/Json) |
| Path | fresh inline Initialize (fits one transaction) |
| Signature | D8XRoTR4SLmaPJzpTETmHTz4J2LrDHtQjkHpcHUrnK3KVtqXv6deGd2hBx7DBj2MCnNhyo5F8KVEFrrFZUBiVhb |
Verified by an independent read-back, not just a confirmed signature:
the account's owner is the metadata program, the zlib payload
decompresses to valid JSON, and name/version plus all 8 instruction
names match the source manifest. The chunked multi-transaction path and
--overwrite rewrite remain unit-tested only; this run exercised the
fresh single-transaction path.
CU Budget Reference
Per-account framework cost from the Mollusk net rows above: full validated load 33 CU + overlay access ~2 CU + fingerprint re-check 6 CU ≈ ~41 CU per account. A full emitted receipt adds ~2.2k CU where an audit trail is wanted.
| Scenario | Typical CU | Hopper overhead (loads + overlays + diff) |
|---|---|---|
| Simple transfer (1 account) | ~5,000 | ~60 CU |
| DeFi swap (3 accounts) | ~50,000 | ~130 CU |
| Complex instruction (6 accounts) | ~150,000 | ~240 CU |
In all scenarios, core Hopper overhead is well under 1% of the instruction budget; opting into full receipt emission adds ~1.1% of a 200k budget.
Running Benchmarks
# Primitive lab from the framework workspace
hopper profile bench
# Cross-framework parity labs from the sibling benchmark checkout
cd ../hopper-bench
./measure.sh all
Golden baselines, Docker runners, competitor locks, CI thresholds, and the
long-form benchmark roadmap are maintained in the sibling hopper-bench
repo.
