Skip to main content

hopper_runtime/
context.rs

1//! Execution context for Hopper programs.
2//!
3//! `Context` is the canonical execution object that Hopper handlers receive.
4//! It provides structured access to the program_id, accounts, and instruction
5//! data, with indexed access and validation helpers.
6//!
7//! Keep it boring: `Context` is the container for accounts, instruction data,
8//! and the instruction-scoped segment borrow registry. `AccountView` owns the
9//! actual access operations.
10
11use crate::account::AccountView;
12use crate::address::Address;
13use crate::audit::AccountAudit;
14use crate::error::ProgramError;
15use crate::layout::LayoutContract;
16use crate::segment_borrow::SegmentBorrowRegistry;
17use crate::ProgramResult;
18
19const MAX_PARAMETRIC_WRITE_ARGS: usize = 8;
20
21/// Execution context for a Hopper instruction handler.
22///
23/// Wraps the program_id, account slice, and instruction data into a single
24/// object with structured access patterns.
25///
26/// # Authored flow
27///
28/// ```ignore
29/// pub fn deposit(ctx: &Context, amount: u64) -> ProgramResult {
30///     let authority = ctx.account(0)?;
31///     let vault = ctx.account(1)?;
32///
33///     authority.require_signer()?;
34///     vault.require_writable()?;
35///     vault.check_disc(1)?;
36///
37///     let mut state = vault.load_mut::<VaultState>()?;
38///     state.balance = state.balance.checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?;
39///     Ok(())
40/// }
41/// ```
42pub struct Context<'a> {
43    /// The program's own address.
44    pub program_id: &'a Address,
45    /// All accounts passed to this instruction.
46    accounts: &'a [AccountView<'a>],
47    /// Raw instruction data (past the discriminator byte, if applicable).
48    pub instruction_data: &'a [u8],
49    /// Segment-level borrow tracking for fine-grained access control.
50    ///
51    /// Enables safe concurrent mutable access to non-overlapping regions
52    /// of the same account while keeping typed access under Hopper's borrow
53    /// registry.
54    /// Prefer the `borrows()` / `borrows_mut()` accessors in new code.
55    pub(crate) segment_borrows: SegmentBorrowRegistry,
56    /// Declared write-set enforced on every Context-mediated write
57    /// acquire (write-policy enforcement). `None` (the default) means no policy:
58    /// writes are governed by the Sealevel `writable` flag and the
59    /// borrow system alone, with zero added cost beyond one pointer
60    /// compare per write acquire.
61    write_policy: Option<&'static crate::write_policy::WritePolicy>,
62    /// Small invocation-local values used to resolve parametric cell rules.
63    /// Kept inline to avoid heap allocation and large SBF stack copies, and
64    /// left uninitialized until a parametric policy is installed: only the
65    /// first `parametric_write_arg_count` entries are ever read, and those
66    /// are written by `set_parametric_write_policy` first. Zeroing the array
67    /// in `Context::new` cost four stores on every instruction of every
68    /// program for a feature most contexts never use.
69    parametric_write_args: [core::mem::MaybeUninit<u32>; MAX_PARAMETRIC_WRITE_ARGS],
70    parametric_write_arg_count: u8,
71}
72
73impl<'a> Context<'a> {
74    /// Create a new context from the entrypoint parameters.
75    #[inline(always)]
76    pub fn new(
77        program_id: &'a Address,
78        accounts: &'a [AccountView<'a>],
79        instruction_data: &'a [u8],
80    ) -> Self {
81        // Start-of-instruction reset for the instruction-AMBIENT touch
82        // log (it lives outside this struct so `AccountView`-level
83        // borrows record with no Context in reach). On SBF this is
84        // redundant with per-invocation heap zeroing; on hosts it is
85        // what scopes the log to this instruction.
86        #[cfg(feature = "touch-map")]
87        crate::segment_borrow::touch_log::reset();
88        Self {
89            program_id,
90            accounts,
91            instruction_data,
92            segment_borrows: SegmentBorrowRegistry::new(),
93            write_policy: None,
94            parametric_write_args: [core::mem::MaybeUninit::uninit(); MAX_PARAMETRIC_WRITE_ARGS],
95            parametric_write_arg_count: 0,
96        }
97    }
98
99    /// The installed parametric write arguments, exactly the entries
100    /// `set_parametric_write_policy` wrote.
101    #[inline(always)]
102    fn parametric_write_args(&self) -> &[u32] {
103        let count = self.parametric_write_arg_count as usize;
104        // SAFETY: `parametric_write_arg_count` is only ever raised by
105        // `set_parametric_write_policy`, which initializes exactly that many
106        // leading entries before storing the count; `MaybeUninit<u32>` has
107        // the layout of `u32`.
108        unsafe {
109            core::slice::from_raw_parts(self.parametric_write_args.as_ptr() as *const u32, count)
110        }
111    }
112
113    /// Install a declared write policy (write-policy enforcement).
114    ///
115    /// From this point on, **every** Context-mediated write acquire,
116    /// segment writes, whole-account `load_mut`, and the raw escape
117    /// hatches `raw_mut` / `as_mut_ptr`, must be fully contained in one
118    /// of the policy's declared ranges or it fails with
119    /// `Custom(0xD000 | account_index)` before any byte is written.
120    /// Whole-account paths claim `[0, data_len)`, so a policy that
121    /// declares only field ranges forces handlers onto the declared
122    /// segment accessors.
123    ///
124    /// `#[hopper::context(strict_writes)]` compiles the context's
125    /// `mut` / `mut(seg, ...)` declarations into a `static` policy
126    /// and installs it during `bind()`. Macro binding also installs the
127    /// ambient gate, which governs supported direct [`AccountView`] mutation
128    /// APIs. Calling only this setter by hand governs Context-mediated
129    /// acquisitions; it does not install that ambient gate. Unsafe raw-memory
130    /// writes remain the caller's responsibility.
131    #[inline(always)]
132    pub fn set_write_policy(&mut self, policy: &'static crate::write_policy::WritePolicy) {
133        self.write_policy = Some(policy);
134        self.parametric_write_arg_count = 0;
135    }
136
137    /// Install a declared write policy and bind the invocation values used by
138    /// its [`ParametricWriteRange`](crate::write_policy::ParametricWriteRange)s.
139    #[inline]
140    pub fn set_parametric_write_policy(
141        &mut self,
142        policy: &'static crate::write_policy::WritePolicy,
143        args: &[u32],
144    ) -> ProgramResult {
145        if args.len() > MAX_PARAMETRIC_WRITE_ARGS {
146            return Err(ProgramError::InvalidInstructionData);
147        }
148        self.write_policy = Some(policy);
149        for (slot, value) in self.parametric_write_args.iter_mut().zip(args) {
150            slot.write(*value);
151        }
152        self.parametric_write_arg_count = args.len() as u8;
153        Ok(())
154    }
155
156    /// The installed write policy, if any.
157    #[inline(always)]
158    pub fn write_policy(&self) -> Option<&'static crate::write_policy::WritePolicy> {
159        self.write_policy
160    }
161
162    /// Return the first byte of a recorded write touch that falls outside the
163    /// installed invocation-resolved policy.
164    ///
165    /// This is the audit counterpart to the acquire-time gate. It checks the
166    /// union of static ranges and selected parametric cells because the touch
167    /// ledger may coalesce adjacent authorized acquires. With no installed
168    /// policy, or an account index that cannot be represented on the wire, it
169    /// fails closed by returning the touch's first byte.
170    #[inline]
171    pub fn first_unauthorized_write_byte(
172        &self,
173        index: usize,
174        offset: u32,
175        size: u32,
176    ) -> Option<u64> {
177        let Some(policy) = self.write_policy else {
178            return Some(offset as u64);
179        };
180        if index > u8::MAX as usize {
181            return Some(offset as u64);
182        }
183        policy.first_unauthorized_byte_with_args(
184            index as u8,
185            offset,
186            size,
187            self.parametric_write_args(),
188        )
189    }
190
191    /// Gate a proposed write acquire behind the installed policy.
192    /// No policy installed = allowed (one branch on a `None`).
193    #[inline(always)]
194    fn check_write_policy(&self, index: usize, offset: u32, size: u32) -> ProgramResult {
195        if let Some(policy) = self.write_policy {
196            // Account indices are u8 on the wire; an index beyond 255
197            // can never have been declared, so refuse it outright rather
198            // than truncating into a potential false allow.
199            if index > u8::MAX as usize {
200                return Err(crate::write_policy::write_policy_violation(u8::MAX));
201            }
202            policy.check_write_with_args(
203                index as u8,
204                offset,
205                size,
206                self.parametric_write_args(),
207            )?;
208        }
209        Ok(())
210    }
211
212    /// Program ID.
213    #[inline(always)]
214    pub fn program_id(&self) -> &Address {
215        self.program_id
216    }
217
218    /// Raw instruction data.
219    #[inline(always)]
220    pub fn instruction_data(&self) -> &'a [u8] {
221        self.instruction_data
222    }
223
224    /// Get an account by index.
225    #[inline(always)]
226    pub fn account(&self, index: usize) -> Result<&'a AccountView<'a>, ProgramError> {
227        self.accounts
228            .get(index)
229            .ok_or(ProgramError::NotEnoughAccountKeys)
230    }
231
232    /// Get an account by index (mutation-intent variant).
233    ///
234    /// Functionally identical to `account()` since `AccountView` uses
235    /// interior mutability for data access (`overlay_mut`, `load_mut`,
236    /// `try_borrow_mut`). The distinct name signals that the caller
237    /// intends to write through the returned reference.
238    #[inline(always)]
239    pub fn account_mut(&self, index: usize) -> Result<&'a AccountView<'a>, ProgramError> {
240        self.accounts
241            .get(index)
242            .ok_or(ProgramError::NotEnoughAccountKeys)
243    }
244
245    /// Get the total number of accounts.
246    #[inline(always)]
247    pub fn num_accounts(&self) -> usize {
248        self.accounts.len()
249    }
250
251    /// Get all accounts as a slice.
252    #[inline(always)]
253    pub fn accounts(&self) -> &'a [AccountView<'a>] {
254        self.accounts
255    }
256
257    /// Access the instruction-scoped segment borrow registry.
258    #[inline(always)]
259    pub fn borrows(&self) -> &SegmentBorrowRegistry {
260        &self.segment_borrows
261    }
262
263    /// Mutably access the instruction-scoped segment borrow registry.
264    #[inline(always)]
265    pub fn borrows_mut(&mut self) -> &mut SegmentBorrowRegistry {
266        &mut self.segment_borrows
267    }
268
269    /// Inspect the instruction account slice for duplicate aliases.
270    #[inline(always)]
271    pub fn audit_accounts(&self) -> AccountAudit<'a> {
272        AccountAudit::new(self.accounts)
273    }
274
275    /// Visit every distinct `(account, offset, size, R/W)` range this
276    /// instruction has touched so far (`touch-map` feature, innovation
277    /// touch-map). The log is cumulative, RAII lease releases do not remove
278    /// records; so calling this at the end of a handler yields the
279    /// instruction's segment-level footprint in first-touch order.
280    /// Pair with [`touch_map_overflowed`](Self::touch_map_overflowed).
281    #[cfg(feature = "touch-map")]
282    #[inline]
283    pub fn for_each_touch<F: FnMut(&crate::segment_borrow::SegmentBorrow)>(&self, f: F) {
284        self.segment_borrows.for_each_touch(f)
285    }
286
287    /// Number of distinct touch records captured (`touch-map` feature).
288    #[cfg(feature = "touch-map")]
289    #[inline(always)]
290    pub fn touch_map_len(&self) -> usize {
291        self.segment_borrows.touch_map_len()
292    }
293
294    /// Whether the touch log overflowed and is partial (`touch-map`
295    /// feature).
296    #[cfg(feature = "touch-map")]
297    #[inline(always)]
298    pub fn touch_map_overflowed(&self) -> bool {
299        self.segment_borrows.touch_map_overflowed()
300    }
301
302    /// Encode this instruction's touch map into the versioned v1 wire
303    /// format (`touch-map` feature). Pure and allocation-free: returns
304    /// the fixed-capacity buffer plus the number of valid bytes. The
305    /// format is documented in [`crate::segment_borrow`] (magic `0x7A`,
306    /// version `0x01`, flags, count, then 9-byte records).
307    ///
308    /// Each touched `(account, offset, size, R/W)` range is resolved to
309    /// the account's slot index in this context's account list. A touch
310    /// whose address is not among the instruction accounts (should be
311    /// impossible, every touch originates from an account in this
312    /// context) or whose slot exceeds `u8::MAX` is skipped and reported
313    /// via flag bit1 rather than mis-attributed. Flag bit0 carries the
314    /// touch log's overflow state so partial maps are honestly marked.
315    #[cfg(feature = "touch-map")]
316    pub fn encode_touch_map(
317        &self,
318    ) -> (
319        [u8; crate::segment_borrow::TOUCH_MAP_MAX_ENCODED_LEN],
320        usize,
321    ) {
322        use crate::segment_borrow::{AccessKind, TouchMapRecord, MAX_TOUCH_RECORDS};
323        let empty = TouchMapRecord {
324            slot: 0,
325            offset: 0,
326            size: 0,
327            write: false,
328        };
329        let mut records = [empty; MAX_TOUCH_RECORDS];
330        let mut n = 0usize;
331        let mut skipped = false;
332        self.segment_borrows.for_each_touch(|t| {
333            let slot = self
334                .accounts
335                .iter()
336                .position(|view| view.address().as_array() == t.key.as_array());
337            match slot {
338                // `n < MAX_TOUCH_RECORDS` always holds: the touch log and
339                // the record array share the same capacity.
340                Some(i) if i <= u8::MAX as usize && n < MAX_TOUCH_RECORDS => {
341                    records[n] = TouchMapRecord {
342                        slot: i as u8,
343                        offset: t.offset,
344                        size: t.size,
345                        write: t.kind == AccessKind::Write,
346                    };
347                    n += 1;
348                }
349                _ => skipped = true,
350            }
351        });
352        crate::segment_borrow::encode_touch_map(
353            &records[..n],
354            self.segment_borrows.touch_map_overflowed(),
355            skipped,
356        )
357    }
358
359    /// Emit this instruction's touch map as a single `sol_log_data`
360    /// record (`touch-map` feature), making the transaction
361    /// self-describing: `hopper tx explain` and the generated TypeScript
362    /// `decodeHopperTouchMap` helper can reconstruct the instruction's
363    /// field-level state effects from the signature alone.
364    ///
365    /// Call at the end of a handler, after the last state access, the
366    /// touch log is cumulative, so this snapshots everything touched so
367    /// far. Off-chain (`cfg(not(target_os = "solana"))`) the syscall is a
368    /// no-op; use [`encode_touch_map`](Self::encode_touch_map) to test
369    /// the encoded bytes.
370    #[cfg(feature = "touch-map")]
371    pub fn emit_touch_map(&self) {
372        let (buf, len) = self.encode_touch_map();
373        hopper_native::log::log_data(&[&buf[..len]]);
374    }
375
376    /// Opt-in post-handler epilogue: finalize a successful instruction by
377    /// emitting its touch map, making the transaction self-describing
378    /// (touch-map support).
379    ///
380    /// This is the single hook the `#[hopper::context(emit_touch_map)]`
381    /// opt-in drives, so a developer gets the self-describing touch-map
382    /// record on the golden path without hand-writing the `sol_log_data`
383    /// syscall. The generated **dispatcher**; which alone sees the
384    /// handler's `Result`, calls this on the handler's **Ok** path only,
385    /// guarded by the context's `EMIT_TOUCH_MAP` const:
386    ///
387    /// ```ignore
388    /// handler(Ctx::bind(&mut ctx)?, ..)?;      // Err short-circuits here
389    /// if Ctx::EMIT_TOUCH_MAP { ctx.finish_with_touch_map(); }
390    /// Ok(())
391    /// ```
392    ///
393    /// It is deliberately NOT called from a `Drop` for the bound context:
394    /// Rust runs drop glue on every scope exit, including `?`/`Err`
395    /// returns, and a `Drop` cannot observe the handler's `Result`, so it
396    /// would emit a misleading record advertising Write ranges for a
397    /// failed, rolled-back instruction (adversarial review, failed-instruction emission regression).
398    /// Routing on the Ok path makes the record fire exclusively on
399    /// success. It is also deliberately routed through a runtime helper
400    /// (rather than a macro-emitted `#[cfg]`) so the **feature gate lives
401    /// here**: the macro always emits the same call, and this method's two
402    /// `cfg` bodies decide whether it does anything.
403    ///
404    /// With the `touch-map` feature **on** it forwards to
405    /// [`emit_touch_map`](Self::emit_touch_map), one `sol_log_data`
406    /// record on-chain, a no-op off-chain. With the feature **off** the
407    /// [zero-cost sibling](#method.finish_with_touch_map) is compiled
408    /// instead, so the generated call emits nothing and costs nothing.
409    #[cfg(feature = "touch-map")]
410    #[inline]
411    pub fn finish_with_touch_map(&self) {
412        self.emit_touch_map();
413    }
414
415    /// Zero-cost sibling of
416    /// [`finish_with_touch_map`](Self::finish_with_touch_map), compiled
417    /// when the `touch-map` feature is off.
418    ///
419    /// Keeps the macro-generated opt-in epilogue call compiling on builds
420    /// that never enabled the touch-map machinery, and emits nothing.
421    /// This is what makes "opt-in present but feature off" produce no
422    /// `sol_log_data` record and pay no compute for it.
423    #[cfg(not(feature = "touch-map"))]
424    #[inline(always)]
425    pub fn finish_with_touch_map(&self) {}
426
427    /// Get the remaining accounts starting at `from`.
428    ///
429    /// NOTE (binary size): the slicing below goes through `get(..)`, never
430    /// `self.accounts[from..]`. A range index LLVM cannot statically bound
431    /// emits `slice_end_index_len_fail`, which *formats* its arguments and
432    /// links `Formatter::pad_integral`, `do_count_chars` and the integer
433    /// `Display` impls, ~3.7 KiB of `core::fmt`, into every Hopper
434    /// program's `.text`. These are `#[inline(always)]` hot-path helpers,
435    /// so one panicking index here taxes every program. Keep them `get`-based.
436    #[inline(always)]
437    pub fn remaining_accounts(&self, from: usize) -> &'a [AccountView<'a>] {
438        let accounts: &'a [AccountView<'a>] = self.accounts;
439        accounts.get(from..).unwrap_or(&[])
440    }
441
442    /// Get remaining accounts in strict duplicate-rejecting mode.
443    #[inline(always)]
444    pub fn remaining_accounts_strict(
445        &self,
446        from: usize,
447    ) -> crate::remaining::RemainingAccounts<'a> {
448        let accounts: &'a [AccountView<'a>] = self.accounts;
449        let declared_end = from.min(accounts.len());
450        crate::remaining::RemainingAccounts::strict(
451            accounts.get(..declared_end).unwrap_or(&[]),
452            self.remaining_accounts(from),
453        )
454    }
455
456    /// Get remaining accounts in duplicate-preserving passthrough mode.
457    #[inline(always)]
458    pub fn remaining_accounts_passthrough(
459        &self,
460        from: usize,
461    ) -> crate::remaining::RemainingAccounts<'a> {
462        let accounts: &'a [AccountView<'a>] = self.accounts;
463        let declared_end = from.min(accounts.len());
464        crate::remaining::RemainingAccounts::passthrough(
465            accounts.get(..declared_end).unwrap_or(&[]),
466            self.remaining_accounts(from),
467        )
468    }
469
470    /// Get remaining accounts in strict mode and bind a sequential typed parser.
471    #[inline(always)]
472    pub fn remaining_accounts_typed(&self, from: usize) -> crate::remaining::RemainingTyped<'a> {
473        self.remaining_accounts_strict(from).typed()
474    }
475
476    /// Get remaining accounts in strict mode and bind a lazy indexed parser.
477    #[inline(always)]
478    pub fn remaining_accounts_lazy(&self, from: usize) -> crate::remaining::RemainingLazy<'a> {
479        self.remaining_accounts_strict(from).lazy()
480    }
481
482    /// Require at least `n` accounts are present.
483    #[inline(always)]
484    pub fn require_accounts(&self, n: usize) -> ProgramResult {
485        if self.accounts.len() >= n {
486            Ok(())
487        } else {
488            Err(ProgramError::NotEnoughAccountKeys)
489        }
490    }
491
492    /// Require all account addresses to be unique.
493    #[inline(always)]
494    pub fn require_unique_accounts(&self) -> ProgramResult {
495        self.audit_accounts().require_all_unique()
496    }
497
498    /// Require that no duplicated account is writable in this instruction.
499    #[inline(always)]
500    pub fn require_unique_writable_accounts(&self) -> ProgramResult {
501        self.audit_accounts().require_unique_writable()
502    }
503
504    /// Require that no duplicated account is used as a signer role.
505    #[inline(always)]
506    pub fn require_unique_signer_accounts(&self) -> ProgramResult {
507        self.audit_accounts().require_unique_signers()
508    }
509
510    /// Require at least `n` bytes of instruction data.
511    #[inline(always)]
512    pub fn require_data_len(&self, n: usize) -> ProgramResult {
513        if self.instruction_data.len() >= n {
514            Ok(())
515        } else {
516            Err(ProgramError::InvalidInstructionData)
517        }
518    }
519
520    // --- Whole-Layout Typed Access ----------------------------------
521
522    /// Validate-and-load the full typed layout for an account.
523    ///
524    /// This is the indexed shortcut for `ctx.account(idx)?.load::<T>()`.
525    /// It's the canonical "Tier A" access path: the runtime checks the
526    /// Hopper header, validates the data length, and projects the typed
527    /// view in one inlined call. no extra cost over the spelled-out form.
528    #[inline(always)]
529    pub fn load<T: LayoutContract + crate::Pod>(
530        &self,
531        index: usize,
532    ) -> Result<crate::Ref<'_, T>, ProgramError> {
533        self.account(index)?.load::<T>()
534    }
535
536    /// Validate-and-load a mutable typed layout for an account.
537    ///
538    /// Indexed shortcut for `ctx.account(idx)?.load_mut::<T>()`. The
539    /// returned guard holds the account-level exclusive borrow until
540    /// it drops.
541    ///
542    /// As a whole-account write borrow, this claims `[0, data_len)`:
543    /// under an installed [write policy](Self::set_write_policy) it
544    /// requires a whole-account allowance (a plain `mut` declaration),
545    /// and with the `touch-map` feature it lands in the instruction
546    /// touch map as a full-account write record.
547    #[inline(always)]
548    pub fn load_mut<T: LayoutContract + crate::Pod>(
549        &mut self,
550        index: usize,
551    ) -> Result<crate::RefMut<'_, T>, ProgramError> {
552        let view = self.account(index)?;
553        let data_len = view.data_len() as u32;
554        self.check_write_policy(index, 0, data_len)?;
555        // The touch-map footprint records inside `try_borrow_mut` (the
556        // choke point every mutable data borrow crosses), so this path
557        // no longer stamps it explicitly, one source of truth.
558        view.load_mut::<T>()
559    }
560
561    /// Cross-program load: validate ABI fingerprint without ownership check.
562    ///
563    /// Use this when reading an account whose owner is another program but
564    /// whose layout is published as a Hopper layout contract.
565    #[inline(always)]
566    pub fn load_cross_program<T: LayoutContract + crate::Pod>(
567        &self,
568        index: usize,
569    ) -> Result<crate::Ref<'_, T>, ProgramError> {
570        self.account(index)?.load_cross_program::<T>()
571    }
572
573    // --- Segment-Level Access (fine-grained borrow tracking) --------
574
575    /// Register a read borrow for a segment of an account and return a
576    /// [`SegRef<T>`](crate::SegRef) that releases both the account-level
577    /// byte guard **and** the segment registry lease on drop.
578    ///
579    /// `index` is the account index. `abs_offset` is the absolute byte
580    /// offset within the account data (including header bytes).
581    ///
582    /// # Type Safety
583    ///
584    /// `T` must implement `Pod` (substrate-level "safe to overlay on
585    /// raw bytes" contract: every bit pattern valid, align-1, no
586    /// padding, no interior pointers). Segment borrow tracking
587    /// prevents conflicting write access to the same byte range for
588    /// the guard's lifetime.
589    ///
590    /// # Canonical path
591    ///
592    /// Three variants exist for different offset sources:
593    ///
594    /// | Variant | Use when |
595    /// |---|---|
596    /// | [`segment_ref_typed`](Self::segment_ref_typed) (canonical) | Offset is a compile-time constant (the common case). The `const OFFSET: u32` generic becomes an immediate in the pointer arithmetic. |
597    /// | [`segment_ref_const`](Self::segment_ref_const) | Offset comes from a runtime [`crate::Segment`] value (dispatching dynamically between named fields). |
598    /// | `segment_ref` (this method) | Offset is fully dynamic (iterating segments in a loop, for example). |
599    ///
600    /// `#[hopper::context]`-generated accessors default to the canonical
601    /// typed path; reach for the others only when the use case
602    /// genuinely needs a runtime offset.
603    #[inline(always)]
604    pub fn segment_ref<'b, T: crate::Pod>(
605        &'b mut self,
606        index: usize,
607        abs_offset: u32,
608    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
609        let view = self
610            .accounts
611            .get(index)
612            .ok_or(ProgramError::NotEnoughAccountKeys)?;
613        view.segment_ref::<T>(
614            &mut self.segment_borrows,
615            abs_offset,
616            core::mem::size_of::<T>() as u32,
617        )
618    }
619
620    /// Borrow several disjoint typed sub-ranges of one account mutably at
621    /// the same time. See
622    /// [`AccountView::split_segments_mut`](crate::AccountView::split_segments_mut).
623    ///
624    /// ```ignore
625    /// let mut segs = ctx.split_segments_mut::<WireU64, 2>(
626    ///     vault_idx, [(BALANCE_OFF, 8), (NONCE_OFF, 8)])?;
627    /// let [bal, nonce] = segs.all_mut();
628    /// bal.set(bal.get() + amount);
629    /// nonce.set(nonce.get() + 1);
630    /// ```
631    #[inline(always)]
632    pub fn split_segments_mut<'b, T: crate::Pod, const N: usize>(
633        &'b mut self,
634        index: usize,
635        ranges: [(u32, u32); N],
636    ) -> Result<crate::SegmentsMut<'b, T, N>, ProgramError> {
637        let mut i = 0;
638        while i < N {
639            self.check_write_policy(index, ranges[i].0, ranges[i].1)?;
640            i += 1;
641        }
642        let view = self
643            .accounts
644            .get(index)
645            .ok_or(ProgramError::NotEnoughAccountKeys)?;
646        view.split_segments_mut_ungated::<T, N>(&mut self.segment_borrows, ranges)
647    }
648
649    /// Register a write borrow for a segment of an account.
650    ///
651    /// Validates bounds, checks writable, and registers a leased
652    /// exclusive borrow, then returns a [`SegRefMut<T>`](crate::SegRefMut)
653    /// that releases on drop.
654    ///
655    /// This primitive permits concurrent mutation of non-overlapping account
656    /// regions. The lease model also permits sequential same-region borrows
657    /// within one instruction.
658    #[inline(always)]
659    pub fn segment_mut<'b, T: crate::Pod>(
660        &'b mut self,
661        index: usize,
662        abs_offset: u32,
663    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
664        self.check_write_policy(index, abs_offset, core::mem::size_of::<T>() as u32)?;
665        let view = self
666            .accounts
667            .get(index)
668            .ok_or(ProgramError::NotEnoughAccountKeys)?;
669        view.segment_mut_ungated::<T>(
670            &mut self.segment_borrows,
671            abs_offset,
672            core::mem::size_of::<T>() as u32,
673        )
674    }
675
676    /// Acquire a growable `Seq<T>` tail for **writing** at `body_end`
677    /// (the layout's `TAIL_PREFIX_OFFSET`), returning a
678    /// [`SeqTailWrite`](crate::tail::SeqTailWrite) guard whose
679    /// [`seq_mut`](crate::tail::SeqTailWrite::seq_mut) yields the O(1)
680    /// streaming cursor.
681    ///
682    /// The tail region is `[body_end, data_len)`, the whole account past
683    /// the fixed head. Under an installed [write policy](Self::set_write_policy)
684    /// this whole region must be granted (a `mut(<seq_field>)` declaration
685    /// compiles to an open-ended [`tail_from`](crate::write_policy::WriteRange::tail_from)
686    /// range), so the fixed head stays protected. Exactly ONE segment
687    /// lease is registered, covering the entire tail region, NOT one per
688    /// element; so overlap detection and the touch map see a single
689    /// tail-region write record regardless of how many elements are
690    /// pushed.
691    #[inline]
692    pub fn tail_seq_mut<'b, T: crate::tail::SeqElement>(
693        &'b mut self,
694        index: usize,
695        body_end: u32,
696    ) -> Result<crate::tail::SeqTailWrite<'b, T>, ProgramError> {
697        let view = self
698            .accounts
699            .get(index)
700            .ok_or(ProgramError::NotEnoughAccountKeys)?;
701        view.check_writable()?;
702        let region_len = (view.data_len() as u32)
703            .checked_sub(body_end)
704            .ok_or(ProgramError::AccountDataTooSmall)?;
705        // The whole tail region must be a granted write range (the
706        // open-ended `tail_from` range contains it; a fixed head range
707        // would refuse a grown region, exactly the protection intended).
708        self.check_write_policy(index, body_end, region_len)?;
709        // ONE write lease over the whole tail region (one touch record).
710        let borrow =
711            self.segment_borrows
712                .register_leased_write(view.address(), body_end, region_len)?;
713        let data = match view.try_borrow_mut_ungated() {
714            Ok(d) => d,
715            Err(e) => {
716                self.segment_borrows.release(&borrow);
717                return Err(e);
718            }
719        };
720        let region = data.slice_from(body_end as usize);
721        // SAFETY: `borrow` was just registered in `self.segment_borrows`;
722        // the lease releases exactly that entry on drop.
723        let lease = unsafe { crate::SegmentLease::new(&mut self.segment_borrows, borrow) };
724        Ok(crate::tail::SeqTailWrite::new(region, lease))
725    }
726
727    /// Acquire a `Seq<T>` tail for **reading** at `body_end`, returning a
728    /// [`SeqTailRead`](crate::tail::SeqTailRead) guard whose
729    /// [`seq`](crate::tail::SeqTailRead::seq) yields the streaming read
730    /// cursor. Registers one shared tail-region lease (reads are not
731    /// gated by the write policy, but the lease still powers overlap
732    /// detection against concurrent writers).
733    #[inline]
734    pub fn tail_seq_ref<'b, T: crate::tail::SeqElement>(
735        &'b mut self,
736        index: usize,
737        body_end: u32,
738    ) -> Result<crate::tail::SeqTailRead<'b, T>, ProgramError> {
739        let view = self
740            .accounts
741            .get(index)
742            .ok_or(ProgramError::NotEnoughAccountKeys)?;
743        let region_len = (view.data_len() as u32)
744            .checked_sub(body_end)
745            .ok_or(ProgramError::AccountDataTooSmall)?;
746        let borrow =
747            self.segment_borrows
748                .register_leased_read(view.address(), body_end, region_len)?;
749        let data = match view.try_borrow() {
750            Ok(d) => d,
751            Err(e) => {
752                self.segment_borrows.release(&borrow);
753                return Err(e);
754            }
755        };
756        let region = data.slice_from(body_end as usize);
757        // SAFETY: `borrow` was just registered in `self.segment_borrows`;
758        // the lease releases exactly that entry on drop.
759        let lease = unsafe { crate::SegmentLease::new(&mut self.segment_borrows, borrow) };
760        Ok(crate::tail::SeqTailRead::new(region, lease))
761    }
762
763    /// Const-driven segment read: pass a compile-time [`crate::Segment`] and the
764    /// account index. Lowers to the same pointer-plus-const-offset shape
765    /// as `segment_ref` but without the caller hand-rolling the offset +
766    /// size arguments.
767    #[inline(always)]
768    pub fn segment_ref_const<'b, T: crate::Pod>(
769        &'b mut self,
770        index: usize,
771        segment: crate::Segment,
772    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
773        let view = self
774            .accounts
775            .get(index)
776            .ok_or(ProgramError::NotEnoughAccountKeys)?;
777        view.segment_ref_const::<T>(&mut self.segment_borrows, segment)
778    }
779
780    /// Const-driven exclusive segment access. Pair with
781    /// `#[hopper::state]` constants for zero-overhead field writes.
782    #[inline(always)]
783    pub fn segment_mut_const<'b, T: crate::Pod>(
784        &'b mut self,
785        index: usize,
786        segment: crate::Segment,
787    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
788        self.check_write_policy(index, segment.offset, segment.size)?;
789        let view = self
790            .accounts
791            .get(index)
792            .ok_or(ProgramError::NotEnoughAccountKeys)?;
793        view.segment_mut_ungated::<T>(&mut self.segment_borrows, segment.offset, segment.size)
794    }
795
796    /// Typed-segment read: the type and offset are both compile-time
797    /// constants, baked into a [`crate::TypedSegment`] zero-sized marker.
798    #[inline(always)]
799    pub fn segment_ref_typed<'b, T: crate::Pod, const OFFSET: u32>(
800        &'b mut self,
801        index: usize,
802        segment: crate::TypedSegment<T, OFFSET>,
803    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
804        let view = self
805            .accounts
806            .get(index)
807            .ok_or(ProgramError::NotEnoughAccountKeys)?;
808        view.segment_ref_typed::<T, OFFSET>(&mut self.segment_borrows, segment)
809    }
810
811    /// Typed-segment write. Mirrors [`Self::segment_ref_typed`] for the
812    /// exclusive path.
813    #[inline(always)]
814    pub fn segment_mut_typed<'b, T: crate::Pod, const OFFSET: u32>(
815        &'b mut self,
816        index: usize,
817        _segment: crate::TypedSegment<T, OFFSET>,
818    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
819        self.check_write_policy(index, OFFSET, core::mem::size_of::<T>() as u32)?;
820        let view = self
821            .accounts
822            .get(index)
823            .ok_or(ProgramError::NotEnoughAccountKeys)?;
824        view.segment_mut_ungated::<T>(
825            &mut self.segment_borrows,
826            OFFSET,
827            core::mem::size_of::<T>() as u32,
828        )
829    }
830
831    /// Explicit unsafe whole-account typed read.
832    #[inline(always)]
833    ///
834    /// # Safety
835    ///
836    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
837    pub unsafe fn raw_ref<T: crate::Pod>(
838        &self,
839        index: usize,
840    ) -> Result<crate::Ref<'_, T>, ProgramError> {
841        let view = self
842            .accounts
843            .get(index)
844            .ok_or(ProgramError::NotEnoughAccountKeys)?;
845        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
846        unsafe { view.raw_ref::<T>() }
847    }
848
849    /// Explicit unsafe whole-account typed write.
850    #[inline(always)]
851    ///
852    /// # Safety
853    ///
854    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
855    pub unsafe fn raw_mut<T: crate::Pod>(
856        &self,
857        index: usize,
858    ) -> Result<crate::RefMut<'_, T>, ProgramError> {
859        let view = self
860            .accounts
861            .get(index)
862            .ok_or(ProgramError::NotEnoughAccountKeys)?;
863        // Whole-account write claim: an installed write policy gates the
864        // raw path exactly like `load_mut` (coarse, never under-claims).
865        self.check_write_policy(index, 0, view.data_len() as u32)?;
866        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
867        unsafe { view.raw_mut::<T>() }
868    }
869
870    /// Legacy alias for [`raw_mut`](Self::raw_mut).
871    ///
872    /// Despite the name, this does **not** bypass borrow tracking: it
873    /// delegates to `raw_mut`, which routes through the checked
874    /// `segment_mut(0, size_of::<T>())` path (bounds, writable, and
875    /// account-level exclusive borrow all enforced). The caller remains
876    /// responsible for using a type that matches the account bytes. For a
877    /// genuinely untracked pointer, use [`as_mut_ptr`](Self::as_mut_ptr).
878    #[inline(always)]
879    ///
880    /// # Safety
881    ///
882    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
883    pub unsafe fn raw_unchecked<T: crate::Pod>(
884        &self,
885        index: usize,
886    ) -> Result<crate::RefMut<'_, T>, ProgramError> {
887        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
888        unsafe { self.raw_mut::<T>(index) }
889    }
890
891    /// Canonical raw-pointer escape hatch to an account's data buffer.
892    ///
893    /// Returns a pointer to the first byte of `accounts[index]`'s data
894    /// region (after the runtime account header, before any Hopper
895    /// 16-byte layout header). The pointer is valid for reads and
896    /// writes for the lifetime of the account view and carries no
897    /// borrow-tracking obligations. Dereferencing it is `unsafe`
898    /// because the caller takes over alias-safety responsibility
899    /// that the segment registry normally upholds.
900    ///
901    /// This is the explicit power-user primitive the audit asks for:
902    /// safe code reaches for `segment_ref_typed` / `segment_mut_typed`
903    /// / the generated `ctx.<field>_segment_mut(...)` accessors; raw
904    /// code drops to `unsafe { ctx.as_mut_ptr(0)?.add(offset) as *mut T }`.
905    ///
906    /// # Safety
907    ///
908    /// The caller must guarantee no aliasing mutable borrow is held
909    /// on the same account for the duration of any write through the
910    /// returned pointer. The returned pointer must be dereferenced
911    /// within the `'info` lifetime of the account view; reading past
912    /// `AccountView::data_len()` is undefined behaviour.
913    #[inline(always)]
914    pub unsafe fn as_mut_ptr(&self, index: usize) -> Result<*mut u8, ProgramError> {
915        let view = self
916            .accounts
917            .get(index)
918            .ok_or(ProgramError::NotEnoughAccountKeys)?;
919        view.require_writable()?;
920        // The untracked pointer is whole-account write capability, so an
921        // installed write policy must have granted the whole account.
922        self.check_write_policy(index, 0, view.data_len() as u32)?;
923        // SAFETY: the account view is live for `'info` and
924        // `data_ptr` yields a pointer inside the loader-provided
925        // per-account buffer. Returning the untyped pointer transfers
926        // alias-safety to the caller as documented above.
927        Ok(view.data_ptr_unchecked())
928    }
929
930    /// Immutable sibling of [`as_mut_ptr`]. Returns a `*const u8`.
931    ///
932    /// Shared-borrow checking still runs, so calling this while an
933    /// exclusive borrow is live on the same account fails with
934    /// `AccountBorrowFailed`. The return value is safe to obtain; the
935    /// caller only needs `unsafe` to dereference it.
936    ///
937    /// [`as_mut_ptr`]: Self::as_mut_ptr
938    #[inline(always)]
939    pub fn as_ptr(&self, index: usize) -> Result<*const u8, ProgramError> {
940        let view = self
941            .accounts
942            .get(index)
943            .ok_or(ProgramError::NotEnoughAccountKeys)?;
944        view.check_borrow()?;
945        Ok(view.data_ptr_unchecked() as *const u8)
946    }
947
948    /// Read instruction data as a typed value (unaligned, little-endian safe).
949    ///
950    /// Reads `size_of::<T>()` bytes starting at `offset` via `read_unaligned`.
951    /// Caller must ensure `T` is a plain-old-data type where all bit patterns
952    /// are valid.
953    #[inline(always)]
954    pub fn read_data<T: crate::ValuePod>(&self, offset: usize) -> Result<T, ProgramError> {
955        let end = offset
956            .checked_add(core::mem::size_of::<T>())
957            .ok_or(ProgramError::ArithmeticOverflow)?;
958        if self.instruction_data.len() < end {
959            return Err(ProgramError::InvalidInstructionData);
960        }
961        // SAFETY: bounds checked; `T: ValuePod` guarantees every bit
962        // pattern is valid by value and the type has no drop glue, so
963        // `read_unaligned` from instruction data is sound.
964        Ok(unsafe {
965            core::ptr::read_unaligned(self.instruction_data.as_ptr().add(offset) as *const T)
966        })
967    }
968
969    /// Get a byte slice from instruction data.
970    #[inline(always)]
971    pub fn data_slice(&self, offset: usize, len: usize) -> Result<&[u8], ProgramError> {
972        let end = offset
973            .checked_add(len)
974            .ok_or(ProgramError::ArithmeticOverflow)?;
975        if self.instruction_data.len() < end {
976            return Err(ProgramError::InvalidInstructionData);
977        }
978        Ok(&self.instruction_data[offset..end])
979    }
980
981    /// Read the first byte of instruction data as an instruction tag.
982    ///
983    /// Common pattern for byte-tag dispatch.
984    #[inline(always)]
985    pub fn instruction_tag(&self) -> Result<u8, ProgramError> {
986        self.instruction_data
987            .first()
988            .copied()
989            .ok_or(ProgramError::InvalidInstructionData)
990    }
991}
992
993/// Borrow-scoped view of a [`Context`].
994///
995/// Generated typed contexts expose this wrapper from their safe `raw()` method
996/// instead of returning `&mut Context<'a>` directly. That keeps account and
997/// remaining-account references tied to the borrow of the generated context,
998/// preventing backend account-view lifetimes from being widened through the raw
999/// escape hatch.
1000pub struct ScopedContext<'ctx, 'a> {
1001    inner: &'ctx mut Context<'a>,
1002}
1003
1004impl<'ctx, 'a> ScopedContext<'ctx, 'a> {
1005    /// Create a borrow-scoped wrapper around a raw Hopper context.
1006    #[inline(always)]
1007    pub fn new(inner: &'ctx mut Context<'a>) -> Self {
1008        Self { inner }
1009    }
1010
1011    /// Program ID, narrowed to the wrapper borrow lifetime.
1012    #[inline(always)]
1013    pub fn program_id(&self) -> &'ctx Address {
1014        self.inner.program_id
1015    }
1016
1017    /// Raw instruction data, narrowed to the wrapper borrow lifetime.
1018    #[inline(always)]
1019    pub fn instruction_data(&self) -> &'ctx [u8] {
1020        self.inner.instruction_data
1021    }
1022
1023    /// Get an account by index, narrowed to the wrapper borrow lifetime.
1024    #[inline(always)]
1025    pub fn account(&self, index: usize) -> Result<&'ctx AccountView<'a>, ProgramError> {
1026        self.inner
1027            .accounts
1028            .get(index)
1029            .ok_or(ProgramError::NotEnoughAccountKeys)
1030    }
1031
1032    /// Mutation-intent account access, narrowed to the wrapper borrow lifetime.
1033    #[inline(always)]
1034    pub fn account_mut(&self, index: usize) -> Result<&'ctx AccountView<'a>, ProgramError> {
1035        self.account(index)
1036    }
1037
1038    /// Get the total number of accounts.
1039    #[inline(always)]
1040    pub fn num_accounts(&self) -> usize {
1041        self.inner.num_accounts()
1042    }
1043
1044    /// Get all accounts as a slice, narrowed to the wrapper borrow lifetime.
1045    #[inline(always)]
1046    pub fn accounts(&self) -> &'ctx [AccountView<'a>] {
1047        self.inner.accounts
1048    }
1049
1050    /// Borrow one runtime-selected typed byte range for reading.
1051    ///
1052    /// This is the safe bridge for generated typed contexts whose semantic
1053    /// column is known statically but whose cell offset is selected at runtime
1054    /// (for example, a slot in a column-oriented intent shard). The returned
1055    /// guard remains tied to this scoped context borrow and participates in the
1056    /// instruction segment-borrow ledger.
1057    #[inline(always)]
1058    pub fn segment_ref<'b, T: crate::Pod>(
1059        &'b mut self,
1060        index: usize,
1061        abs_offset: u32,
1062    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
1063        self.inner.segment_ref::<T>(index, abs_offset)
1064    }
1065
1066    /// Borrow one runtime-selected typed byte range for mutation.
1067    ///
1068    /// The underlying [`Context::segment_mut`] performs the active
1069    /// `strict_writes` containment check before registering the exclusive
1070    /// segment lease, so exposing this method does not create a policy escape.
1071    /// It lets typed handlers keep their generated manifest/IDL metadata while
1072    /// selecting an exact cell inside a declared column at runtime.
1073    #[inline(always)]
1074    pub fn segment_mut<'b, T: crate::Pod>(
1075        &'b mut self,
1076        index: usize,
1077        abs_offset: u32,
1078    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
1079        self.inner.segment_mut::<T>(index, abs_offset)
1080    }
1081
1082    /// Borrow several disjoint runtime-selected typed ranges for mutation.
1083    /// Every range is checked against the active write policy before any lease
1084    /// is granted.
1085    #[inline(always)]
1086    pub fn split_segments_mut<'b, T: crate::Pod, const N: usize>(
1087        &'b mut self,
1088        index: usize,
1089        ranges: [(u32, u32); N],
1090    ) -> Result<crate::SegmentsMut<'b, T, N>, ProgramError> {
1091        self.inner.split_segments_mut::<T, N>(index, ranges)
1092    }
1093
1094    /// Access the instruction-scoped segment borrow registry.
1095    #[inline(always)]
1096    pub fn borrows(&self) -> &SegmentBorrowRegistry {
1097        &self.inner.segment_borrows
1098    }
1099
1100    /// Mutably access the instruction-scoped segment borrow registry.
1101    #[inline(always)]
1102    pub fn borrows_mut(&mut self) -> &mut SegmentBorrowRegistry {
1103        &mut self.inner.segment_borrows
1104    }
1105
1106    /// Inspect the currently reachable account slice for duplicate aliases.
1107    #[inline(always)]
1108    pub fn audit_accounts(&self) -> AccountAudit<'ctx> {
1109        AccountAudit::new(self.inner.accounts)
1110    }
1111
1112    /// Get the remaining accounts starting at `from`, narrowed to the wrapper
1113    /// borrow lifetime.
1114    #[inline(always)]
1115    pub fn remaining_accounts(&self, from: usize) -> &'ctx [AccountView<'a>] {
1116        if from >= self.inner.accounts.len() {
1117            &[]
1118        } else {
1119            &self.inner.accounts[from..]
1120        }
1121    }
1122
1123    /// Get remaining accounts in strict duplicate-rejecting mode.
1124    #[inline(always)]
1125    pub fn remaining_accounts_strict(
1126        &self,
1127        from: usize,
1128    ) -> crate::remaining::RemainingAccounts<'ctx> {
1129        let declared_end = from.min(self.inner.accounts.len());
1130        crate::remaining::RemainingAccounts::strict(
1131            &self.inner.accounts[..declared_end],
1132            self.remaining_accounts(from),
1133        )
1134    }
1135
1136    /// Get remaining accounts in duplicate-preserving passthrough mode.
1137    #[inline(always)]
1138    pub fn remaining_accounts_passthrough(
1139        &self,
1140        from: usize,
1141    ) -> crate::remaining::RemainingAccounts<'ctx> {
1142        let declared_end = from.min(self.inner.accounts.len());
1143        crate::remaining::RemainingAccounts::passthrough(
1144            &self.inner.accounts[..declared_end],
1145            self.remaining_accounts(from),
1146        )
1147    }
1148
1149    /// Get remaining accounts in strict mode and bind a sequential typed parser.
1150    #[inline(always)]
1151    pub fn remaining_accounts_typed(&self, from: usize) -> crate::remaining::RemainingTyped<'ctx> {
1152        self.remaining_accounts_strict(from).typed()
1153    }
1154
1155    /// Get remaining accounts in strict mode and bind a lazy indexed parser.
1156    #[inline(always)]
1157    pub fn remaining_accounts_lazy(&self, from: usize) -> crate::remaining::RemainingLazy<'ctx> {
1158        self.remaining_accounts_strict(from).lazy()
1159    }
1160
1161    /// Require at least `n` accounts are present.
1162    #[inline(always)]
1163    pub fn require_accounts(&self, n: usize) -> ProgramResult {
1164        self.inner.require_accounts(n)
1165    }
1166
1167    /// Require all account addresses to be unique.
1168    #[inline(always)]
1169    pub fn require_unique_accounts(&self) -> ProgramResult {
1170        self.audit_accounts().require_all_unique()
1171    }
1172
1173    /// Require that no duplicated account is writable in this instruction.
1174    #[inline(always)]
1175    pub fn require_unique_writable_accounts(&self) -> ProgramResult {
1176        self.audit_accounts().require_unique_writable()
1177    }
1178
1179    /// Require that no duplicated account is used as a signer role.
1180    #[inline(always)]
1181    pub fn require_unique_signer_accounts(&self) -> ProgramResult {
1182        self.audit_accounts().require_unique_signers()
1183    }
1184
1185    /// Require at least `n` bytes of instruction data.
1186    #[inline(always)]
1187    pub fn require_data_len(&self, n: usize) -> ProgramResult {
1188        self.inner.require_data_len(n)
1189    }
1190
1191    /// Read instruction data as a typed value.
1192    #[inline(always)]
1193    pub fn read_data<T: crate::ValuePod>(&self, offset: usize) -> Result<T, ProgramError> {
1194        self.inner.read_data(offset)
1195    }
1196
1197    /// Get a byte slice from instruction data.
1198    #[inline(always)]
1199    pub fn data_slice(&self, offset: usize, len: usize) -> Result<&'ctx [u8], ProgramError> {
1200        let end = offset
1201            .checked_add(len)
1202            .ok_or(ProgramError::ArithmeticOverflow)?;
1203        self.inner
1204            .instruction_data
1205            .get(offset..end)
1206            .ok_or(ProgramError::InvalidInstructionData)
1207    }
1208}
1209
1210// ── Tests ────────────────────────────────────────────────────────────
1211
1212#[cfg(test)]
1213mod write_policy_tests {
1214    use super::*;
1215    use crate::write_policy::{WritePolicy, WriteRange};
1216    use hopper_native::{
1217        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
1218    };
1219
1220    const DATA_LEN: usize = 64;
1221    const BALANCE_OFF: u32 = 16;
1222    const NONCE_OFF: u32 = 24;
1223
1224    fn make_account(address_byte: u8) -> (std::vec::Vec<u64>, AccountView<'static>) {
1225        // Word-sized backing: `RuntimeAccount` has u64 fields (align 8) and a
1226        // `Vec<u8>` allocation only guarantees alignment 1, writing the
1227        // header through an under-aligned pointer is UB by spec even where
1228        // the system allocator happens to over-align. Caught by the Miri
1229        // Tree Borrows lane (`scripts/miri-core.*`); same fix as the
1230        // competitor_bug_classes fixtures (adversarial review 2026-07-07).
1231        let total = RuntimeAccount::SIZE + DATA_LEN;
1232        let mut backing = std::vec![0u64; total.div_ceil(8)];
1233        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1234        // SAFETY: `backing` is sized for the header plus DATA_LEN bytes,
1235        // 8-aligned by construction, and outlives the returned view (the
1236        // caller holds the Vec).
1237        unsafe {
1238            raw.write(RuntimeAccount {
1239                borrow_state: NOT_BORROWED,
1240                is_signer: 1,
1241                is_writable: 1,
1242                executable: 0,
1243                resize_delta: 0,
1244                address: NativeAddress::new_from_array([address_byte; 32]),
1245                owner: NativeAddress::new_from_array([2; 32]),
1246                lamports: 42,
1247                data_len: DATA_LEN as u64,
1248            });
1249        }
1250        // SAFETY: `raw` points at a fully initialized RuntimeAccount with
1251        // its data region in the same allocation.
1252        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1253        (backing, AccountView::from_backend(backend))
1254    }
1255
1256    // Field-granular policy on account 0: balance + nonce only.
1257    static FIELD_POLICY: WritePolicy = WritePolicy::new(&[
1258        WriteRange::new(0, BALANCE_OFF, 8),
1259        WriteRange::new(0, NONCE_OFF, 8),
1260    ]);
1261    // Whole-account allowance on account 0 (a plain `mut` declaration).
1262    static WHOLE_POLICY: WritePolicy = WritePolicy::new(&[WriteRange::whole_account(0)]);
1263
1264    #[test]
1265    fn no_policy_leaves_every_write_path_open() {
1266        let (_b, account) = make_account(1);
1267        let accounts = [account];
1268        let pid = Address::new([9u8; 32]);
1269        let mut ctx = Context::new(&pid, &accounts, &[]);
1270
1271        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1272        assert!(ctx.segment_mut::<[u8; 4]>(0, 0).is_ok());
1273    }
1274
1275    #[test]
1276    fn field_policy_allows_declared_segments_and_refuses_the_rest() {
1277        let (_b, account) = make_account(1);
1278        let accounts = [account];
1279        let pid = Address::new([9u8; 32]);
1280        let mut ctx = Context::new(&pid, &accounts, &[]);
1281        ctx.set_write_policy(&FIELD_POLICY);
1282
1283        // Declared ranges work, including disjoint simultaneous writes.
1284        {
1285            let mut segs = ctx
1286                .split_segments_mut::<[u8; 8], 2>(0, [(BALANCE_OFF, 8), (NONCE_OFF, 8)])
1287                .unwrap();
1288            let [bal, nonce] = segs.all_mut();
1289            bal[0] = 1;
1290            nonce[0] = 2;
1291        }
1292        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1293
1294        // An undeclared range is refused with the indexed policy error,
1295        // before any borrow state changes, so reads still work after.
1296        assert_eq!(
1297            ctx.segment_mut::<[u8; 8]>(0, 0).unwrap_err(),
1298            crate::write_policy::write_policy_violation(0)
1299        );
1300        assert!(ctx.segment_ref::<[u8; 8]>(0, 0).is_ok());
1301
1302        // A split where ONE range is undeclared is refused whole.
1303        assert!(ctx
1304            .split_segments_mut::<[u8; 8], 2>(0, [(BALANCE_OFF, 8), (0, 8)])
1305            .is_err());
1306
1307        // Reads are never policy-gated.
1308        assert!(ctx.segment_ref::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1309    }
1310
1311    #[test]
1312    fn scoped_context_runtime_segments_preserve_write_policy() {
1313        let (_b, account) = make_account(1);
1314        let accounts = [account];
1315        let pid = Address::new([9u8; 32]);
1316        let mut ctx = Context::new(&pid, &accounts, &[]);
1317        ctx.set_write_policy(&FIELD_POLICY);
1318
1319        {
1320            let mut scoped = ScopedContext::new(&mut ctx);
1321            let mut balance = scoped
1322                .segment_mut::<[u8; 8]>(0, BALANCE_OFF)
1323                .expect("declared runtime-selected range must be writable");
1324            balance[0] = 7;
1325        }
1326
1327        {
1328            let mut scoped = ScopedContext::new(&mut ctx);
1329            assert_eq!(
1330                scoped.segment_mut::<[u8; 8]>(0, 0).unwrap_err(),
1331                crate::write_policy::write_policy_violation(0),
1332            );
1333            assert!(scoped.segment_ref::<[u8; 8]>(0, 0).is_ok());
1334        }
1335    }
1336
1337    #[test]
1338    fn field_policy_refuses_whole_account_write_paths() {
1339        let (_b, account) = make_account(1);
1340        let accounts = [account];
1341        let pid = Address::new([9u8; 32]);
1342        let mut ctx = Context::new(&pid, &accounts, &[]);
1343        ctx.set_write_policy(&FIELD_POLICY);
1344
1345        // The untracked whole-account pointer is whole-account write
1346        // capability: a field-granular policy must refuse it.
1347        // SAFETY: never dereferenced; testing the acquire-time gate only.
1348        assert_eq!(
1349            unsafe { ctx.as_mut_ptr(0) }.unwrap_err(),
1350            crate::write_policy::write_policy_violation(0)
1351        );
1352    }
1353
1354    #[test]
1355    fn whole_account_allowance_admits_all_write_paths() {
1356        let (_b, account) = make_account(1);
1357        let accounts = [account];
1358        let pid = Address::new([9u8; 32]);
1359        let mut ctx = Context::new(&pid, &accounts, &[]);
1360        ctx.set_write_policy(&WHOLE_POLICY);
1361
1362        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1363        // SAFETY: never dereferenced; testing the acquire-time gate only.
1364        assert!(unsafe { ctx.as_mut_ptr(0) }.is_ok());
1365    }
1366
1367    #[test]
1368    fn empty_policy_is_a_machine_checked_read_only_instruction() {
1369        static READ_ONLY: WritePolicy = WritePolicy::new(&[]);
1370        let (_b, account) = make_account(1);
1371        let accounts = [account];
1372        let pid = Address::new([9u8; 32]);
1373        let mut ctx = Context::new(&pid, &accounts, &[]);
1374        ctx.set_write_policy(&READ_ONLY);
1375
1376        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_err());
1377        // SAFETY: never dereferenced; testing the acquire-time gate only.
1378        assert!(unsafe { ctx.as_mut_ptr(0) }.is_err());
1379        // Reads remain untouched.
1380        assert!(ctx.segment_ref::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1381        assert!(ctx.as_ptr(0).is_ok());
1382    }
1383
1384    #[repr(C)]
1385    #[derive(Clone, Copy)]
1386    struct PolicyLayout {
1387        a: [u8; 8],
1388    }
1389    // SAFETY: repr(C), all-byte-array fields, every bit pattern valid,
1390    // no padding, align 1.
1391    unsafe impl crate::Zeroable for PolicyLayout {}
1392    // SAFETY: as above.
1393    unsafe impl crate::Pod for PolicyLayout {}
1394    impl crate::field_map::FieldMap for PolicyLayout {
1395        const FIELDS: &'static [crate::field_map::FieldInfo] = &[crate::field_map::FieldInfo::new(
1396            "a",
1397            crate::layout::HopperHeader::SIZE,
1398            8,
1399        )];
1400    }
1401    impl LayoutContract for PolicyLayout {
1402        const DISC: u8 = 77;
1403        const VERSION: u8 = 1;
1404        const LAYOUT_ID: [u8; 8] = [0x77; 8];
1405        const SIZE: usize = crate::layout::HopperHeader::SIZE + core::mem::size_of::<Self>();
1406    }
1407
1408    #[test]
1409    fn load_mut_is_gated_before_header_validation_or_borrow() {
1410        let (_b, account) = make_account(1);
1411        let accounts = [account];
1412        let pid = Address::new([9u8; 32]);
1413        let mut ctx = Context::new(&pid, &accounts, &[]);
1414        ctx.set_write_policy(&FIELD_POLICY);
1415
1416        // The whole-account claim `[0, data_len)` is refused by a
1417        // field-granular policy, with the policy error, not a layout
1418        // error, proving the gate runs before any borrow or header read.
1419        assert_eq!(
1420            ctx.load_mut::<PolicyLayout>(0).unwrap_err(),
1421            crate::write_policy::write_policy_violation(0)
1422        );
1423
1424        // Under a whole-account allowance the gate passes; the account
1425        // has no valid Hopper header, so whatever happens next it is not
1426        // a policy refusal.
1427        let mut ctx2 = Context::new(&pid, &accounts, &[]);
1428        ctx2.set_write_policy(&WHOLE_POLICY);
1429        assert_ne!(
1430            ctx2.load_mut::<PolicyLayout>(0).unwrap_err(),
1431            crate::write_policy::write_policy_violation(0)
1432        );
1433    }
1434
1435    #[cfg(feature = "touch-map")]
1436    #[test]
1437    fn whole_account_load_mut_lands_in_the_touch_map() {
1438        use crate::segment_borrow::AccessKind;
1439
1440        let (_b, account) = make_account(1);
1441        let accounts = [account];
1442        let pid = Address::new([9u8; 32]);
1443        let mut ctx = Context::new(&pid, &accounts, &[]);
1444
1445        // A segment write followed by a whole-account borrow recorded
1446        // through the same ledger: the touch map now sees both shapes.
1447        drop(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1448        {
1449            let view = ctx.account(0).unwrap();
1450            let data_len = view.data_len() as u32;
1451            let addr = *view.address();
1452            ctx.borrows_mut()
1453                .record_account_touch(&addr, data_len, AccessKind::Write);
1454        }
1455
1456        let mut seen = std::vec::Vec::new();
1457        ctx.for_each_touch(|t| seen.push((t.offset, t.size, t.kind)));
1458        assert_eq!(seen.len(), 2);
1459        assert_eq!(seen[0], (BALANCE_OFF, 8, AccessKind::Write));
1460        assert_eq!(seen[1], (0, DATA_LEN as u32, AccessKind::Write));
1461    }
1462
1463    #[cfg(feature = "touch-map")]
1464    #[test]
1465    fn touch_map_emission_round_trips_through_the_wire_format() {
1466        use crate::segment_borrow::{decode_touch_map_for_tests, AccessKind, TouchMapRecord};
1467
1468        let (_b0, account0) = make_account(1);
1469        let (_b1, account1) = make_account(2);
1470        let accounts = [account0, account1];
1471        let pid = Address::new([9u8; 32]);
1472        let mut ctx = Context::new(&pid, &accounts, &[]);
1473
1474        // A write and a read on account 1, a read on account 0, and a
1475        // whole-account write on account 0, every touch shape.
1476        drop(ctx.segment_mut::<[u8; 8]>(1, BALANCE_OFF).unwrap());
1477        drop(ctx.segment_ref::<[u8; 8]>(1, NONCE_OFF).unwrap());
1478        drop(ctx.segment_ref::<[u8; 4]>(0, 0).unwrap());
1479        {
1480            let view = ctx.account(0).unwrap();
1481            let (data_len, addr) = (view.data_len() as u32, *view.address());
1482            ctx.borrows_mut()
1483                .record_account_touch(&addr, data_len, AccessKind::Write);
1484        }
1485
1486        let (buf, len) = ctx.encode_touch_map();
1487        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1488        assert_eq!(flags, 0, "complete map must carry no overflow/skip flags");
1489        assert_eq!(
1490            records,
1491            std::vec![
1492                TouchMapRecord {
1493                    slot: 1,
1494                    offset: BALANCE_OFF,
1495                    size: 8,
1496                    write: true,
1497                },
1498                TouchMapRecord {
1499                    slot: 1,
1500                    offset: NONCE_OFF,
1501                    size: 8,
1502                    write: false,
1503                },
1504                TouchMapRecord {
1505                    slot: 0,
1506                    offset: 0,
1507                    size: 4,
1508                    write: false,
1509                },
1510                TouchMapRecord {
1511                    slot: 0,
1512                    offset: 0,
1513                    size: DATA_LEN as u32,
1514                    write: true,
1515                },
1516            ]
1517        );
1518
1519        // Off-chain the syscall is a no-op; the call must still be safe.
1520        ctx.emit_touch_map();
1521    }
1522
1523    #[cfg(feature = "touch-map")]
1524    #[test]
1525    fn touch_map_emission_marks_overflow_honestly() {
1526        use crate::segment_borrow::{decode_touch_map_for_tests, TOUCH_MAP_FLAG_OVERFLOWED};
1527
1528        let (_b, account) = make_account(1);
1529        let accounts = [account];
1530        let pid = Address::new([9u8; 32]);
1531        let mut ctx = Context::new(&pid, &accounts, &[]);
1532
1533        // Touch more distinct ranges than the log capacity. The stride
1534        // leaves a one-byte gap between consecutive ranges so no exact
1535        // union exists, coalescing (which keeps contiguous workloads
1536        // complete) cannot save this map, and the honest outcome is the
1537        // wire-visible overflow flag.
1538        let addr = *ctx.account(0).unwrap().address();
1539        let mut i: u32 = 0;
1540        while (i as usize) < crate::segment_borrow::MAX_TOUCH_RECORDS + 3 {
1541            let b = ctx
1542                .borrows_mut()
1543                .register_leased_read(&addr, i * 2, 1)
1544                .unwrap();
1545            ctx.borrows_mut().release(&b);
1546            i += 1;
1547        }
1548        assert!(ctx.touch_map_overflowed());
1549
1550        let (buf, len) = ctx.encode_touch_map();
1551        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1552        assert_eq!(flags & TOUCH_MAP_FLAG_OVERFLOWED, TOUCH_MAP_FLAG_OVERFLOWED);
1553        assert_eq!(records.len(), crate::segment_borrow::MAX_TOUCH_RECORDS);
1554    }
1555
1556    /// The opt-in epilogue helper the generated dispatcher calls on the
1557    /// handler's Ok path when the context declared
1558    /// `#[hopper::context(emit_touch_map)]`. With the `touch-map` feature
1559    /// on it must finalize the instruction exactly like a hand-written
1560    /// `emit_touch_map`: snapshot the cumulative touch log and hand the
1561    /// same wire bytes the encoder produces to `sol_log_data`. Off-chain
1562    /// the syscall is a no-op, so we prove the record is decodable via the
1563    /// shared encoder and that calling the helper is safe.
1564    #[cfg(feature = "touch-map")]
1565    #[test]
1566    fn finish_with_touch_map_finalizes_a_decodable_record() {
1567        use crate::segment_borrow::{decode_touch_map_for_tests, TouchMapRecord};
1568
1569        let (_b, account) = make_account(1);
1570        let accounts = [account];
1571        let pid = Address::new([9u8; 32]);
1572        let mut ctx = Context::new(&pid, &accounts, &[]);
1573
1574        // A bound handler with the opt-in touches state, then the
1575        // dispatcher calls `finish_with_touch_map()` on the Ok path.
1576        drop(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1577        drop(ctx.segment_ref::<[u8; 8]>(0, NONCE_OFF).unwrap());
1578
1579        // The record the epilogue emits is byte-identical to the encoder's
1580        // output (what `emit_touch_map` / `finish_with_touch_map` send).
1581        let (buf, len) = ctx.encode_touch_map();
1582        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1583        assert_eq!(flags, 0, "complete map carries no overflow/skip flags");
1584        assert_eq!(
1585            records,
1586            std::vec![
1587                TouchMapRecord {
1588                    slot: 0,
1589                    offset: BALANCE_OFF,
1590                    size: 8,
1591                    write: true,
1592                },
1593                TouchMapRecord {
1594                    slot: 0,
1595                    offset: NONCE_OFF,
1596                    size: 8,
1597                    write: false,
1598                },
1599            ]
1600        );
1601
1602        // The helper the dispatcher calls on the Ok path: off-chain
1603        // no-op, must be safe and must not disturb the recorded footprint.
1604        ctx.finish_with_touch_map();
1605    }
1606
1607    /// The mirror of the opt-in: a context that touched nothing (a handler
1608    /// that did no state access, or one whose context did NOT opt in and
1609    /// so whose dispatcher never calls the helper) has an empty footprint,
1610    /// the epilogue would emit a header-only record with zero touch
1611    /// entries. This pins "without the opt-in / without touches, produces
1612    /// none".
1613    #[cfg(feature = "touch-map")]
1614    #[test]
1615    fn untouched_context_finish_emits_no_touch_records() {
1616        use crate::segment_borrow::decode_touch_map_for_tests;
1617
1618        let (_b, account) = make_account(1);
1619        let accounts = [account];
1620        let pid = Address::new([9u8; 32]);
1621        let ctx = Context::new(&pid, &accounts, &[]);
1622
1623        assert_eq!(ctx.touch_map_len(), 0);
1624        let (buf, len) = ctx.encode_touch_map();
1625        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1626        assert_eq!(flags, 0, "empty map carries no flags");
1627        assert!(
1628            records.is_empty(),
1629            "no touches means no records: {records:?}"
1630        );
1631
1632        // Safe to finalize even with nothing to report.
1633        ctx.finish_with_touch_map();
1634    }
1635
1636    /// Failed-instruction emission regression: the touch-map record must be emitted on
1637    /// the handler's **Ok** path ONLY, never on `Err`. This reconstructs
1638    /// the exact shape the dispatcher generates for an opted-in typed
1639    /// context,
1640    ///
1641    /// ```ignore
1642    /// handler(Ctx::bind(&mut ctx)?, ..)?;              // Err short-circuits
1643    /// if Ctx::EMIT_TOUCH_MAP { ctx.finish_with_touch_map(); }
1644    /// Ok(())
1645    /// ```
1646    ///
1647    /// and drives it with a handler that returns `Err` and the same
1648    /// handler that returns `Ok`. The old `Drop`-based emit fired on both
1649    /// paths (drop glue runs on every scope exit) and so leaked a
1650    /// misleading record for the rolled-back instruction; the Ok-only
1651    /// dispatch emits nothing on `Err` and exactly one decodable record on
1652    /// `Ok`. Off-chain the real syscall is a no-op, so the finish point is
1653    /// made observable by snapshotting the same wire bytes
1654    /// `finish_with_touch_map` would send.
1655    #[cfg(feature = "touch-map")]
1656    #[test]
1657    fn dispatch_emits_touch_map_on_ok_path_only() {
1658        use crate::segment_borrow::decode_touch_map_for_tests;
1659
1660        // The context opted in, so its `EMIT_TOUCH_MAP` const is `true`.
1661        const EMIT_TOUCH_MAP: bool = true;
1662
1663        // Faithful reconstruction of the generated dispatch helper body.
1664        // `emitted` captures each record the Ok-path finish point would
1665        // send, its length is the number of touch-map records emitted.
1666        fn generated_dispatch(
1667            ctx: &mut Context<'_>,
1668            handler: impl FnOnce(&mut Context<'_>) -> ProgramResult,
1669            emitted: &mut std::vec::Vec<std::vec::Vec<u8>>,
1670        ) -> ProgramResult {
1671            // `handler(Ctx::bind(&mut ctx)?, ..)?`, an `Err` here
1672            // short-circuits before the emit below, exactly as `?` does
1673            // after a real bound handler returns.
1674            handler(ctx)?;
1675            // `if Ctx::EMIT_TOUCH_MAP { ctx.finish_with_touch_map(); }`,
1676            // reached only on the Ok path. Off-chain the syscall is a
1677            // no-op, so snapshot the identical bytes to observe the emit.
1678            if EMIT_TOUCH_MAP {
1679                let (buf, len) = ctx.encode_touch_map();
1680                emitted.push(buf[..len].to_vec());
1681                ctx.finish_with_touch_map();
1682            }
1683            Ok(())
1684        }
1685
1686        // A handler that touches state, then fails (as `require!`/`?`
1687        // would). The touch log is populated, but the instruction rolls
1688        // back; so NO self-describing record may be emitted.
1689        let (_b, account) = make_account(1);
1690        let accounts = [account];
1691        let pid = Address::new([9u8; 32]);
1692        let mut ctx = Context::new(&pid, &accounts, &[]);
1693        let mut emitted = std::vec::Vec::new();
1694        let err = generated_dispatch(
1695            &mut ctx,
1696            |c| {
1697                drop(c.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1698                Err(ProgramError::Custom(7))
1699            },
1700            &mut emitted,
1701        );
1702        assert_eq!(
1703            err,
1704            Err(ProgramError::Custom(7)),
1705            "handler error must propagate"
1706        );
1707        assert!(
1708            emitted.is_empty(),
1709            "a failed instruction must emit NO touch-map record: {emitted:?}",
1710        );
1711
1712        // The same handler shape, now returning Ok, must emit exactly one
1713        // decodable record describing what it touched.
1714        let (_b2, account2) = make_account(1);
1715        let accounts2 = [account2];
1716        let mut ctx_ok = Context::new(&pid, &accounts2, &[]);
1717        let mut emitted_ok = std::vec::Vec::new();
1718        let ok = generated_dispatch(
1719            &mut ctx_ok,
1720            |c| {
1721                drop(c.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1722                Ok(())
1723            },
1724            &mut emitted_ok,
1725        );
1726        assert_eq!(ok, Ok(()), "successful handler returns Ok");
1727        assert_eq!(
1728            emitted_ok.len(),
1729            1,
1730            "a successful instruction must emit exactly one touch-map record",
1731        );
1732        let (_flags, records) = decode_touch_map_for_tests(&emitted_ok[0]).unwrap();
1733        assert_eq!(
1734            records.len(),
1735            1,
1736            "the record must describe the one touched range"
1737        );
1738        assert_eq!(records[0].offset, BALANCE_OFF);
1739        assert!(records[0].write, "the touched range was a write");
1740    }
1741}