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}