Expand description
Declared write sets enforced at borrow acquisition.
Sealevel’s account model stops at one bit of write granularity: the
transaction-level writable flag covers the entire account. This
module extends that to byte-range granularity: an instruction
declares the exact ranges it is allowed to write, and the
Context rejects any write borrow outside
the declared set at acquisition time, before a byte is exposed mutably.
§How it composes
#[hopper::context(strict_writes)]compiles the context’s declaredmut/mut(seg, ...)attributes into astaticWritePolicyand installs it duringbind(). The descriptors are constant data, and the policy scans that slice at each write acquisition.- Every Context-mediated write path is gated: segment writes
(
segment_mut,segment_mut_const,segment_mut_typed,split_segments_mut), whole-account typed loads (load_mut), and the raw escape hatches (raw_mut,as_mut_ptr). - A whole-account borrow claims
[0, data_len), so under a policy that declares only field ranges,load_mut/as_mut_ptrare refused and the handler must use the declared segment accessors. That is the discipline the policy exists to enforce. - Paired with the
touch-mapfeature, the declared set can be compared against the actual footprint in tests: declared-vs-actual write verification from the emitted touch records.
§The lamport dimension
Byte ranges cover data; Sealevel writability also covers
lamport mutation (close, top-up, transfer). A policy built with
WritePolicy::with_lamports declares that second dimension: the
listed account indices may have their lamports mutated; other
account’s lamport mutation is refused. Because lamport operations
flow through AccountView (not Context), enforcement uses an
instruction-scoped ambient gate (try_install_lamport_gate)
consulted by the runtime’s lamport choke points: the
native_boundary try_set_lamports/close funnel used by the
runtime and hopper-core lifecycle helpers, and the
validated CPI tiers’ writable-meta construction (a writable CPI
hand-off is unbounded delegation of both dimensions).
The gate stores address values copied at install time rather than a
pointer into the account slice. Checks compare the address of
the live view being mutated against those values. A leaked guard
(mem::forget) therefore leaves an observable stale value policy
installed (fail-closed for unknown addresses) rather than any form
of memory unsafety; see the gate section below for the full
contract, including the loud fail-closed install errors
(0xD1__ page).
§Enforcement boundary
The policy governs access through Context (data ranges) and
through the runtime’s AccountView/CPI surface (lamports). Generated
#[hopper::context] code and the documented safe APIs use those paths.
Direct substrate access (hopper_native calls such
as batch::transfer_lamports, or try_borrow_mut on the raw
backend view) and the unsafe unchecked CPI tier are outside the
governed surface, exactly like the documented raw-pointer escape
hatches; they are visible in review and lintable. The Sealevel
writable flag is still enforced underneath in all cases.
For the common no-CPI lamport move the escape hatch has a
first-class gated alternative:
crate::lamports::transfer_lamports (re-exported at the crate
root and through hopper::prelude; lamports(...) contexts also
expose it as a generated ctx.transfer_lamports(..) method) runs
the substrate helper’s exact arithmetic through the
native_boundary funnel, checking both sides against the gate
before any balance changes, so gated programs retain the declared lamport
checks without paying for a System CPI.
Structs§
- Lamport
Gate Guard - RAII installation of the lamport gate for one bound instruction.
- Parametric
Write Range - A runtime-selected fixed-size cell inside a statically declared column.
- Write
Policy - Declared write-set for one instruction.
- Write
Range - One allowed write range on one instruction account.
Enums§
- Lamport
Policy - The lamport-write dimension of a
WritePolicy.
Constants§
- AMBIENT_
GATE_ ARG_ CAPACITY - Maximum number of invocation-resolved selector values retained by one
ambient gate. This is the same bounded ABI used by
Context. - AMBIENT_
GATE_ TOO_ MANY_ ARGUMENTS - Install refused because invocation-resolved exact-cell selectors exceed the bounded ambient ABI.
- AMBIENT_
GATE_ UNGUARDED_ BUILD - Install refused: this build carries the
unguarded-raw-surfacessize opt-out, but the policy declares data write ranges (fixed or parametric), governance the opt-out build cannot enforce on the rawAccountViewsurfaces. Refusing at install keeps the bypass loud on EVERY tier: macro-bound strict contexts are already a compile error in such builds, and this is the runtime fence for hand-rolled installs. Lamports-only policies still install (their dimensions stay enforced). - LAMPORT_
GATE_ CAPACITY - Per-gate account capacity: the runtime’s transaction account bound.
An instruction can never carry more accounts than the transaction
that contains it, so a gate over one instruction’s slice always
fits; anything larger is refused loudly at install
(
LAMPORT_GATE_TOO_MANY_ACCOUNTS). - LAMPORT_
GATE_ CONTENDED - Install refused (host fallback tier only): another still-active gate
occupies the process-global single-slot store. On
no_stdmulti-threaded hosts without thethread-local-registryfeature the gate cannot attribute nesting to a thread, so a concurrent second install is refused loudly, never silently shared with, or allowed to corrupt, the gate another thread installed. Enablethread-local-registry(or run gated instructions one at a time) to lift this. - LAMPORT_
GATE_ DEPTH - Concurrent gates per tier: Solana’s nested-CPI depth budget (the
runtime caps the instruction stack at 5 = one top-level + 4 nested
CPI levels). On-chain every CPI level runs in a fresh VM whose
writable data is re-initialized, so a single store only ever sees
the nested binds of one handler frame; on host, hopper’s CPI
syscall is a no-op (no nested handler dispatch), so concurrent
gates arise only from manually nested binds. Four slots cover both
with room to spare, and exhaustion fails closed loudly
(
LAMPORT_GATE_DEPTH_EXCEEDED) rather than corrupting a live gate. - LAMPORT_
GATE_ DEPTH_ EXCEEDED - Install refused: all
LAMPORT_GATE_DEPTHslots on this tier are occupied by still-active (or leaked) gates. Nesting deeper than the Solana invoke-depth budget, or leaking guards viamem::forget, exhausts the store; the install fails closed instead of evicting a live gate. - LAMPORT_
GATE_ INSTALL_ ERROR_ PAGE - Error page for lamport-gate installation failures (
0xD1__). - LAMPORT_
GATE_ TOO_ MANY_ ACCOUNTS - Install refused: the instruction’s account slice has more accounts
than
LAMPORT_GATE_CAPACITY. The gate refuses loudly rather than silently truncating the governed set (a truncated gate would treat the overflow accounts as foreign, surprising, and wrong the moment one of them was declared). - RAW_
SURFACES_ GUARDED - Whether the public raw
AccountViewsurfaces are governed by the ambient write gate in this build (the default).falseonly under theunguarded-raw-surfacesopt-out;strict_writescodegen const-asserts this istrue, making the opt-out impossible to combine with a bound strict context. - WRITE_
POLICY_ VIOLATION_ PAGE - Error page for write-policy violations.
Traits§
- Exact
Cell Selector - A selector whose runtime value and wire representation are guaranteed to agree with Hopper’s exact-cell Effect ABI.
Functions§
- install_
lamport_ gate - Infallible wrapper around
try_install_lamport_gate, kept for call sites that predate the fallible API. - lamport_
gate_ active - Whether a lamport gate is currently installed on this tier (diagnostic/testing). Counts shadowed and leaked (stale) gates.
- try_
install_ ambient_ gate_ with_ args - Install the complete instruction-scoped ambient mutation gate, including invocation-resolved selector values for parametric exact-cell policies.
- try_
install_ lamport_ gate - Install the instruction-scoped lamport gate for
accountsunderpolicy, the fallible entry point. - write_
policy_ violation - Build the write-policy-violation error for an account index.