Self-describing transactions
Opt-in byte-range touch maps that let a confirmed transaction name the exact fields it wrote, decoded live from devnet signatures.
Hopper's borrow ledger names byte ranges, not whole accounts. Touch
maps put that granularity on the wire: a context that opts in emits one
compact record per write, and hopper tx explain decodes a confirmed
transaction back into the exact fields it wrote. No other 2026 framework
can express this. Pinocchio-derived substrates track one borrow byte per
account, and Anchor v2's finest write tracking is an account-index mask.
The claim, scoped the way we make it: Hopper transactions can explain
their own state effects. Not "every Hopper transaction does". Emission
is per-context opt-in by design, because every record costs a
sol_log_data and we do not hide CU costs in defaults.
Opting in
#[derive(Accounts)]
#[accounts(strict_writes, emit_touch_map)]
pub struct Withdraw<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut(balance), has_one = authority)]
pub vault: Account<'info, Vault>,
}
On the wire, the map is one sol_log_data record, magic 0x7A,
version 0x01, then (slot, offset, size, R/W) entries, which
hopper tx explain decodes from the transaction's log stream.
strict_writescompiles the declaredmut/mut(seg)structure into an enforced write-set: the published ranges ARE the enforceable ones, so published-equals-enforced holds by construction.emit_touch_mapemits the observed write records on success. Emission is Ok-only: a handler that touches state and then errors emits zero records (host-verified in the e2e suite).
What a decode looks like
$ hopper tx explain 4uc6v9hC...51SzKnfP
W slot 1 (vault) [48..56) -> Vault.balance
That is a real devnet transaction: the smoke program's withdraw
(program 2YPBvKJ8h37bUEFBrmytzNuKfUJ5Q2o2tkTiqRCZdjme, signature
4uc6v9hCR6R2tRxGFJYgFbyPxnhWQZV4cL6VCfLnWNbPe2qAK79aX7HgkhuVfJaS4w6FjSoV7hDit6bn51SzKnfP)
names the exact 8 bytes it wrote and the field they belong to.
The wrapper blind spot, closed
Until 2026-07-10, only segment-path writes were recorded. A
whole-account get_mut() borrow bypassed the map. The touch log now
lives in instruction-ambient storage (reserved VM heap on SBF), so
whole-account borrows record from inside AccountView::load_mut itself:
wrapper get_mut, raw load_mut, even borrows with no Context in
reach. Proven on-chain the same day. The wrapper-path
bump_whole_vault decodes live as W slot 1 [0..76) (signature
4yTv8gKBktSAg4sNiZgCwEnh6CkgHA89NA1r2ZrEAcNamak1xsu8u4veTYJ88wMQMaPmwsyx6jJnnmce9R2oyXMB).
Whole-account records are honest-but-coarse: [0..len) is the borrow's
actual grant, versus the segment path's field-precise 8-byte record.
What it costs, disclosed
Enabling the touch-map feature crate-wide costs a measured +52 CU
on the smoke crate's event_cpi demo instruction (one get_mut record
plus the per-context log reset). That is the price of
write-observability in a touch-map-enabled crate, and programs that do
not enable the feature pay none of it. Per-record cost is one
sol_log_data-class syscall; see the CU costs page.
Scope, plainly
- Emission is per-context opt-in; do not read "every Hopper tx explains itself" into this page.
- Raw byte borrows (
try_borrow_mut) and lamport-only changes are not data-map records. mutation_complete(thestrict_writes/lamport gate) is scoped too: substrate escape hatches (substrate::batch::transfer_lamports, theunsafeunchecked CPI tier) sit outside the gate. Sealevel'swritablebit still enforces underneath, so the failure mode is a failed transaction, never fund loss.
Why byte ranges matter (the honest version)
No SIMD prices sub-account granularity today. Agave charges a flat
300 CU per writable account (count-based, WRITE_LOCK_UNITS), and the
write-lock-pricing SIMDs closed unmerged. What Solana DOES reward is
declaring less: SIMD-0186 (Accepted) prices loaded bytes, and a
declared-writable account that a proven write-set shows is never
mutated can be demoted to read-only by the client, dropping a write
lock and 300 CU of block cost on the shipped scheduler
(InstructionDescriptor::effective_writable, demonstrated live by the
Sentinel showcase). Byte-range write-sets are the vocabulary Solana
does not have yet: a program that publishes them is scheduler-legible
in a way account-granular frameworks cannot retrofit without forking
the account type they all share, and only a framework with an
enforced per-byte write policy can supply the data truthfully. The end-to-end proof path lives in
examples/hopper-smoke/tests/touch_map_e2e.rs (compiled .so, captured
log stream, decoded through the same wire-format check hopper tx explain applies); the live signatures are on the
Benchmarks page, and the decoder is described in
Manifests and tx tooling.
