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    /// Refuse a transaction that fills both slots of any listed pair with
493    /// one account.
494    ///
495    /// `#[derive(Accounts)]` calls this with every pair of mutable slots
496    /// the context declares (declared `dup` aliases and optional slots
497    /// left out), so `from` and `to` can never be the same vault unless
498    /// the author said so. On chain the loader serializes a repeated
499    /// account once and marks the later slots as duplicates of that
500    /// record, so two slots alias exactly when their views share the
501    /// record: one pointer compare per pair, no address bytes read. Off
502    /// chain, where the host harness builds a record per slot, the
503    /// addresses are compared by value. Refuses with
504    /// [`crate::ERR_ALIASED_MUTABLE_ACCOUNTS`].
505    #[inline(always)]
506    pub fn require_distinct_slots(&self, pairs: &[(usize, usize)]) -> ProgramResult {
507        let mut i = 0;
508        while i < pairs.len() {
509            let (a, b) = pairs[i];
510            self.require_distinct_pair(a, b)?;
511            i += 1;
512        }
513        Ok(())
514    }
515
516    /// [`Self::require_distinct_slots`] for one pair, with no pair table or
517    /// loop: what `#[derive(Accounts)]` emits for a context with exactly
518    /// two mutable slots (two loads and one compare).
519    #[inline(always)]
520    pub fn require_distinct_pair(&self, a: usize, b: usize) -> ProgramResult {
521        if same_record(self.account(a)?, self.account(b)?) {
522            return Err(crate::ERR_ALIASED_MUTABLE_ACCOUNTS);
523        }
524        Ok(())
525    }
526
527    /// Require all account addresses to be unique.
528    #[inline(always)]
529    pub fn require_unique_accounts(&self) -> ProgramResult {
530        self.audit_accounts().require_all_unique()
531    }
532
533    /// Require that no duplicated account is writable in this instruction.
534    #[inline(always)]
535    pub fn require_unique_writable_accounts(&self) -> ProgramResult {
536        self.audit_accounts().require_unique_writable()
537    }
538
539    /// Require that no duplicated account is used as a signer role.
540    #[inline(always)]
541    pub fn require_unique_signer_accounts(&self) -> ProgramResult {
542        self.audit_accounts().require_unique_signers()
543    }
544
545    /// Require at least `n` bytes of instruction data.
546    #[inline(always)]
547    pub fn require_data_len(&self, n: usize) -> ProgramResult {
548        if self.instruction_data.len() >= n {
549            Ok(())
550        } else {
551            Err(ProgramError::InvalidInstructionData)
552        }
553    }
554
555    // --- Whole-Layout Typed Access ----------------------------------
556
557    /// Validate-and-load the full typed layout for an account.
558    ///
559    /// This is the indexed shortcut for `ctx.account(idx)?.load::<T>()`.
560    /// It's the canonical "Tier A" access path: the runtime checks the
561    /// Hopper header, validates the data length, and projects the typed
562    /// view in one inlined call. no extra cost over the spelled-out form.
563    #[inline(always)]
564    pub fn load<T: LayoutContract + crate::Pod>(
565        &self,
566        index: usize,
567    ) -> Result<crate::Ref<'_, T>, ProgramError> {
568        self.account(index)?.load::<T>()
569    }
570
571    /// Validate-and-load a mutable typed layout for an account.
572    ///
573    /// Indexed shortcut for `ctx.account(idx)?.load_mut::<T>()`. The
574    /// returned guard holds the account-level exclusive borrow until
575    /// it drops.
576    ///
577    /// As a whole-account write borrow, this claims `[0, data_len)`:
578    /// under an installed [write policy](Self::set_write_policy) it
579    /// requires a whole-account allowance (a plain `mut` declaration),
580    /// and with the `touch-map` feature it lands in the instruction
581    /// touch map as a full-account write record.
582    #[inline(always)]
583    pub fn load_mut<T: LayoutContract + crate::Pod>(
584        &mut self,
585        index: usize,
586    ) -> Result<crate::RefMut<'_, T>, ProgramError> {
587        let view = self.account(index)?;
588        let data_len = view.data_len() as u32;
589        self.check_write_policy(index, 0, data_len)?;
590        // The touch-map footprint records inside `try_borrow_mut` (the
591        // choke point every mutable data borrow crosses), so this path
592        // no longer stamps it explicitly, one source of truth.
593        view.load_mut::<T>()
594    }
595
596    /// Cross-program load: validate ABI fingerprint without ownership check.
597    ///
598    /// Use this when reading an account whose owner is another program but
599    /// whose layout is published as a Hopper layout contract.
600    #[inline(always)]
601    pub fn load_cross_program<T: LayoutContract + crate::Pod>(
602        &self,
603        index: usize,
604    ) -> Result<crate::Ref<'_, T>, ProgramError> {
605        self.account(index)?.load_cross_program::<T>()
606    }
607
608    // --- Segment-Level Access (fine-grained borrow tracking) --------
609
610    /// Register a read borrow for a segment of an account and return a
611    /// [`SegRef<T>`](crate::SegRef) that releases both the account-level
612    /// byte guard **and** the segment registry lease on drop.
613    ///
614    /// `index` is the account index. `abs_offset` is the absolute byte
615    /// offset within the account data (including header bytes).
616    ///
617    /// # Type Safety
618    ///
619    /// `T` must implement `Pod` (substrate-level "safe to overlay on
620    /// raw bytes" contract: every bit pattern valid, align-1, no
621    /// padding, no interior pointers). Segment borrow tracking
622    /// prevents conflicting write access to the same byte range for
623    /// the guard's lifetime.
624    ///
625    /// # Canonical path
626    ///
627    /// Three variants exist for different offset sources:
628    ///
629    /// | Variant | Use when |
630    /// |---|---|
631    /// | [`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. |
632    /// | [`segment_ref_const`](Self::segment_ref_const) | Offset comes from a runtime [`crate::Segment`] value (dispatching dynamically between named fields). |
633    /// | `segment_ref` (this method) | Offset is fully dynamic (iterating segments in a loop, for example). |
634    ///
635    /// `#[hopper::context]`-generated accessors default to the canonical
636    /// typed path; reach for the others only when the use case
637    /// genuinely needs a runtime offset.
638    #[inline(always)]
639    pub fn segment_ref<'b, T: crate::Pod>(
640        &'b mut self,
641        index: usize,
642        abs_offset: u32,
643    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
644        let view = self
645            .accounts
646            .get(index)
647            .ok_or(ProgramError::NotEnoughAccountKeys)?;
648        view.segment_ref::<T>(
649            &mut self.segment_borrows,
650            abs_offset,
651            core::mem::size_of::<T>() as u32,
652        )
653    }
654
655    /// Borrow several disjoint typed sub-ranges of one account mutably at
656    /// the same time. See
657    /// [`AccountView::split_segments_mut`](crate::AccountView::split_segments_mut).
658    ///
659    /// ```ignore
660    /// let mut segs = ctx.split_segments_mut::<WireU64, 2>(
661    ///     vault_idx, [(BALANCE_OFF, 8), (NONCE_OFF, 8)])?;
662    /// let [bal, nonce] = segs.all_mut();
663    /// bal.set(bal.get() + amount);
664    /// nonce.set(nonce.get() + 1);
665    /// ```
666    #[inline(always)]
667    pub fn split_segments_mut<'b, T: crate::Pod, const N: usize>(
668        &'b mut self,
669        index: usize,
670        ranges: [(u32, u32); N],
671    ) -> Result<crate::SegmentsMut<'b, T, N>, ProgramError> {
672        let mut i = 0;
673        while i < N {
674            self.check_write_policy(index, ranges[i].0, ranges[i].1)?;
675            i += 1;
676        }
677        let view = self
678            .accounts
679            .get(index)
680            .ok_or(ProgramError::NotEnoughAccountKeys)?;
681        view.split_segments_mut_ungated::<T, N>(&mut self.segment_borrows, ranges)
682    }
683
684    /// Register a write borrow for a segment of an account.
685    ///
686    /// Validates bounds, checks writable, and registers a leased
687    /// exclusive borrow, then returns a [`SegRefMut<T>`](crate::SegRefMut)
688    /// that releases on drop.
689    ///
690    /// This primitive permits concurrent mutation of non-overlapping account
691    /// regions. The lease model also permits sequential same-region borrows
692    /// within one instruction.
693    #[inline(always)]
694    pub fn segment_mut<'b, T: crate::Pod>(
695        &'b mut self,
696        index: usize,
697        abs_offset: u32,
698    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
699        self.check_write_policy(index, abs_offset, core::mem::size_of::<T>() as u32)?;
700        let view = self
701            .accounts
702            .get(index)
703            .ok_or(ProgramError::NotEnoughAccountKeys)?;
704        view.segment_mut_ungated::<T>(
705            &mut self.segment_borrows,
706            abs_offset,
707            core::mem::size_of::<T>() as u32,
708        )
709    }
710
711    /// Acquire a growable `Seq<T>` tail for **writing** at `body_end`
712    /// (the layout's `TAIL_PREFIX_OFFSET`), returning a
713    /// [`SeqTailWrite`](crate::tail::SeqTailWrite) guard whose
714    /// [`seq_mut`](crate::tail::SeqTailWrite::seq_mut) yields the O(1)
715    /// streaming cursor.
716    ///
717    /// The tail region is `[body_end, data_len)`, the whole account past
718    /// the fixed head. Under an installed [write policy](Self::set_write_policy)
719    /// this whole region must be granted (a `mut(<seq_field>)` declaration
720    /// compiles to an open-ended [`tail_from`](crate::write_policy::WriteRange::tail_from)
721    /// range), so the fixed head stays protected. Exactly ONE segment
722    /// lease is registered, covering the entire tail region, NOT one per
723    /// element; so overlap detection and the touch map see a single
724    /// tail-region write record regardless of how many elements are
725    /// pushed.
726    #[inline]
727    pub fn tail_seq_mut<'b, T: crate::tail::SeqElement>(
728        &'b mut self,
729        index: usize,
730        body_end: u32,
731    ) -> Result<crate::tail::SeqTailWrite<'b, T>, ProgramError> {
732        let view = self
733            .accounts
734            .get(index)
735            .ok_or(ProgramError::NotEnoughAccountKeys)?;
736        view.check_writable()?;
737        let region_len = (view.data_len() as u32)
738            .checked_sub(body_end)
739            .ok_or(ProgramError::AccountDataTooSmall)?;
740        // The whole tail region must be a granted write range (the
741        // open-ended `tail_from` range contains it; a fixed head range
742        // would refuse a grown region, exactly the protection intended).
743        self.check_write_policy(index, body_end, region_len)?;
744        // ONE write lease over the whole tail region (one touch record).
745        let borrow =
746            self.segment_borrows
747                .register_leased_write(view.address(), body_end, region_len)?;
748        let data = match view.try_borrow_mut_ungated() {
749            Ok(d) => d,
750            Err(e) => {
751                self.segment_borrows.release(&borrow);
752                return Err(e);
753            }
754        };
755        let region = data.slice_from(body_end as usize);
756        // SAFETY: `borrow` was just registered in `self.segment_borrows`;
757        // the lease releases exactly that entry on drop.
758        let lease = unsafe { crate::SegmentLease::new(&mut self.segment_borrows, borrow) };
759        Ok(crate::tail::SeqTailWrite::new(region, lease))
760    }
761
762    /// Acquire a `Seq<T>` tail for **reading** at `body_end`, returning a
763    /// [`SeqTailRead`](crate::tail::SeqTailRead) guard whose
764    /// [`seq`](crate::tail::SeqTailRead::seq) yields the streaming read
765    /// cursor. Registers one shared tail-region lease (reads are not
766    /// gated by the write policy, but the lease still powers overlap
767    /// detection against concurrent writers).
768    #[inline]
769    pub fn tail_seq_ref<'b, T: crate::tail::SeqElement>(
770        &'b mut self,
771        index: usize,
772        body_end: u32,
773    ) -> Result<crate::tail::SeqTailRead<'b, T>, ProgramError> {
774        let view = self
775            .accounts
776            .get(index)
777            .ok_or(ProgramError::NotEnoughAccountKeys)?;
778        let region_len = (view.data_len() as u32)
779            .checked_sub(body_end)
780            .ok_or(ProgramError::AccountDataTooSmall)?;
781        let borrow =
782            self.segment_borrows
783                .register_leased_read(view.address(), body_end, region_len)?;
784        let data = match view.try_borrow() {
785            Ok(d) => d,
786            Err(e) => {
787                self.segment_borrows.release(&borrow);
788                return Err(e);
789            }
790        };
791        let region = data.slice_from(body_end as usize);
792        // SAFETY: `borrow` was just registered in `self.segment_borrows`;
793        // the lease releases exactly that entry on drop.
794        let lease = unsafe { crate::SegmentLease::new(&mut self.segment_borrows, borrow) };
795        Ok(crate::tail::SeqTailRead::new(region, lease))
796    }
797
798    /// Const-driven segment read: pass a compile-time [`crate::Segment`] and the
799    /// account index. Lowers to the same pointer-plus-const-offset shape
800    /// as `segment_ref` but without the caller hand-rolling the offset +
801    /// size arguments.
802    #[inline(always)]
803    pub fn segment_ref_const<'b, T: crate::Pod>(
804        &'b mut self,
805        index: usize,
806        segment: crate::Segment,
807    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
808        let view = self
809            .accounts
810            .get(index)
811            .ok_or(ProgramError::NotEnoughAccountKeys)?;
812        view.segment_ref_const::<T>(&mut self.segment_borrows, segment)
813    }
814
815    /// Const-driven exclusive segment access. Pair with
816    /// `#[hopper::state]` constants for zero-overhead field writes.
817    #[inline(always)]
818    pub fn segment_mut_const<'b, T: crate::Pod>(
819        &'b mut self,
820        index: usize,
821        segment: crate::Segment,
822    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
823        self.check_write_policy(index, segment.offset, segment.size)?;
824        let view = self
825            .accounts
826            .get(index)
827            .ok_or(ProgramError::NotEnoughAccountKeys)?;
828        view.segment_mut_ungated::<T>(&mut self.segment_borrows, segment.offset, segment.size)
829    }
830
831    /// Typed-segment read: the type and offset are both compile-time
832    /// constants, baked into a [`crate::TypedSegment`] zero-sized marker.
833    #[inline(always)]
834    pub fn segment_ref_typed<'b, T: crate::Pod, const OFFSET: u32>(
835        &'b mut self,
836        index: usize,
837        segment: crate::TypedSegment<T, OFFSET>,
838    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
839        let view = self
840            .accounts
841            .get(index)
842            .ok_or(ProgramError::NotEnoughAccountKeys)?;
843        view.segment_ref_typed::<T, OFFSET>(&mut self.segment_borrows, segment)
844    }
845
846    /// Typed-segment write. Mirrors [`Self::segment_ref_typed`] for the
847    /// exclusive path.
848    #[inline(always)]
849    pub fn segment_mut_typed<'b, T: crate::Pod, const OFFSET: u32>(
850        &'b mut self,
851        index: usize,
852        _segment: crate::TypedSegment<T, OFFSET>,
853    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
854        self.check_write_policy(index, OFFSET, core::mem::size_of::<T>() as u32)?;
855        let view = self
856            .accounts
857            .get(index)
858            .ok_or(ProgramError::NotEnoughAccountKeys)?;
859        view.segment_mut_ungated::<T>(
860            &mut self.segment_borrows,
861            OFFSET,
862            core::mem::size_of::<T>() as u32,
863        )
864    }
865
866    /// Explicit unsafe whole-account typed read.
867    #[inline(always)]
868    ///
869    /// # Safety
870    ///
871    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
872    pub unsafe fn raw_ref<T: crate::Pod>(
873        &self,
874        index: usize,
875    ) -> Result<crate::Ref<'_, T>, ProgramError> {
876        let view = self
877            .accounts
878            .get(index)
879            .ok_or(ProgramError::NotEnoughAccountKeys)?;
880        // SAFETY: This function's `# Safety` contract is the callee's,
881        // forwarded unchanged.
882        unsafe { view.raw_ref::<T>() }
883    }
884
885    /// Explicit unsafe whole-account typed write.
886    #[inline(always)]
887    ///
888    /// # Safety
889    ///
890    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
891    pub unsafe fn raw_mut<T: crate::Pod>(
892        &self,
893        index: usize,
894    ) -> Result<crate::RefMut<'_, T>, ProgramError> {
895        let view = self
896            .accounts
897            .get(index)
898            .ok_or(ProgramError::NotEnoughAccountKeys)?;
899        // Whole-account write claim: an installed write policy gates the
900        // raw path exactly like `load_mut` (coarse, never under-claims).
901        self.check_write_policy(index, 0, view.data_len() as u32)?;
902        // SAFETY: This function's `# Safety` contract is the callee's,
903        // forwarded unchanged.
904        unsafe { view.raw_mut::<T>() }
905    }
906
907    /// Legacy alias for [`raw_mut`](Self::raw_mut).
908    ///
909    /// Despite the name, this does **not** bypass borrow tracking: it
910    /// delegates to `raw_mut`, which routes through the checked
911    /// `segment_mut(0, size_of::<T>())` path (bounds, writable, and
912    /// account-level exclusive borrow all enforced). The caller remains
913    /// responsible for using a type that matches the account bytes. For a
914    /// genuinely untracked pointer, use [`as_mut_ptr`](Self::as_mut_ptr).
915    #[inline(always)]
916    ///
917    /// # Safety
918    ///
919    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
920    pub unsafe fn raw_unchecked<T: crate::Pod>(
921        &self,
922        index: usize,
923    ) -> Result<crate::RefMut<'_, T>, ProgramError> {
924        // SAFETY: This function's `# Safety` contract is the callee's,
925        // forwarded unchanged.
926        unsafe { self.raw_mut::<T>(index) }
927    }
928
929    /// Canonical raw-pointer escape hatch to an account's data buffer.
930    ///
931    /// Returns a pointer to the first byte of `accounts[index]`'s data
932    /// region (after the runtime account header, before any Hopper
933    /// 16-byte layout header). The pointer is valid for reads and
934    /// writes for the lifetime of the account view and carries no
935    /// borrow-tracking obligations. Dereferencing it is `unsafe`
936    /// because the caller takes over alias-safety responsibility
937    /// that the segment registry normally upholds.
938    ///
939    /// This is the explicit power-user primitive the audit asks for:
940    /// safe code reaches for `segment_ref_typed` / `segment_mut_typed`
941    /// / the generated `ctx.<field>_segment_mut(...)` accessors; raw
942    /// code drops to `unsafe { ctx.as_mut_ptr(0)?.add(offset) as *mut T }`.
943    ///
944    /// # Safety
945    ///
946    /// The caller must guarantee no aliasing mutable borrow is held
947    /// on the same account for the duration of any write through the
948    /// returned pointer. The returned pointer must be dereferenced
949    /// within the `'info` lifetime of the account view; reading past
950    /// `AccountView::data_len()` is undefined behaviour.
951    #[inline(always)]
952    pub unsafe fn as_mut_ptr(&self, index: usize) -> Result<*mut u8, ProgramError> {
953        let view = self
954            .accounts
955            .get(index)
956            .ok_or(ProgramError::NotEnoughAccountKeys)?;
957        view.require_writable()?;
958        // The untracked pointer is whole-account write capability, so an
959        // installed write policy must have granted the whole account.
960        self.check_write_policy(index, 0, view.data_len() as u32)?;
961        // SAFETY: the account view is live for `'info` and
962        // `data_ptr` yields a pointer inside the loader-provided
963        // per-account buffer. Returning the untyped pointer transfers
964        // alias-safety to the caller as documented above.
965        Ok(view.data_ptr_unchecked())
966    }
967
968    /// Immutable sibling of [`as_mut_ptr`]. Returns a `*const u8`.
969    ///
970    /// Shared-borrow checking still runs, so calling this while an
971    /// exclusive borrow is live on the same account fails with
972    /// `AccountBorrowFailed`. The return value is safe to obtain; the
973    /// caller only needs `unsafe` to dereference it.
974    ///
975    /// [`as_mut_ptr`]: Self::as_mut_ptr
976    #[inline(always)]
977    pub fn as_ptr(&self, index: usize) -> Result<*const u8, ProgramError> {
978        let view = self
979            .accounts
980            .get(index)
981            .ok_or(ProgramError::NotEnoughAccountKeys)?;
982        view.check_borrow()?;
983        Ok(view.data_ptr_unchecked() as *const u8)
984    }
985
986    /// Read instruction data as a typed value (unaligned, little-endian safe).
987    ///
988    /// Reads `size_of::<T>()` bytes starting at `offset` via `read_unaligned`.
989    /// Caller must ensure `T` is a plain-old-data type where all bit patterns
990    /// are valid.
991    #[inline(always)]
992    pub fn read_data<T: crate::ValuePod>(&self, offset: usize) -> Result<T, ProgramError> {
993        let end = offset
994            .checked_add(core::mem::size_of::<T>())
995            .ok_or(ProgramError::ArithmeticOverflow)?;
996        if self.instruction_data.len() < end {
997            return Err(ProgramError::InvalidInstructionData);
998        }
999        // SAFETY: bounds checked; `T: ValuePod` guarantees every bit
1000        // pattern is valid by value and the type has no drop glue, so
1001        // `read_unaligned` from instruction data is sound.
1002        Ok(unsafe {
1003            core::ptr::read_unaligned(self.instruction_data.as_ptr().add(offset) as *const T)
1004        })
1005    }
1006
1007    /// Get a byte slice from instruction data.
1008    #[inline(always)]
1009    pub fn data_slice(&self, offset: usize, len: usize) -> Result<&[u8], ProgramError> {
1010        let end = offset
1011            .checked_add(len)
1012            .ok_or(ProgramError::ArithmeticOverflow)?;
1013        if self.instruction_data.len() < end {
1014            return Err(ProgramError::InvalidInstructionData);
1015        }
1016        Ok(&self.instruction_data[offset..end])
1017    }
1018
1019    /// Read the first byte of instruction data as an instruction tag.
1020    ///
1021    /// Common pattern for byte-tag dispatch.
1022    #[inline(always)]
1023    pub fn instruction_tag(&self) -> Result<u8, ProgramError> {
1024        self.instruction_data
1025            .first()
1026            .copied()
1027            .ok_or(ProgramError::InvalidInstructionData)
1028    }
1029}
1030
1031/// Borrow-scoped view of a [`Context`].
1032///
1033/// Generated typed contexts expose this wrapper from their safe `raw()` method
1034/// instead of returning `&mut Context<'a>` directly. That keeps account and
1035/// remaining-account references tied to the borrow of the generated context,
1036/// preventing backend account-view lifetimes from being widened through the raw
1037/// escape hatch.
1038pub struct ScopedContext<'ctx, 'a> {
1039    inner: &'ctx mut Context<'a>,
1040}
1041
1042impl<'ctx, 'a> ScopedContext<'ctx, 'a> {
1043    /// Create a borrow-scoped wrapper around a raw Hopper context.
1044    #[inline(always)]
1045    pub fn new(inner: &'ctx mut Context<'a>) -> Self {
1046        Self { inner }
1047    }
1048
1049    /// Program ID, narrowed to the wrapper borrow lifetime.
1050    #[inline(always)]
1051    pub fn program_id(&self) -> &'ctx Address {
1052        self.inner.program_id
1053    }
1054
1055    /// Raw instruction data, narrowed to the wrapper borrow lifetime.
1056    #[inline(always)]
1057    pub fn instruction_data(&self) -> &'ctx [u8] {
1058        self.inner.instruction_data
1059    }
1060
1061    /// Get an account by index, narrowed to the wrapper borrow lifetime.
1062    #[inline(always)]
1063    pub fn account(&self, index: usize) -> Result<&'ctx AccountView<'a>, ProgramError> {
1064        self.inner
1065            .accounts
1066            .get(index)
1067            .ok_or(ProgramError::NotEnoughAccountKeys)
1068    }
1069
1070    /// Mutation-intent account access, narrowed to the wrapper borrow lifetime.
1071    #[inline(always)]
1072    pub fn account_mut(&self, index: usize) -> Result<&'ctx AccountView<'a>, ProgramError> {
1073        self.account(index)
1074    }
1075
1076    /// Get the total number of accounts.
1077    #[inline(always)]
1078    pub fn num_accounts(&self) -> usize {
1079        self.inner.num_accounts()
1080    }
1081
1082    /// Get all accounts as a slice, narrowed to the wrapper borrow lifetime.
1083    #[inline(always)]
1084    pub fn accounts(&self) -> &'ctx [AccountView<'a>] {
1085        self.inner.accounts
1086    }
1087
1088    /// Borrow one runtime-selected typed byte range for reading.
1089    ///
1090    /// This is the safe bridge for generated typed contexts whose semantic
1091    /// column is known statically but whose cell offset is selected at runtime
1092    /// (for example, a slot in a column-oriented intent shard). The returned
1093    /// guard remains tied to this scoped context borrow and participates in the
1094    /// instruction segment-borrow ledger.
1095    #[inline(always)]
1096    pub fn segment_ref<'b, T: crate::Pod>(
1097        &'b mut self,
1098        index: usize,
1099        abs_offset: u32,
1100    ) -> Result<crate::SegRef<'b, T>, ProgramError> {
1101        self.inner.segment_ref::<T>(index, abs_offset)
1102    }
1103
1104    /// Borrow one runtime-selected typed byte range for mutation.
1105    ///
1106    /// The underlying [`Context::segment_mut`] performs the active
1107    /// `strict_writes` containment check before registering the exclusive
1108    /// segment lease, so exposing this method does not create a policy escape.
1109    /// It lets typed handlers keep their generated manifest/IDL metadata while
1110    /// selecting an exact cell inside a declared column at runtime.
1111    #[inline(always)]
1112    pub fn segment_mut<'b, T: crate::Pod>(
1113        &'b mut self,
1114        index: usize,
1115        abs_offset: u32,
1116    ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
1117        self.inner.segment_mut::<T>(index, abs_offset)
1118    }
1119
1120    /// Borrow several disjoint runtime-selected typed ranges for mutation.
1121    /// Every range is checked against the active write policy before any lease
1122    /// is granted.
1123    #[inline(always)]
1124    pub fn split_segments_mut<'b, T: crate::Pod, const N: usize>(
1125        &'b mut self,
1126        index: usize,
1127        ranges: [(u32, u32); N],
1128    ) -> Result<crate::SegmentsMut<'b, T, N>, ProgramError> {
1129        self.inner.split_segments_mut::<T, N>(index, ranges)
1130    }
1131
1132    /// Access the instruction-scoped segment borrow registry.
1133    #[inline(always)]
1134    pub fn borrows(&self) -> &SegmentBorrowRegistry {
1135        &self.inner.segment_borrows
1136    }
1137
1138    /// Mutably access the instruction-scoped segment borrow registry.
1139    #[inline(always)]
1140    pub fn borrows_mut(&mut self) -> &mut SegmentBorrowRegistry {
1141        &mut self.inner.segment_borrows
1142    }
1143
1144    /// Inspect the currently reachable account slice for duplicate aliases.
1145    #[inline(always)]
1146    pub fn audit_accounts(&self) -> AccountAudit<'ctx> {
1147        AccountAudit::new(self.inner.accounts)
1148    }
1149
1150    /// Get the remaining accounts starting at `from`, narrowed to the wrapper
1151    /// borrow lifetime.
1152    #[inline(always)]
1153    pub fn remaining_accounts(&self, from: usize) -> &'ctx [AccountView<'a>] {
1154        if from >= self.inner.accounts.len() {
1155            &[]
1156        } else {
1157            &self.inner.accounts[from..]
1158        }
1159    }
1160
1161    /// Get remaining accounts in strict duplicate-rejecting mode.
1162    #[inline(always)]
1163    pub fn remaining_accounts_strict(
1164        &self,
1165        from: usize,
1166    ) -> crate::remaining::RemainingAccounts<'ctx> {
1167        let declared_end = from.min(self.inner.accounts.len());
1168        crate::remaining::RemainingAccounts::strict(
1169            &self.inner.accounts[..declared_end],
1170            self.remaining_accounts(from),
1171        )
1172    }
1173
1174    /// Get remaining accounts in duplicate-preserving passthrough mode.
1175    #[inline(always)]
1176    pub fn remaining_accounts_passthrough(
1177        &self,
1178        from: usize,
1179    ) -> crate::remaining::RemainingAccounts<'ctx> {
1180        let declared_end = from.min(self.inner.accounts.len());
1181        crate::remaining::RemainingAccounts::passthrough(
1182            &self.inner.accounts[..declared_end],
1183            self.remaining_accounts(from),
1184        )
1185    }
1186
1187    /// Get remaining accounts in strict mode and bind a sequential typed parser.
1188    #[inline(always)]
1189    pub fn remaining_accounts_typed(&self, from: usize) -> crate::remaining::RemainingTyped<'ctx> {
1190        self.remaining_accounts_strict(from).typed()
1191    }
1192
1193    /// Get remaining accounts in strict mode and bind a lazy indexed parser.
1194    #[inline(always)]
1195    pub fn remaining_accounts_lazy(&self, from: usize) -> crate::remaining::RemainingLazy<'ctx> {
1196        self.remaining_accounts_strict(from).lazy()
1197    }
1198
1199    /// Require at least `n` accounts are present.
1200    #[inline(always)]
1201    pub fn require_accounts(&self, n: usize) -> ProgramResult {
1202        self.inner.require_accounts(n)
1203    }
1204
1205    /// Require all account addresses to be unique.
1206    #[inline(always)]
1207    pub fn require_unique_accounts(&self) -> ProgramResult {
1208        self.audit_accounts().require_all_unique()
1209    }
1210
1211    /// Require that no duplicated account is writable in this instruction.
1212    #[inline(always)]
1213    pub fn require_unique_writable_accounts(&self) -> ProgramResult {
1214        self.audit_accounts().require_unique_writable()
1215    }
1216
1217    /// Require that no duplicated account is used as a signer role.
1218    #[inline(always)]
1219    pub fn require_unique_signer_accounts(&self) -> ProgramResult {
1220        self.audit_accounts().require_unique_signers()
1221    }
1222
1223    /// Require at least `n` bytes of instruction data.
1224    #[inline(always)]
1225    pub fn require_data_len(&self, n: usize) -> ProgramResult {
1226        self.inner.require_data_len(n)
1227    }
1228
1229    /// Read instruction data as a typed value.
1230    #[inline(always)]
1231    pub fn read_data<T: crate::ValuePod>(&self, offset: usize) -> Result<T, ProgramError> {
1232        self.inner.read_data(offset)
1233    }
1234
1235    /// Get a byte slice from instruction data.
1236    #[inline(always)]
1237    pub fn data_slice(&self, offset: usize, len: usize) -> Result<&'ctx [u8], ProgramError> {
1238        let end = offset
1239            .checked_add(len)
1240            .ok_or(ProgramError::ArithmeticOverflow)?;
1241        self.inner
1242            .instruction_data
1243            .get(offset..end)
1244            .ok_or(ProgramError::InvalidInstructionData)
1245    }
1246}
1247
1248// ── Tests ────────────────────────────────────────────────────────────
1249
1250/// Whether two views name the same loader record.
1251///
1252/// The loader serializes a repeated account once and marks later slots as
1253/// duplicates of it, so on chain identity is the record pointer. The host
1254/// harness builds one record per slot, so there the addresses are compared
1255/// by value.
1256#[inline(always)]
1257fn same_record(left: &AccountView<'_>, right: &AccountView<'_>) -> bool {
1258    #[cfg(target_os = "solana")]
1259    {
1260        core::ptr::eq(left.address(), right.address())
1261    }
1262    #[cfg(not(target_os = "solana"))]
1263    {
1264        left.address() == right.address()
1265    }
1266}
1267
1268#[cfg(test)]
1269mod write_policy_tests {
1270    use super::*;
1271    use crate::write_policy::{WritePolicy, WriteRange};
1272    use hopper_native::{
1273        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
1274    };
1275
1276    const DATA_LEN: usize = 64;
1277    const BALANCE_OFF: u32 = 16;
1278    const NONCE_OFF: u32 = 24;
1279
1280    fn make_account(address_byte: u8) -> (std::vec::Vec<u64>, AccountView<'static>) {
1281        // Word-sized backing: `RuntimeAccount` has u64 fields (align 8) and a
1282        // `Vec<u8>` allocation only guarantees alignment 1, writing the
1283        // header through an under-aligned pointer is UB by spec even where
1284        // the system allocator happens to over-align. Caught by the Miri
1285        // Tree Borrows lane (`scripts/miri-core.*`); same fix as the
1286        // competitor_bug_classes fixtures (adversarial review 2026-07-07).
1287        let total = RuntimeAccount::SIZE + DATA_LEN;
1288        let mut backing = std::vec![0u64; total.div_ceil(8)];
1289        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1290        // SAFETY: `backing` is sized for the header plus DATA_LEN bytes,
1291        // 8-aligned by construction, and outlives the returned view (the
1292        // caller holds the Vec).
1293        unsafe {
1294            raw.write(RuntimeAccount {
1295                borrow_state: NOT_BORROWED,
1296                is_signer: 1,
1297                is_writable: 1,
1298                executable: 0,
1299                resize_delta: 0,
1300                address: NativeAddress::new_from_array([address_byte; 32]),
1301                owner: NativeAddress::new_from_array([2; 32]),
1302                lamports: 42,
1303                data_len: DATA_LEN as u64,
1304            });
1305        }
1306        // SAFETY: `raw` points at a fully initialized RuntimeAccount with
1307        // its data region in the same allocation.
1308        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1309        (backing, AccountView::from_backend(backend))
1310    }
1311
1312    // Field-granular policy on account 0: balance + nonce only.
1313    static FIELD_POLICY: WritePolicy = WritePolicy::new(&[
1314        WriteRange::new(0, BALANCE_OFF, 8),
1315        WriteRange::new(0, NONCE_OFF, 8),
1316    ]);
1317    // Whole-account allowance on account 0 (a plain `mut` declaration).
1318    static WHOLE_POLICY: WritePolicy = WritePolicy::new(&[WriteRange::whole_account(0)]);
1319
1320    /// One account in two mutable roles is refused; distinct accounts and
1321    /// a missing slot keep their own errors.
1322    #[test]
1323    fn distinct_slots_refuses_one_account_in_two_roles() {
1324        let (_a, first) = make_account(1);
1325        let (_b, second) = make_account(1);
1326        let (_c, third) = make_account(3);
1327        let accounts = [first, second, third];
1328        let pid = Address::new([9u8; 32]);
1329        let ctx = Context::new(&pid, &accounts, &[]);
1330
1331        assert_eq!(ctx.require_distinct_slots(&[]), Ok(()));
1332        assert_eq!(ctx.require_distinct_slots(&[(0, 2), (1, 2)]), Ok(()));
1333        assert_eq!(
1334            ctx.require_distinct_slots(&[(0, 2), (0, 1)]),
1335            Err(crate::ERR_ALIASED_MUTABLE_ACCOUNTS)
1336        );
1337        assert_eq!(
1338            ctx.require_distinct_slots(&[(0, 5)]),
1339            Err(ProgramError::NotEnoughAccountKeys)
1340        );
1341    }
1342
1343    #[test]
1344    fn no_policy_leaves_every_write_path_open() {
1345        let (_b, account) = make_account(1);
1346        let accounts = [account];
1347        let pid = Address::new([9u8; 32]);
1348        let mut ctx = Context::new(&pid, &accounts, &[]);
1349
1350        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1351        assert!(ctx.segment_mut::<[u8; 4]>(0, 0).is_ok());
1352    }
1353
1354    #[test]
1355    fn field_policy_allows_declared_segments_and_refuses_the_rest() {
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(&FIELD_POLICY);
1361
1362        // Declared ranges work, including disjoint simultaneous writes.
1363        {
1364            let mut segs = ctx
1365                .split_segments_mut::<[u8; 8], 2>(0, [(BALANCE_OFF, 8), (NONCE_OFF, 8)])
1366                .unwrap();
1367            let [bal, nonce] = segs.all_mut();
1368            bal[0] = 1;
1369            nonce[0] = 2;
1370        }
1371        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1372
1373        // An undeclared range is refused with the indexed policy error,
1374        // before any borrow state changes, so reads still work after.
1375        assert_eq!(
1376            ctx.segment_mut::<[u8; 8]>(0, 0).unwrap_err(),
1377            crate::write_policy::write_policy_violation(0)
1378        );
1379        assert!(ctx.segment_ref::<[u8; 8]>(0, 0).is_ok());
1380
1381        // A split where ONE range is undeclared is refused whole.
1382        assert!(ctx
1383            .split_segments_mut::<[u8; 8], 2>(0, [(BALANCE_OFF, 8), (0, 8)])
1384            .is_err());
1385
1386        // Reads are never policy-gated.
1387        assert!(ctx.segment_ref::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1388    }
1389
1390    #[test]
1391    fn scoped_context_runtime_segments_preserve_write_policy() {
1392        let (_b, account) = make_account(1);
1393        let accounts = [account];
1394        let pid = Address::new([9u8; 32]);
1395        let mut ctx = Context::new(&pid, &accounts, &[]);
1396        ctx.set_write_policy(&FIELD_POLICY);
1397
1398        {
1399            let mut scoped = ScopedContext::new(&mut ctx);
1400            let mut balance = scoped
1401                .segment_mut::<[u8; 8]>(0, BALANCE_OFF)
1402                .expect("declared runtime-selected range must be writable");
1403            balance[0] = 7;
1404        }
1405
1406        {
1407            let mut scoped = ScopedContext::new(&mut ctx);
1408            assert_eq!(
1409                scoped.segment_mut::<[u8; 8]>(0, 0).unwrap_err(),
1410                crate::write_policy::write_policy_violation(0),
1411            );
1412            assert!(scoped.segment_ref::<[u8; 8]>(0, 0).is_ok());
1413        }
1414    }
1415
1416    #[test]
1417    fn field_policy_refuses_whole_account_write_paths() {
1418        let (_b, account) = make_account(1);
1419        let accounts = [account];
1420        let pid = Address::new([9u8; 32]);
1421        let mut ctx = Context::new(&pid, &accounts, &[]);
1422        ctx.set_write_policy(&FIELD_POLICY);
1423
1424        // The untracked whole-account pointer is whole-account write
1425        // capability: a field-granular policy must refuse it.
1426        // SAFETY: never dereferenced; testing the acquire-time gate only.
1427        assert_eq!(
1428            unsafe { ctx.as_mut_ptr(0) }.unwrap_err(),
1429            crate::write_policy::write_policy_violation(0)
1430        );
1431    }
1432
1433    #[test]
1434    fn whole_account_allowance_admits_all_write_paths() {
1435        let (_b, account) = make_account(1);
1436        let accounts = [account];
1437        let pid = Address::new([9u8; 32]);
1438        let mut ctx = Context::new(&pid, &accounts, &[]);
1439        ctx.set_write_policy(&WHOLE_POLICY);
1440
1441        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1442        // SAFETY: never dereferenced; testing the acquire-time gate only.
1443        assert!(unsafe { ctx.as_mut_ptr(0) }.is_ok());
1444    }
1445
1446    #[test]
1447    fn empty_policy_is_a_machine_checked_read_only_instruction() {
1448        static READ_ONLY: WritePolicy = WritePolicy::new(&[]);
1449        let (_b, account) = make_account(1);
1450        let accounts = [account];
1451        let pid = Address::new([9u8; 32]);
1452        let mut ctx = Context::new(&pid, &accounts, &[]);
1453        ctx.set_write_policy(&READ_ONLY);
1454
1455        assert!(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).is_err());
1456        // SAFETY: never dereferenced; testing the acquire-time gate only.
1457        assert!(unsafe { ctx.as_mut_ptr(0) }.is_err());
1458        // Reads remain untouched.
1459        assert!(ctx.segment_ref::<[u8; 8]>(0, BALANCE_OFF).is_ok());
1460        assert!(ctx.as_ptr(0).is_ok());
1461    }
1462
1463    #[repr(C)]
1464    #[derive(Clone, Copy)]
1465    struct PolicyLayout {
1466        a: [u8; 8],
1467    }
1468    // SAFETY: repr(C), all-byte-array fields, every bit pattern valid,
1469    // no padding, align 1.
1470    unsafe impl crate::Zeroable for PolicyLayout {}
1471    // SAFETY: as above.
1472    unsafe impl crate::Pod for PolicyLayout {}
1473    impl crate::field_map::FieldMap for PolicyLayout {
1474        const FIELDS: &'static [crate::field_map::FieldInfo] = &[crate::field_map::FieldInfo::new(
1475            "a",
1476            crate::layout::HopperHeader::SIZE,
1477            8,
1478        )];
1479    }
1480    impl LayoutContract for PolicyLayout {
1481        const DISC: u8 = 77;
1482        const VERSION: u8 = 1;
1483        const LAYOUT_ID: [u8; 8] = [0x77; 8];
1484        const SIZE: usize = crate::layout::HopperHeader::SIZE + core::mem::size_of::<Self>();
1485    }
1486
1487    #[test]
1488    fn load_mut_is_gated_before_header_validation_or_borrow() {
1489        let (_b, account) = make_account(1);
1490        let accounts = [account];
1491        let pid = Address::new([9u8; 32]);
1492        let mut ctx = Context::new(&pid, &accounts, &[]);
1493        ctx.set_write_policy(&FIELD_POLICY);
1494
1495        // The whole-account claim `[0, data_len)` is refused by a
1496        // field-granular policy, with the policy error, not a layout
1497        // error, proving the gate runs before any borrow or header read.
1498        assert_eq!(
1499            ctx.load_mut::<PolicyLayout>(0).unwrap_err(),
1500            crate::write_policy::write_policy_violation(0)
1501        );
1502
1503        // Under a whole-account allowance the gate passes; the account
1504        // has no valid Hopper header, so whatever happens next it is not
1505        // a policy refusal.
1506        let mut ctx2 = Context::new(&pid, &accounts, &[]);
1507        ctx2.set_write_policy(&WHOLE_POLICY);
1508        assert_ne!(
1509            ctx2.load_mut::<PolicyLayout>(0).unwrap_err(),
1510            crate::write_policy::write_policy_violation(0)
1511        );
1512    }
1513
1514    #[cfg(feature = "touch-map")]
1515    #[test]
1516    fn whole_account_load_mut_lands_in_the_touch_map() {
1517        use crate::segment_borrow::AccessKind;
1518
1519        let (_b, account) = make_account(1);
1520        let accounts = [account];
1521        let pid = Address::new([9u8; 32]);
1522        let mut ctx = Context::new(&pid, &accounts, &[]);
1523
1524        // A segment write followed by a whole-account borrow recorded
1525        // through the same ledger: the touch map now sees both shapes.
1526        drop(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1527        {
1528            let view = ctx.account(0).unwrap();
1529            let data_len = view.data_len() as u32;
1530            let addr = *view.address();
1531            ctx.borrows_mut()
1532                .record_account_touch(&addr, data_len, AccessKind::Write);
1533        }
1534
1535        let mut seen = std::vec::Vec::new();
1536        ctx.for_each_touch(|t| seen.push((t.offset, t.size, t.kind)));
1537        assert_eq!(seen.len(), 2);
1538        assert_eq!(seen[0], (BALANCE_OFF, 8, AccessKind::Write));
1539        assert_eq!(seen[1], (0, DATA_LEN as u32, AccessKind::Write));
1540    }
1541
1542    #[cfg(feature = "touch-map")]
1543    #[test]
1544    fn touch_map_emission_round_trips_through_the_wire_format() {
1545        use crate::segment_borrow::{decode_touch_map_for_tests, AccessKind, TouchMapRecord};
1546
1547        let (_b0, account0) = make_account(1);
1548        let (_b1, account1) = make_account(2);
1549        let accounts = [account0, account1];
1550        let pid = Address::new([9u8; 32]);
1551        let mut ctx = Context::new(&pid, &accounts, &[]);
1552
1553        // A write and a read on account 1, a read on account 0, and a
1554        // whole-account write on account 0, every touch shape.
1555        drop(ctx.segment_mut::<[u8; 8]>(1, BALANCE_OFF).unwrap());
1556        drop(ctx.segment_ref::<[u8; 8]>(1, NONCE_OFF).unwrap());
1557        drop(ctx.segment_ref::<[u8; 4]>(0, 0).unwrap());
1558        {
1559            let view = ctx.account(0).unwrap();
1560            let (data_len, addr) = (view.data_len() as u32, *view.address());
1561            ctx.borrows_mut()
1562                .record_account_touch(&addr, data_len, AccessKind::Write);
1563        }
1564
1565        let (buf, len) = ctx.encode_touch_map();
1566        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1567        assert_eq!(flags, 0, "complete map must carry no overflow/skip flags");
1568        assert_eq!(
1569            records,
1570            std::vec![
1571                TouchMapRecord {
1572                    slot: 1,
1573                    offset: BALANCE_OFF,
1574                    size: 8,
1575                    write: true,
1576                },
1577                TouchMapRecord {
1578                    slot: 1,
1579                    offset: NONCE_OFF,
1580                    size: 8,
1581                    write: false,
1582                },
1583                TouchMapRecord {
1584                    slot: 0,
1585                    offset: 0,
1586                    size: 4,
1587                    write: false,
1588                },
1589                TouchMapRecord {
1590                    slot: 0,
1591                    offset: 0,
1592                    size: DATA_LEN as u32,
1593                    write: true,
1594                },
1595            ]
1596        );
1597
1598        // Off-chain the syscall is a no-op; the call must still be safe.
1599        ctx.emit_touch_map();
1600    }
1601
1602    #[cfg(feature = "touch-map")]
1603    #[test]
1604    fn touch_map_emission_marks_overflow_honestly() {
1605        use crate::segment_borrow::{decode_touch_map_for_tests, TOUCH_MAP_FLAG_OVERFLOWED};
1606
1607        let (_b, account) = make_account(1);
1608        let accounts = [account];
1609        let pid = Address::new([9u8; 32]);
1610        let mut ctx = Context::new(&pid, &accounts, &[]);
1611
1612        // Touch more distinct ranges than the log capacity. The stride
1613        // leaves a one-byte gap between consecutive ranges so no exact
1614        // union exists, coalescing (which keeps contiguous workloads
1615        // complete) cannot save this map, and the honest outcome is the
1616        // wire-visible overflow flag.
1617        let addr = *ctx.account(0).unwrap().address();
1618        let mut i: u32 = 0;
1619        while (i as usize) < crate::segment_borrow::MAX_TOUCH_RECORDS + 3 {
1620            let b = ctx
1621                .borrows_mut()
1622                .register_leased_read(&addr, i * 2, 1)
1623                .unwrap();
1624            ctx.borrows_mut().release(&b);
1625            i += 1;
1626        }
1627        assert!(ctx.touch_map_overflowed());
1628
1629        let (buf, len) = ctx.encode_touch_map();
1630        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1631        assert_eq!(flags & TOUCH_MAP_FLAG_OVERFLOWED, TOUCH_MAP_FLAG_OVERFLOWED);
1632        assert_eq!(records.len(), crate::segment_borrow::MAX_TOUCH_RECORDS);
1633    }
1634
1635    /// The opt-in epilogue helper the generated dispatcher calls on the
1636    /// handler's Ok path when the context declared
1637    /// `#[hopper::context(emit_touch_map)]`. With the `touch-map` feature
1638    /// on it must finalize the instruction exactly like a hand-written
1639    /// `emit_touch_map`: snapshot the cumulative touch log and hand the
1640    /// same wire bytes the encoder produces to `sol_log_data`. Off-chain
1641    /// the syscall is a no-op, so we prove the record is decodable via the
1642    /// shared encoder and that calling the helper is safe.
1643    #[cfg(feature = "touch-map")]
1644    #[test]
1645    fn finish_with_touch_map_finalizes_a_decodable_record() {
1646        use crate::segment_borrow::{decode_touch_map_for_tests, TouchMapRecord};
1647
1648        let (_b, account) = make_account(1);
1649        let accounts = [account];
1650        let pid = Address::new([9u8; 32]);
1651        let mut ctx = Context::new(&pid, &accounts, &[]);
1652
1653        // A bound handler with the opt-in touches state, then the
1654        // dispatcher calls `finish_with_touch_map()` on the Ok path.
1655        drop(ctx.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1656        drop(ctx.segment_ref::<[u8; 8]>(0, NONCE_OFF).unwrap());
1657
1658        // The record the epilogue emits is byte-identical to the encoder's
1659        // output (what `emit_touch_map` / `finish_with_touch_map` send).
1660        let (buf, len) = ctx.encode_touch_map();
1661        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1662        assert_eq!(flags, 0, "complete map carries no overflow/skip flags");
1663        assert_eq!(
1664            records,
1665            std::vec![
1666                TouchMapRecord {
1667                    slot: 0,
1668                    offset: BALANCE_OFF,
1669                    size: 8,
1670                    write: true,
1671                },
1672                TouchMapRecord {
1673                    slot: 0,
1674                    offset: NONCE_OFF,
1675                    size: 8,
1676                    write: false,
1677                },
1678            ]
1679        );
1680
1681        // The helper the dispatcher calls on the Ok path: off-chain
1682        // no-op, must be safe and must not disturb the recorded footprint.
1683        ctx.finish_with_touch_map();
1684    }
1685
1686    /// The mirror of the opt-in: a context that touched nothing (a handler
1687    /// that did no state access, or one whose context did NOT opt in and
1688    /// so whose dispatcher never calls the helper) has an empty footprint,
1689    /// the epilogue would emit a header-only record with zero touch
1690    /// entries. This pins "without the opt-in / without touches, produces
1691    /// none".
1692    #[cfg(feature = "touch-map")]
1693    #[test]
1694    fn untouched_context_finish_emits_no_touch_records() {
1695        use crate::segment_borrow::decode_touch_map_for_tests;
1696
1697        let (_b, account) = make_account(1);
1698        let accounts = [account];
1699        let pid = Address::new([9u8; 32]);
1700        let ctx = Context::new(&pid, &accounts, &[]);
1701
1702        assert_eq!(ctx.touch_map_len(), 0);
1703        let (buf, len) = ctx.encode_touch_map();
1704        let (flags, records) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1705        assert_eq!(flags, 0, "empty map carries no flags");
1706        assert!(
1707            records.is_empty(),
1708            "no touches means no records: {records:?}"
1709        );
1710
1711        // Safe to finalize even with nothing to report.
1712        ctx.finish_with_touch_map();
1713    }
1714
1715    /// Failed-instruction emission regression: the touch-map record must be emitted on
1716    /// the handler's **Ok** path ONLY, never on `Err`. This reconstructs
1717    /// the exact shape the dispatcher generates for an opted-in typed
1718    /// context,
1719    ///
1720    /// ```ignore
1721    /// handler(Ctx::bind(&mut ctx)?, ..)?;              // Err short-circuits
1722    /// if Ctx::EMIT_TOUCH_MAP { ctx.finish_with_touch_map(); }
1723    /// Ok(())
1724    /// ```
1725    ///
1726    /// and drives it with a handler that returns `Err` and the same
1727    /// handler that returns `Ok`. The old `Drop`-based emit fired on both
1728    /// paths (drop glue runs on every scope exit) and so leaked a
1729    /// misleading record for the rolled-back instruction; the Ok-only
1730    /// dispatch emits nothing on `Err` and exactly one decodable record on
1731    /// `Ok`. Off-chain the real syscall is a no-op, so the finish point is
1732    /// made observable by snapshotting the same wire bytes
1733    /// `finish_with_touch_map` would send.
1734    #[cfg(feature = "touch-map")]
1735    #[test]
1736    fn dispatch_emits_touch_map_on_ok_path_only() {
1737        use crate::segment_borrow::decode_touch_map_for_tests;
1738
1739        // The context opted in, so its `EMIT_TOUCH_MAP` const is `true`.
1740        const EMIT_TOUCH_MAP: bool = true;
1741
1742        // Faithful reconstruction of the generated dispatch helper body.
1743        // `emitted` captures each record the Ok-path finish point would
1744        // send, its length is the number of touch-map records emitted.
1745        fn generated_dispatch(
1746            ctx: &mut Context<'_>,
1747            handler: impl FnOnce(&mut Context<'_>) -> ProgramResult,
1748            emitted: &mut std::vec::Vec<std::vec::Vec<u8>>,
1749        ) -> ProgramResult {
1750            // `handler(Ctx::bind(&mut ctx)?, ..)?`, an `Err` here
1751            // short-circuits before the emit below, exactly as `?` does
1752            // after a real bound handler returns.
1753            handler(ctx)?;
1754            // `if Ctx::EMIT_TOUCH_MAP { ctx.finish_with_touch_map(); }`,
1755            // reached only on the Ok path. Off-chain the syscall is a
1756            // no-op, so snapshot the identical bytes to observe the emit.
1757            if EMIT_TOUCH_MAP {
1758                let (buf, len) = ctx.encode_touch_map();
1759                emitted.push(buf[..len].to_vec());
1760                ctx.finish_with_touch_map();
1761            }
1762            Ok(())
1763        }
1764
1765        // A handler that touches state, then fails (as `require!`/`?`
1766        // would). The touch log is populated, but the instruction rolls
1767        // back; so NO self-describing record may be emitted.
1768        let (_b, account) = make_account(1);
1769        let accounts = [account];
1770        let pid = Address::new([9u8; 32]);
1771        let mut ctx = Context::new(&pid, &accounts, &[]);
1772        let mut emitted = std::vec::Vec::new();
1773        let err = generated_dispatch(
1774            &mut ctx,
1775            |c| {
1776                drop(c.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1777                Err(ProgramError::Custom(7))
1778            },
1779            &mut emitted,
1780        );
1781        assert_eq!(
1782            err,
1783            Err(ProgramError::Custom(7)),
1784            "handler error must propagate"
1785        );
1786        assert!(
1787            emitted.is_empty(),
1788            "a failed instruction must emit NO touch-map record: {emitted:?}",
1789        );
1790
1791        // The same handler shape, now returning Ok, must emit exactly one
1792        // decodable record describing what it touched.
1793        let (_b2, account2) = make_account(1);
1794        let accounts2 = [account2];
1795        let mut ctx_ok = Context::new(&pid, &accounts2, &[]);
1796        let mut emitted_ok = std::vec::Vec::new();
1797        let ok = generated_dispatch(
1798            &mut ctx_ok,
1799            |c| {
1800                drop(c.segment_mut::<[u8; 8]>(0, BALANCE_OFF).unwrap());
1801                Ok(())
1802            },
1803            &mut emitted_ok,
1804        );
1805        assert_eq!(ok, Ok(()), "successful handler returns Ok");
1806        assert_eq!(
1807            emitted_ok.len(),
1808            1,
1809            "a successful instruction must emit exactly one touch-map record",
1810        );
1811        let (_flags, records) = decode_touch_map_for_tests(&emitted_ok[0]).unwrap();
1812        assert_eq!(
1813            records.len(),
1814            1,
1815            "the record must describe the one touched range"
1816        );
1817        assert_eq!(records[0].offset, BALANCE_OFF);
1818        assert!(records[0].write, "the touched range was a write");
1819    }
1820}