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