Skip to content
hopper
Get started
Operations / cu-costs

CU costs

Compute-unit notes for validation, loading, receipts, events, and access tiers.

This reference covers Hopper 0.5.0, published on crates.io. See the release status for package verification, dated benchmarks, and remaining audit work.

This page preserves the per-primitive measurements from the pinned 2026-07-09 Mollusk run below. They describe that artifact, toolchain, and runtime; they are not measurements of the current release or a universal instruction budget. Operations outside that lab are identified separately.

Use the provenance matching the program and runtime being compared. Later end-to-end release measurements do not become invalid because they differ from this historical table. Cross-framework comparisons have their own provenance in BENCHMARKS.md.

Provenance

Field Value
Measured 2026-07-09
Framework commit 5819a63, clean tree
Benchmark program hopper-bench on-chain program (sibling benchmark repo). It path-depends on this framework, so the artifact embeds this commit's codegen: hopper_bench.so, 18,088 bytes, sha256 1652867bd3f47c7fb688ed29bc0fc935f2b057e91f58fc0007454da5f40b036b
SVM harness mollusk-svm 0.10.3 (validator-free, default feature set), driven by the primitive-bench host runner
Toolchain rustc 1.96.0; cargo-build-sbf 4.0.0 (platform-tools v1.53); release profile
Runner command from the hopper-bench repo root: cargo build-sbf --manifest-path hopper-bench/Cargo.toml, then cargo run --manifest-path primitive-bench/Cargo.toml --release -- --out-dir results/primitive-bench-2026-07-09
Raw artifacts hopper-bench/results/primitive-bench-2026-07-09/primitive-cu.{md,csv}, the CSV (disc,primitive,whole_ix_cu,bracketed_cu,net_cu,...) is the machine-readable source for any tooling that consumes this table

Method. Each primitive is one instruction of the benchmark program (discriminators 0–21), executed under Mollusk. Two columns per row:

  • Net CU: the delta between the two sol_log_compute_units() calls bracketing the primitive, minus the empty-bracket overhead measured by a dedicated probe (disc 21: 101 CU in this run). This is the closest estimate of the primitive alone in this historical fixture.
  • Whole-ix CU: compute_units_consumed for the entire instruction: entrypoint account parse, dispatch, fixture checks, and the measurement logging (four log-class syscalls, ≈ 400 CU) all included. Useful as an upper bound and when comparing against end-to-end instruction numbers.

The runner executes every disc twice and refuses to publish if the two runs disagree, so each number is CU-exact for this artifact. Treat net values of 0–2 CU as at measurement resolution: at that scale the bracket cannot distinguish the primitive from zero.

CU costs are toolchain- and runtime-relative (identical code has been observed to move between Solana runtime versions), so compare numbers only within one provenance block. The prior run of this same lab (2026-07-07, same harness and toolchain, framework commit fa2bfdb) is retained at hopper-bench/results/primitive-bench/ so deltas stay checkable. The next clean refresh targets the Agave 4.2.1 / Mollusk 0.15.x transition and must pin the exact resolved versions in its archive; the older Agave 4.0 / Mollusk 0.13 queue is no longer the release target.

Per-primitive costs (Mollusk, 2026-07-09)

Disc Operation Net CU Whole-ix CU Category Notes
0 check_signer 3 461 Validation is-signer flag check
1 check_writable 3 462 Validation is-writable flag check
2 check_owner 14 472 Validation owner vs program id, 4×u64 word compare
3 Vault::load() (T1 full check) 33 495 Account loading owner + disc + version + layout_id + size
4 check_keys_eq 15 491 Validation two 32-byte keys, word compare
5 Vault::overlay() (57 B) 2 475 Memory access (Tier A) bounds-checked projection; caller establishes header/layout identity
6 write_header 6 483 Account init write the 16-byte Hopper header
7 zero_init (57 B) 21 498 Account init zero the account, then write header
8 check_account_fast 5 462 Validation (fast path) fused fast-path account check
9 emit_event (32 B payload) 240 688 Events one sol_log_data segment, syscall included
10 TrustProfile::load (Strict) 29 493 Trust loading full cross-program trust validation
11 pod_from_bytes (57 B) 2 475 Memory access (Tier B) bounds-checked Pod cast
12 StateReceipt::begin + commit 1,915 2,395 Receipts snapshot + diff + encode cycle
13 read_layout_id + compare 6 479 Fingerprint check 8-byte layout fingerprint verify
14 StateSnapshot::capture + diff 0 † 476 State tracking see footnote, optimizes out in this shape
15 overlay_mut + field write 4 482 Memory access (Tier A mut) bounds-checked mutable projection + one field set
16 raw_cast_baseline (unsafe ptr) 2 475 Raw baseline size check + pointer cast only
17 StateReceipt (enriched fields) 1,917 2,396 Receipts + phase, compat_impact, validation, migration
18 receipt + emit (64 B log) 2,231 2,711 Receipts begin + set + commit + to_bytes + emit
19 proc_macro_typed_dispatch 183 651 Macro dispatch full #[hopper::program] path: dispatch + binding + u64 decode + handler
20 write_proc_header (probe) n/a 73 Harness probe unbracketed: entrypoint + dispatch + header write, zero logging
21 measurement_overhead (probe) 0 444 Harness probe empty bracket = 101 CU, subtracted from every net figure

† Disc 14 footnote. The measured region captures a snapshot of an unchanged account, diffs it, and discards the result. At this commit the compiler inlines the whole sequence and eliminates it as dead code, so the bracket measures 0 CU. Read it as "a discarded snapshot+diff costs nothing", not "state tracking is free": when the diff result feeds a receipt or a branch, you pay the receipt-row costs below. The 2026-07-07 run measured this same source at 26 CU net, the elimination is new codegen behavior at this commit, not a measurement trick.

Why whole-ix is ~450 CU when net is 3. The whole-instruction column carries the measurement scaffolding: two msg! marker logs plus the two bracket syscalls (~400 CU of log-class syscalls at 100 CU each) and the entrypoint/dispatch path. The two probes bound it: disc 21 (no accounts, empty bracket, full logging) costs 444 CU whole; disc 20 (one account, dispatch + header write, no logging at all) costs 73 CU whole, the leanest complete Hopper instruction in this lab. Production instructions carry no brackets, so their whole-instruction cost is much closer to the disc-20 shape; see the end-to-end parity numbers in BENCHMARKS.md (e.g. vault withdraw 486 CU, auth-fail rejection 66 CU, whole instructions including PDA verification).

Memory-access tiers

Same run, net CU:

Tier Operation Net CU What you get
Raw (unsafe) raw ptr cast 2 size check + pointer cast only, the unchecked baseline
B (pod) pod_from_bytes 2 bounds-checked typed view
A (safe) Vault::overlay() 2 bounds-checked projection; caller establishes header/layout identity
A (mut) overlay_mut + field set 4 mutable overlay + one write
Full load Vault::load() 33 owner + disc + version + layout_id + size
Strict trust TrustProfile::load 29 full cross-program trust validation

The measured claim this table supports: the safe, validated overlay costs the same as a raw unsafe pointer cast, 2 CU net each, in the same run, at measurement resolution. Validation you opt into scales the cost: the full protocol-grade load is 33 CU net.

Validation costs

Same run, net CU:

Check Net CU Purpose
check_signer 3 verify account is a signer
check_writable 3 verify account is writable
check_account_fast 5 fused fast-path account check
check_owner 14 owner vs program id (word compare)
check_keys_eq 15 compare two account keys (word compare)
Full T1 load 33 all checks: owner + disc + version + layout_id + size
Strict trust load 29 TrustProfile with all validations

Receipts and state tracking

Same run, net CU:

Operation Net CU Notes
read_layout_id + compare 6 8-byte fingerprint verification
StateSnapshot::capture + diff (discarded) 0 † see the disc-14 footnote above
StateReceipt::begin + commit 1,915 full snapshot + diff + encode cycle
StateReceipt (enriched) 1,917 + phase, compat_impact, validation, migration
receipt + emit 2,231 full cycle: begin + set + commit + emit
emit_event (32 B) 240 log-based event emission (sol_log_data)

A complete audit trail of one state mutation, full enriched receipt plus emission, measures 2,231 CU net, ≈ 1.1% of a 200,000 CU instruction budget (down from 3,141 CU in the 2026-07-07 run; the receipt encode path got ~29% cheaper at this commit). Receipts remain a reasonable default for audit-sensitive state changes; CU-critical one-shot programs can stay on the load/overlay/fingerprint tier and skip receipts entirely.

Self-CPI events (event_cpi)

Measured separately (2026-07-10, Mollusk 0.10.3, examples/hopper-smoke instruction 4 on a pinned program id so the event-authority bump, and therefore the sha256 verify-loop attempt count, is identical across runs; the number is that instruction's total, not a net-of-harness primitive):

Operation CU Notes
emit_receipt total (whole instruction) 3,586 bind (4 accounts incl. one PDA verify) + one counter write + self-CPI + generated sink
inner sink execution alone 279 entrypoint + dispatch + signer check + PDA address pin
top-level forgery refusal 112 marker instruction with an unsigned authority dies at the signer check

The total moved 3,534 → 3,586 (+52) later on 2026-07-10 when the instruction-ambient touch log landed: hopper-smoke enables the touch-map feature crate-wide, so this instruction's one get_mut records its write footprint (plus the per-context log reset) even though the context never emits a map. That +52 is the measured price of write-observability in a touch-map-enabled crate; programs that do not enable the feature pay none of it.

Two structural notes, both disclosed wherever the feature is claimed:

  • The dominant cost is the CPI itself (the ~1k-CU-class invoke plus the nested entrypoint). The log-based emit_event (240 CU) remains the cheap tier when log truncation is acceptable.
  • The generated event path verifies its event-authority PDA at runtime; it does not automatically substitute a canonical_pda! constant. The dated fixture below used a sha256 compare loop: ~148 CU per attempt (the 256-attempt exhaustion below ÷ 256), attempt count = 256 − bump. This smoke program's authority sits at the first attempt and its verify measures 171 CU. bind() fuses validation and bump capture into exactly ONE derivation (measured: the fuse took this instruction from 3,705 to 3,534 CU). A failed bind with a wrong authority address exhausts the loop: ~37.9k CU on the failing (attacker-paid) transaction.

Budgeting rule of thumb

For client-side budgeting (SetComputeUnitLimit), the schema field cu_estimate is author-supplied and must come from a measured worst-case run of the actual instruction, not from summing this table, per-primitive nets do not include the entrypoint/dispatch cost of your real program, and composition effects are not additive at single-CU resolution.

What this lab does not measure

The following axes have no current measured per-primitive figure. Their old April 2026 numbers came from a different method (validator-log deltas on Solana 2.1) and are retired; see the appendix. Until the lab grows discs for them, do not budget from this page:

  • Token / Mint constraint reads (token::mint, mint::decimals, …)
  • Token-2022 extension TLV walks (extensions::*)
  • PDA derivation (seeds/bump paths). A supplied or stored bump selects one derivation; bare bump and seeds_fn perform a canonical search at runtime. Typed program-owned accounts can use the optimized SHA-256 check for a selected bump, but ownership and a matching address do not prove that the stored byte is canonical. An eligible empty, non-signer init with bump = <arg> can omit Hopper's separate derivation pre-check because the signed creation CPI must establish the account's signer privilege. The runtime still validates the PDA signer seeds; this is not a hash-free transaction. Signer or non-empty accounts retain the separate check. Literal seeds can use hopper::canonical_pda! to search and curve-check on the build host, then compare the emitted address on chain. const_pda! only hashes a supplied bump; it does not prove canonicality or curve membership. An address comparison still executes instructions; removing derivation does not make the instruction cost zero CU. Use current, pinned whole-instruction measurements when choosing a path; this lab provides no isolated PDA cost or universal speedup ratio.
  • Logging macro variants (hopper_log!, msg! with formatting, hopper_emit_cpi!). The measured anchor points from this run: one log-class syscall bills 100 CU base (the empty bracket measures 101 CU), and a 32-byte sol_log_data event measures 240 CU net (disc 9).
  • CPI tiers (invoke, invoke_signed, DynCpi). Measured end-to-end context: the router parity lab (BENCHMARKS.md) prices full router hops, each including one CPI to a mock AMM, at ~1.5k CU per hop.

This lab has no isolated rows for AccountView::address(), lamports(), or data_len(). They inline field reads, which does not by itself prove zero CU. raw_ref() also acquires an account borrow and checks the typed view's size; it is not merely a pointer read. Measure these operations in the actual instruction that uses them.

How to measure yourself

From the sibling hopper-bench repo (exact protocol of this page):

cargo build-sbf --manifest-path hopper-bench/Cargo.toml
cargo run --manifest-path primitive-bench/Cargo.toml --release -- \
  --out-dir results/primitive-bench-$(date +%F)

From this framework workspace, hopper profile bench runs the same primitive lab, and hopper profile elf gives static size analysis, SBF instruction-count estimates, ELF section summaries, and a flamegraph for any compiled .so.

Appendix: retired April 2026 validator-log figures

Everything below was measured (or estimated) with the retired method, raw sol_log_compute_units deltas in solana-test-validator 2.1 transaction logs, without bracket-overhead subtraction, or from first-principles syscall counting. These figures are not comparable to the net column above and must not be quoted or budgeted against. They are kept only so old quotes stay traceable. The per-primitive April figures (e.g. check_keys_eq ~40, receipts ~50/~80/~150) are recorded row-by-row next to their measured replacements in BENCHMARKS.md.

Retired account-access estimates: try_borrow ~2, pod_from_bytes ~3, Account::load ~5, load_mut ~7, <field>_segment_ref ~4.

Retired token/mint constraint estimates: token::mint/token::authority ~8, token::token_program ~4, mint::authority/mint::freeze_authority ~12, mint::decimals ~3, associated_token::mint ~60.

Retired Token-2022 TLV estimates: single-extension scans ~35–50 depending on extension and compare width.

Retired PDA estimates: inferred bump ~1,500–3,000; stored bump ~25.

Retired dispatch estimates: ~3 per single-byte match arm.

Retired logging estimates: hopper_log! literal ~100; label+u64 ~200; msg! formatted ~300–600; emit! ~250; hopper_emit_cpi! ~1,500.

Retired CPI estimates: invoke ~600 + recipient; invoke_signed ~750 + recipient; signer-seed threading ~150.

Retired baseline estimates: empty sol_log_ ~100; sol_log_64_ ~100; sol_invoke_signed_c to a no-op recipient ~600.

The April-era comparative multiples that used to accompany these tables (unmeasured per-constraint multipliers, for example) were never re-measured under the current method and are withdrawn with them; measured cross-framework numbers live in BENCHMARKS.md.