Skip to main content

Module write_policy

Module write_policy 

Source
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 declared mut / mut(seg, ...) attributes into a static WritePolicy and installs it during bind(). 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_ptr are refused and the handler must use the declared segment accessors. That is the discipline the policy exists to enforce.
  • Paired with the touch-map feature, 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§

LamportGateGuard
RAII installation of the lamport gate for one bound instruction.
ParametricWriteRange
A runtime-selected fixed-size cell inside a statically declared column.
WritePolicy
Declared write-set for one instruction.
WriteRange
One allowed write range on one instruction account.

Enums§

LamportPolicy
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-surfaces size opt-out, but the policy declares data write ranges (fixed or parametric), governance the opt-out build cannot enforce on the raw AccountView surfaces. 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_std multi-threaded hosts without the thread-local-registry feature 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. Enable thread-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_DEPTH slots on this tier are occupied by still-active (or leaked) gates. Nesting deeper than the Solana invoke-depth budget, or leaking guards via mem::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 AccountView surfaces are governed by the ambient write gate in this build (the default). false only under the unguarded-raw-surfaces opt-out; strict_writes codegen const-asserts this is true, making the opt-out impossible to combine with a bound strict context.
WRITE_POLICY_VIOLATION_PAGE
Error page for write-policy violations.

Traits§

ExactCellSelector
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 accounts under policy, the fallible entry point.
write_policy_violation
Build the write-policy-violation error for an account index.