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::audit::AccountAudit;
13use crate::address::Address;
14use crate::error::ProgramError;
15use crate::layout::LayoutContract;
16use crate::segment_borrow::SegmentBorrowRegistry;
17use crate::ProgramResult;
18
19/// Execution context for a Hopper instruction handler.
20///
21/// Wraps the program_id, account slice, and instruction data into a single
22/// object with structured access patterns.
23///
24/// # Authored flow
25///
26/// ```ignore
27/// pub fn deposit(ctx: &Context, amount: u64) -> ProgramResult {
28/// let authority = ctx.account(0)?;
29/// let vault = ctx.account(1)?;
30///
31/// authority.require_signer()?;
32/// vault.require_writable()?;
33/// vault.check_disc(1)?;
34///
35/// let mut state = vault.load_mut::<VaultState>()?;
36/// state.balance = state.balance.checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?;
37/// Ok(())
38/// }
39/// ```
40pub struct Context<'a> {
41 /// The program's own address.
42 pub program_id: &'a Address,
43 /// All accounts passed to this instruction.
44 accounts: &'a [AccountView],
45 /// Raw instruction data (past the discriminator byte, if applicable).
46 pub instruction_data: &'a [u8],
47 /// Segment-level borrow tracking for fine-grained access control.
48 ///
49 /// Enables safe concurrent mutable access to non-overlapping regions
50 /// of the same account. This is what makes Hopper strictly safer than
51 /// raw Pinocchio without adding meaningful CU overhead.
52 /// Prefer the `borrows()` / `borrows_mut()` accessors in new code.
53 pub(crate) segment_borrows: SegmentBorrowRegistry,
54}
55
56impl<'a> Context<'a> {
57 /// Create a new context from the entrypoint parameters.
58 #[inline(always)]
59 pub fn new(
60 program_id: &'a Address,
61 accounts: &'a [AccountView],
62 instruction_data: &'a [u8],
63 ) -> Self {
64 Self {
65 program_id,
66 accounts,
67 instruction_data,
68 segment_borrows: SegmentBorrowRegistry::new(),
69 }
70 }
71
72 /// Program ID.
73 #[inline(always)]
74 pub fn program_id(&self) -> &Address {
75 self.program_id
76 }
77
78 /// Raw instruction data.
79 #[inline(always)]
80 pub fn instruction_data(&self) -> &'a [u8] {
81 self.instruction_data
82 }
83
84 /// Get an account by index.
85 #[inline(always)]
86 pub fn account(&self, index: usize) -> Result<&AccountView, ProgramError> {
87 self.accounts.get(index).ok_or(ProgramError::NotEnoughAccountKeys)
88 }
89
90 /// Get an account by index (mutation-intent variant).
91 ///
92 /// Functionally identical to `account()` since `AccountView` uses
93 /// interior mutability for data access (`overlay_mut`, `load_mut`,
94 /// `try_borrow_mut`). The distinct name signals that the caller
95 /// intends to write through the returned reference.
96 #[inline(always)]
97 pub fn account_mut(&self, index: usize) -> Result<&AccountView, ProgramError> {
98 self.accounts.get(index).ok_or(ProgramError::NotEnoughAccountKeys)
99 }
100
101 /// Get the total number of accounts.
102 #[inline(always)]
103 pub fn num_accounts(&self) -> usize {
104 self.accounts.len()
105 }
106
107 /// Get all accounts as a slice.
108 #[inline(always)]
109 pub fn accounts(&self) -> &[AccountView] {
110 self.accounts
111 }
112
113 /// Access the instruction-scoped segment borrow registry.
114 #[inline(always)]
115 pub fn borrows(&self) -> &SegmentBorrowRegistry {
116 &self.segment_borrows
117 }
118
119 /// Mutably access the instruction-scoped segment borrow registry.
120 #[inline(always)]
121 pub fn borrows_mut(&mut self) -> &mut SegmentBorrowRegistry {
122 &mut self.segment_borrows
123 }
124
125 /// Inspect the instruction account slice for duplicate aliases.
126 #[inline(always)]
127 pub fn audit_accounts(&self) -> AccountAudit<'a> {
128 AccountAudit::new(self.accounts)
129 }
130
131 /// Get the remaining accounts starting at `from`.
132 #[inline(always)]
133 pub fn remaining_accounts(&self, from: usize) -> &[AccountView] {
134 if from >= self.accounts.len() {
135 &[]
136 } else {
137 &self.accounts[from..]
138 }
139 }
140
141 /// Require at least `n` accounts are present.
142 #[inline(always)]
143 pub fn require_accounts(&self, n: usize) -> ProgramResult {
144 if self.accounts.len() >= n {
145 Ok(())
146 } else {
147 Err(ProgramError::NotEnoughAccountKeys)
148 }
149 }
150
151 /// Require all account addresses to be unique.
152 #[inline(always)]
153 pub fn require_unique_accounts(&self) -> ProgramResult {
154 self.audit_accounts().require_all_unique()
155 }
156
157 /// Require that no duplicated account is writable in this instruction.
158 #[inline(always)]
159 pub fn require_unique_writable_accounts(&self) -> ProgramResult {
160 self.audit_accounts().require_unique_writable()
161 }
162
163 /// Require that no duplicated account is used as a signer role.
164 #[inline(always)]
165 pub fn require_unique_signer_accounts(&self) -> ProgramResult {
166 self.audit_accounts().require_unique_signers()
167 }
168
169 /// Require at least `n` bytes of instruction data.
170 #[inline(always)]
171 pub fn require_data_len(&self, n: usize) -> ProgramResult {
172 if self.instruction_data.len() >= n {
173 Ok(())
174 } else {
175 Err(ProgramError::InvalidInstructionData)
176 }
177 }
178
179 // --- Whole-Layout Typed Access ----------------------------------
180
181 /// Validate-and-load the full typed layout for an account.
182 ///
183 /// This is the indexed shortcut for `ctx.account(idx)?.load::<T>()`.
184 /// It's the canonical "Tier A" access path: the runtime checks the
185 /// Hopper header, validates the data length, and projects the typed
186 /// view in one inlined call. no extra cost over the spelled-out form.
187 #[inline(always)]
188 pub fn load<T: LayoutContract>(
189 &self,
190 index: usize,
191 ) -> Result<crate::Ref<'_, T>, ProgramError> {
192 self.account(index)?.load::<T>()
193 }
194
195 /// Validate-and-load a mutable typed layout for an account.
196 ///
197 /// Indexed shortcut for `ctx.account(idx)?.load_mut::<T>()`. The
198 /// returned guard holds the account-level exclusive borrow until
199 /// it drops.
200 #[inline(always)]
201 pub fn load_mut<T: LayoutContract>(
202 &self,
203 index: usize,
204 ) -> Result<crate::RefMut<'_, T>, ProgramError> {
205 self.account(index)?.load_mut::<T>()
206 }
207
208 /// Cross-program load: validate ABI fingerprint without ownership check.
209 ///
210 /// Use this when reading an account whose owner is another program but
211 /// whose layout is published as a Hopper layout contract.
212 #[inline(always)]
213 pub fn load_cross_program<T: LayoutContract>(
214 &self,
215 index: usize,
216 ) -> Result<crate::Ref<'_, T>, ProgramError> {
217 self.account(index)?.load_cross_program::<T>()
218 }
219
220 // --- Segment-Level Access (fine-grained borrow tracking) --------
221
222 /// Register a read borrow for a segment of an account and return a
223 /// [`SegRef<T>`](crate::SegRef) that releases both the account-level
224 /// byte guard **and** the segment registry lease on drop.
225 ///
226 /// `index` is the account index. `abs_offset` is the absolute byte
227 /// offset within the account data (including header bytes).
228 ///
229 /// # Type Safety
230 ///
231 /// `T` must implement `Pod` (substrate-level "safe to overlay on
232 /// raw bytes" contract: every bit pattern valid, align-1, no
233 /// padding, no interior pointers). Segment borrow tracking
234 /// prevents conflicting write access to the same byte range for
235 /// the guard's lifetime.
236 ///
237 /// # Canonical path (audit ST1 / winning-architecture spec)
238 ///
239 /// Three variants exist for different offset sources:
240 ///
241 /// | Variant | Use when |
242 /// |---|---|
243 /// | [`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. |
244 /// | [`segment_ref_const`](Self::segment_ref_const) | Offset comes from a runtime [`Segment`] value (dispatching dynamically between named fields). |
245 /// | `segment_ref` (this method) | Offset is fully dynamic (iterating segments in a loop, for example). |
246 ///
247 /// `#[hopper::context]`-generated accessors default to the canonical
248 /// typed path; reach for the others only when the use case
249 /// genuinely needs a runtime offset.
250 #[inline(always)]
251 pub fn segment_ref<'b, T: crate::Pod>(
252 &'b mut self,
253 index: usize,
254 abs_offset: u32,
255 ) -> Result<crate::SegRef<'b, T>, ProgramError> {
256 let view = self.accounts.get(index)
257 .ok_or(ProgramError::NotEnoughAccountKeys)?;
258 view.segment_ref::<T>(&mut self.segment_borrows, abs_offset, core::mem::size_of::<T>() as u32)
259 }
260
261 /// Register a write borrow for a segment of an account.
262 ///
263 /// Validates bounds, checks writable, and registers a leased
264 /// exclusive borrow, then returns a [`SegRefMut<T>`](crate::SegRefMut)
265 /// that releases on drop.
266 ///
267 /// This is the primitive that enables safe concurrent mutation of
268 /// non-overlapping account regions. Hopper's core innovation .
269 /// and the lease model (added post-audit) makes sequential
270 /// same-region borrows inside one instruction work correctly.
271 #[inline(always)]
272 pub fn segment_mut<'b, T: crate::Pod>(
273 &'b mut self,
274 index: usize,
275 abs_offset: u32,
276 ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
277 let view = self.accounts.get(index)
278 .ok_or(ProgramError::NotEnoughAccountKeys)?;
279 view.segment_mut::<T>(&mut self.segment_borrows, abs_offset, core::mem::size_of::<T>() as u32)
280 }
281
282 /// Const-driven segment read: pass a compile-time [`Segment`] and the
283 /// account index. Lowers to the same pointer-plus-const-offset shape
284 /// as `segment_ref` but without the caller hand-rolling the offset +
285 /// size arguments.
286 #[inline(always)]
287 pub fn segment_ref_const<'b, T: crate::Pod>(
288 &'b mut self,
289 index: usize,
290 segment: crate::Segment,
291 ) -> Result<crate::SegRef<'b, T>, ProgramError> {
292 let view = self.accounts.get(index)
293 .ok_or(ProgramError::NotEnoughAccountKeys)?;
294 view.segment_ref_const::<T>(&mut self.segment_borrows, segment)
295 }
296
297 /// Const-driven exclusive segment access. Pair with
298 /// `#[hopper::state]` constants for zero-overhead field writes.
299 #[inline(always)]
300 pub fn segment_mut_const<'b, T: crate::Pod>(
301 &'b mut self,
302 index: usize,
303 segment: crate::Segment,
304 ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
305 let view = self.accounts.get(index)
306 .ok_or(ProgramError::NotEnoughAccountKeys)?;
307 view.segment_mut_const::<T>(&mut self.segment_borrows, segment)
308 }
309
310 /// Typed-segment read: the type and offset are both compile-time
311 /// constants, baked into a [`TypedSegment`] zero-sized marker.
312 #[inline(always)]
313 pub fn segment_ref_typed<'b, T: crate::Pod, const OFFSET: u32>(
314 &'b mut self,
315 index: usize,
316 segment: crate::TypedSegment<T, OFFSET>,
317 ) -> Result<crate::SegRef<'b, T>, ProgramError> {
318 let view = self.accounts.get(index)
319 .ok_or(ProgramError::NotEnoughAccountKeys)?;
320 view.segment_ref_typed::<T, OFFSET>(&mut self.segment_borrows, segment)
321 }
322
323 /// Typed-segment write. Mirrors [`segment_ref_typed`] for the
324 /// exclusive path.
325 #[inline(always)]
326 pub fn segment_mut_typed<'b, T: crate::Pod, const OFFSET: u32>(
327 &'b mut self,
328 index: usize,
329 segment: crate::TypedSegment<T, OFFSET>,
330 ) -> Result<crate::SegRefMut<'b, T>, ProgramError> {
331 let view = self.accounts.get(index)
332 .ok_or(ProgramError::NotEnoughAccountKeys)?;
333 view.segment_mut_typed::<T, OFFSET>(&mut self.segment_borrows, segment)
334 }
335
336 /// Explicit unsafe whole-account typed read.
337 #[inline(always)]
338 pub unsafe fn raw_ref<T: crate::Pod>(
339 &self,
340 index: usize,
341 ) -> Result<crate::Ref<'_, T>, ProgramError> {
342 let view = self.accounts.get(index)
343 .ok_or(ProgramError::NotEnoughAccountKeys)?;
344 unsafe { view.raw_ref::<T>() }
345 }
346
347 /// Explicit unsafe whole-account typed write.
348 #[inline(always)]
349 pub unsafe fn raw_mut<T: crate::Pod>(
350 &self,
351 index: usize,
352 ) -> Result<crate::RefMut<'_, T>, ProgramError> {
353 let view = self.accounts.get(index)
354 .ok_or(ProgramError::NotEnoughAccountKeys)?;
355 unsafe { view.raw_mut::<T>() }
356 }
357
358 /// Explicit unsafe escape hatch for whole-account typed projection.
359 ///
360 /// This bypasses segment borrow tracking. The caller is responsible for
361 /// alias safety and for using a type that matches the account bytes.
362 #[inline(always)]
363 pub unsafe fn raw_unchecked<T: crate::Pod>(
364 &self,
365 index: usize,
366 ) -> Result<crate::RefMut<'_, T>, ProgramError> {
367 unsafe { self.raw_mut::<T>(index) }
368 }
369
370 /// Canonical raw-pointer escape hatch to an account's data buffer.
371 ///
372 /// Returns a pointer to the first byte of `accounts[index]`'s data
373 /// region (after the runtime account header, before any Hopper
374 /// 16-byte layout header). The pointer is valid for reads and
375 /// writes for the lifetime of the account view and carries no
376 /// borrow-tracking obligations. Dereferencing it is `unsafe`
377 /// because the caller takes over alias-safety responsibility
378 /// that the segment registry normally upholds.
379 ///
380 /// This is the explicit power-user primitive the audit asks for:
381 /// safe code reaches for `segment_ref_typed` / `segment_mut_typed`
382 /// / the generated `ctx.<field>_segment_mut(...)` accessors; raw
383 /// code drops to `unsafe { ctx.as_mut_ptr(0)?.add(offset) as *mut T }`.
384 ///
385 /// # Safety
386 ///
387 /// The caller must guarantee no aliasing mutable borrow is held
388 /// on the same account for the duration of any write through the
389 /// returned pointer. The returned pointer must be dereferenced
390 /// within the `'info` lifetime of the account view; reading past
391 /// `AccountView::data_len()` is undefined behaviour.
392 #[cfg(feature = "hopper-native-backend")]
393 #[inline(always)]
394 pub unsafe fn as_mut_ptr(&self, index: usize) -> Result<*mut u8, ProgramError> {
395 let view = self
396 .accounts
397 .get(index)
398 .ok_or(ProgramError::NotEnoughAccountKeys)?;
399 view.require_writable()?;
400 // SAFETY: the account view is live for `'info` and
401 // `data_ptr` yields a pointer inside the loader-provided
402 // per-account buffer. Returning the untyped pointer transfers
403 // alias-safety to the caller as documented above.
404 Ok(view.data_ptr_unchecked())
405 }
406
407 /// Immutable sibling of [`as_mut_ptr`]. Returns a `*const u8`.
408 ///
409 /// Shared-borrow checking still runs, so calling this while an
410 /// exclusive borrow is live on the same account fails with
411 /// `AccountBorrowFailed`. The return value is safe to obtain; the
412 /// caller only needs `unsafe` to dereference it.
413 ///
414 /// [`as_mut_ptr`]: Self::as_mut_ptr
415 #[cfg(feature = "hopper-native-backend")]
416 #[inline(always)]
417 pub fn as_ptr(&self, index: usize) -> Result<*const u8, ProgramError> {
418 let view = self
419 .accounts
420 .get(index)
421 .ok_or(ProgramError::NotEnoughAccountKeys)?;
422 view.check_borrow()?;
423 Ok(view.data_ptr_unchecked() as *const u8)
424 }
425
426 /// Read instruction data as a typed value (unaligned, little-endian safe).
427 ///
428 /// Reads `size_of::<T>()` bytes starting at `offset` via `read_unaligned`.
429 /// Caller must ensure `T` is a plain-old-data type where all bit patterns
430 /// are valid.
431 #[inline(always)]
432 pub fn read_data<T: crate::Pod>(&self, offset: usize) -> Result<T, ProgramError> {
433 let end = offset.checked_add(core::mem::size_of::<T>())
434 .ok_or(ProgramError::ArithmeticOverflow)?;
435 if self.instruction_data.len() < end {
436 return Err(ProgramError::InvalidInstructionData);
437 }
438 // SAFETY: bounds checked; `T: Pod` guarantees every bit
439 // pattern is valid and the type has no drop glue, so
440 // `read_unaligned` into instruction data is sound.
441 Ok(unsafe {
442 core::ptr::read_unaligned(self.instruction_data.as_ptr().add(offset) as *const T)
443 })
444 }
445
446 /// Get a byte slice from instruction data.
447 #[inline(always)]
448 pub fn data_slice(&self, offset: usize, len: usize) -> Result<&[u8], ProgramError> {
449 let end = offset.checked_add(len).ok_or(ProgramError::ArithmeticOverflow)?;
450 if self.instruction_data.len() < end {
451 return Err(ProgramError::InvalidInstructionData);
452 }
453 Ok(&self.instruction_data[offset..end])
454 }
455
456 /// Read the first byte of instruction data as an instruction tag.
457 ///
458 /// Common pattern for byte-tag dispatch.
459 #[inline(always)]
460 pub fn instruction_tag(&self) -> Result<u8, ProgramError> {
461 self.instruction_data.first().copied().ok_or(ProgramError::InvalidInstructionData)
462 }
463}