hopper_core/frame/mod.rs
1//! Borrowed-state execution context.
2//!
3//! The `Frame` is Hopper's execution model. It wraps the instruction's accounts
4//! and data, enforcing single-mutable-borrow discipline and phased execution.
5//!
6//! ## Execution Phases
7//!
8//! 1. **Resolve** -- Parse accounts from the input slice into named typed slots
9//! 2. **Validate** -- Run the validation graph (account-local, cross-account, state-transition)
10//! 3. **Borrow** -- Obtain zero-copy overlays with borrow discipline
11//! 4. **Mutate** -- Execute state changes through verified mutable references
12//! 5. **Emit** -- Fire events
13//! 6. **Commit** -- (implicit: Solana runtime commits on success)
14//!
15//! The `Frame` ensures that:
16//! - Each account is borrowed at most once mutably
17//! - Immutable borrows can coexist
18//! - Validation runs before mutation
19//! - Events are emitted after state changes
20
21pub mod args;
22pub mod phase;
23
24use crate::account::SliceCursor;
25use crate::account::{FixedLayout, Pod, HEADER_LEN};
26use hopper_runtime::segment_borrow::SegmentBorrowRegistry;
27use hopper_runtime::{
28 error::ProgramError, AccountView, Address, ProgramResult, Ref, RefMut, SegRef, SegRefMut,
29 SegmentLease,
30};
31
32/// Maximum accounts in a single frame. Matches Solana's transaction limit.
33pub const MAX_FRAME_ACCOUNTS: usize = 64;
34
35/// Execution frame holding the instruction's accounts and data.
36///
37/// `Frame` is the entry point for Hopper's phased execution model.
38/// It tracks which accounts have been borrowed (mutably or immutably)
39/// to prevent aliasing violations at runtime.
40pub struct Frame<'a> {
41 /// Program ID that is executing.
42 program_id: &'a Address,
43 /// Raw account views.
44 accounts: &'a [AccountView],
45 /// Instruction data cursor.
46 ix_data: SliceCursor<'a>,
47 /// Borrow tracking: bit N = 1 means account N is mutably borrowed.
48 /// This is a runtime check -- not as strong as the borrow checker, but
49 /// catches the most dangerous pattern (double-mutable-borrow).
50 mutable_borrows: u64,
51 /// Segment-level borrow tracking for fine-grained conflict detection.
52 /// Allows concurrent mutable access to non-overlapping regions of the
53 /// same account, the key safety property missing from raw pointer access.
54 segment_borrows: SegmentBorrowRegistry,
55}
56
57impl<'a> Frame<'a> {
58 /// Create a new execution frame.
59 #[inline(always)]
60 pub fn new(
61 program_id: &'a Address,
62 accounts: &'a [AccountView],
63 instruction_data: &'a [u8],
64 ) -> Result<Self, ProgramError> {
65 if accounts.len() > MAX_FRAME_ACCOUNTS {
66 return Err(ProgramError::InvalidArgument);
67 }
68 Ok(Self {
69 program_id,
70 accounts,
71 ix_data: SliceCursor::new(instruction_data),
72 mutable_borrows: 0,
73 segment_borrows: SegmentBorrowRegistry::new(),
74 })
75 }
76
77 /// Program ID.
78 #[inline(always)]
79 pub fn program_id(&self) -> &Address {
80 self.program_id
81 }
82
83 /// Number of accounts in this frame.
84 #[inline(always)]
85 pub fn account_count(&self) -> usize {
86 self.accounts.len()
87 }
88
89 /// Get raw account view by index.
90 #[inline(always)]
91 pub fn account_view(&self, index: usize) -> Result<&AccountView, ProgramError> {
92 self.accounts
93 .get(index)
94 .ok_or(ProgramError::NotEnoughAccountKeys)
95 }
96
97 /// Get instruction data cursor.
98 #[inline(always)]
99 pub fn ix_data(&mut self) -> &mut SliceCursor<'a> {
100 &mut self.ix_data
101 }
102
103 /// Get raw instruction data.
104 #[inline(always)]
105 pub fn ix_data_raw(&self) -> &[u8] {
106 self.ix_data.data_from_position()
107 }
108
109 // --- Immutable Account Access -----------------------------------
110
111 /// Get an immutable account view (no borrow tracking needed for reads).
112 #[inline(always)]
113 pub fn account(&self, index: usize) -> Result<FrameAccount<'_>, ProgramError> {
114 let view = self
115 .accounts
116 .get(index)
117 .ok_or(ProgramError::NotEnoughAccountKeys)?;
118 Ok(FrameAccount { view })
119 }
120
121 // --- Mutable Account Access (with borrow tracking) -------------
122
123 /// Get a mutable account view with runtime borrow checking.
124 ///
125 /// Returns an error if this account is already borrowed mutably.
126 /// This prevents the most dangerous aliasing pattern in Solana programs.
127 #[inline]
128 pub fn account_mut(&mut self, index: usize) -> Result<FrameAccountMut<'_>, ProgramError> {
129 if index >= self.accounts.len() {
130 return Err(ProgramError::NotEnoughAccountKeys);
131 }
132
133 let bit = 1u64 << (index as u32);
134 if self.mutable_borrows & bit != 0 {
135 // Already mutably borrowed -- prevent aliasing.
136 return Err(ProgramError::AccountBorrowFailed);
137 }
138
139 self.mutable_borrows |= bit;
140 let view = &self.accounts[index];
141
142 Ok(FrameAccountMut {
143 view,
144 borrow_mask: &mut self.mutable_borrows,
145 bit,
146 })
147 }
148
149 // --- Segment-Level Access (fine-grained borrow tracking) --------
150
151 /// Get the segment borrow registry for direct manipulation.
152 #[inline(always)]
153 pub fn segment_borrows(&self) -> &SegmentBorrowRegistry {
154 &self.segment_borrows
155 }
156
157 /// Get the mutable segment borrow registry.
158 #[inline(always)]
159 pub fn segment_borrows_mut(&mut self) -> &mut SegmentBorrowRegistry {
160 &mut self.segment_borrows
161 }
162
163 /// Read a typed value from a segment of an account's data region.
164 ///
165 /// Registers a **read** borrow for the given byte range, then projects
166 /// the pointer through the live byte-borrow guard into a `Ref<'_, T>`.
167 /// Returns an error if the range conflicts with an existing write
168 /// borrow on the same account.
169 ///
170 /// `offset` is relative to the layout body (after the 16-byte header).
171 ///
172 /// # Preferred path
173 ///
174 /// Most programs don't need to construct a `Frame` at all, the
175 /// `hopper_runtime::Context` handler signature gives you
176 /// `ctx.segment_ref::<T>(index, abs_offset)` with the same tightened
177 /// Pod contract, the same RAII guard, and none of the phased
178 /// execution bookkeeping. Reach for `Frame::segment_ref` only when
179 /// you're inside the advanced `frame`-gated execution model.
180 ///
181 /// # Safety Contract
182 ///
183 /// - T must be `Pod + FixedLayout` (safe to interpret from any bit pattern,
184 /// alignment-1, no padding).
185 /// - Bounds are checked at runtime.
186 /// - Borrow conflicts are checked at runtime.
187 /// - The returned [`SegRef<T>`] owns both the byte-slice borrow and
188 /// a RAII lease on the segment registry entry. Dropping it
189 /// releases **both**, no sticky-ledger residue from post-audit.
190 #[inline]
191 pub fn segment_ref<'f, T: Pod + FixedLayout>(
192 &'f mut self,
193 index: usize,
194 offset: u32,
195 ) -> Result<SegRef<'f, T>, ProgramError> {
196 let view = self
197 .accounts
198 .get(index)
199 .ok_or(ProgramError::NotEnoughAccountKeys)?;
200 let data = view.try_borrow()?;
201
202 let abs_offset = (HEADER_LEN as u32)
203 .checked_add(offset)
204 .ok_or(ProgramError::ArithmeticOverflow)?;
205 let end = abs_offset
206 .checked_add(T::SIZE as u32)
207 .ok_or(ProgramError::ArithmeticOverflow)?;
208 if end as usize > data.len() {
209 return Err(ProgramError::AccountDataTooSmall);
210 }
211
212 let borrow = self.segment_borrows.register_leased_read(
213 view.address(),
214 abs_offset,
215 T::SIZE as u32,
216 )?;
217
218 // SAFETY: T is Pod + FixedLayout (all bit patterns valid, align-1).
219 // Bounds checked above. `Ref::project` consumes the byte-slice
220 // guard and yields a `Ref<T>` whose lifetime is tied to the
221 // account borrow; the `SegmentLease` we build immediately after
222 // releases the registry entry on drop.
223 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
224 let ptr = unsafe { data.as_bytes_ptr().add(abs_offset as usize) as *const T };
225 let inner: Ref<'f, T> = unsafe { data.project(ptr) };
226 // SAFETY: `borrow` was just registered in `self.segment_borrows`;
227 // the lease is the sole releaser for that entry.
228 let lease: SegmentLease<'f> =
229 unsafe { SegmentLease::new(&mut self.segment_borrows, borrow) };
230 Ok(SegRef::new(inner, lease))
231 }
232
233 /// Get a mutable typed reference to a segment of an account's data.
234 ///
235 /// Registers a **write** borrow for the given byte range, then projects
236 /// the pointer through the live byte-borrow guard into a `RefMut<'_, T>`.
237 /// Returns an error if the range overlaps any existing borrow (read or
238 /// write) on the same account.
239 ///
240 /// This is the core primitive that makes Hopper strictly better than
241 /// raw Pinocchio: you get the same pointer arithmetic, but with
242 /// segment-level conflict detection that prevents aliasing bugs.
243 ///
244 /// `offset` is relative to the layout body (after the 16-byte header).
245 ///
246 /// # Safety Contract
247 ///
248 /// - T must be `Pod + FixedLayout`.
249 /// - Bounds are checked at runtime.
250 /// - Borrow conflicts are checked at runtime.
251 /// - The returned [`SegRefMut<T>`] carries both the account-level
252 /// exclusive byte guard and a RAII registry lease, dropping it
253 /// releases the full borrow cleanly, so sequential patterns on
254 /// the same segment compose like ordinary Rust borrows.
255 ///
256 /// # Example
257 ///
258 /// ```ignore
259 /// // Only borrows the "balance" region [32..40), not the entire account.
260 /// {
261 /// let mut balance = frame.segment_mut::<WireU64>(0, 32)?;
262 /// balance.set(balance.get() + amount);
263 /// } // SegRefMut drops here, releasing both guards.
264 ///
265 /// // Now we can re-borrow the same (or a different) segment safely.
266 /// let mut balance_again = frame.segment_mut::<WireU64>(0, 32)?;
267 /// ```
268 #[inline]
269 pub fn segment_mut<'f, T: Pod + FixedLayout>(
270 &'f mut self,
271 index: usize,
272 offset: u32,
273 ) -> Result<SegRefMut<'f, T>, ProgramError> {
274 let view = self
275 .accounts
276 .get(index)
277 .ok_or(ProgramError::NotEnoughAccountKeys)?;
278
279 // Check writable before doing anything else.
280 if !view.is_writable() {
281 return Err(ProgramError::InvalidAccountData);
282 }
283
284 let data = view.try_borrow_mut()?;
285 let abs_offset = (HEADER_LEN as u32)
286 .checked_add(offset)
287 .ok_or(ProgramError::ArithmeticOverflow)?;
288 let end = abs_offset
289 .checked_add(T::SIZE as u32)
290 .ok_or(ProgramError::ArithmeticOverflow)?;
291 if end as usize > data.len() {
292 return Err(ProgramError::AccountDataTooSmall);
293 }
294
295 let borrow = self.segment_borrows.register_leased_write(
296 view.address(),
297 abs_offset,
298 T::SIZE as u32,
299 )?;
300
301 // SAFETY: as above; the projected `RefMut<T>` inherits the
302 // byte-slice exclusive borrow, and the lease ensures the
303 // registry entry is swap-removed on drop.
304 let bytes_ptr = (&*data) as *const [u8] as *mut [u8] as *mut u8;
305 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
306 let ptr = unsafe { bytes_ptr.add(abs_offset as usize) as *mut T };
307 let inner: RefMut<'f, T> = unsafe { data.project(ptr) };
308 let lease: SegmentLease<'f> =
309 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
310 unsafe { SegmentLease::new(&mut self.segment_borrows, borrow) };
311 Ok(SegRefMut::new(inner, lease))
312 }
313
314 /// Unsafe escape hatch for performance-critical paths.
315 ///
316 /// Skips borrow tracking entirely. The caller takes full responsibility
317 /// for aliasing safety. Returns a `RefMut<T>` so the borrow guard is
318 /// still tied to the returned value's lifetime, the "unchecked" part
319 /// is only the conflict-detection skip, not the lifetime tying.
320 ///
321 /// # Safety
322 ///
323 /// The caller must guarantee no other mutable reference to the same
324 /// byte range exists for the duration of the returned reference, and
325 /// that no overlapping segment borrow has been registered.
326 #[inline(always)]
327 pub unsafe fn segment_mut_unchecked<T: Pod + FixedLayout>(
328 &self,
329 index: usize,
330 offset: u32,
331 ) -> Result<RefMut<'_, T>, ProgramError> {
332 let view = self
333 .accounts
334 .get(index)
335 .ok_or(ProgramError::NotEnoughAccountKeys)?;
336 let data = view.try_borrow_mut()?;
337
338 let abs_offset = (HEADER_LEN as u32)
339 .checked_add(offset)
340 .ok_or(ProgramError::ArithmeticOverflow)?;
341 let end = abs_offset
342 .checked_add(T::SIZE as u32)
343 .ok_or(ProgramError::ArithmeticOverflow)?;
344 if end as usize > data.len() {
345 return Err(ProgramError::AccountDataTooSmall);
346 }
347
348 let bytes_ptr = (&*data) as *const [u8] as *mut [u8] as *mut u8;
349 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
350 let ptr = unsafe { bytes_ptr.add(abs_offset as usize) as *mut T };
351 Ok(unsafe { data.project(ptr) })
352 }
353
354 // --- Validation Helpers -----------------------------------------
355
356 /// Validate that account at `index` is a signer.
357 #[inline(always)]
358 pub fn require_signer(&self, index: usize) -> ProgramResult {
359 crate::check::check_signer(self.account_view(index)?)
360 }
361
362 /// Validate that account at `index` is writable.
363 #[inline(always)]
364 pub fn require_writable(&self, index: usize) -> ProgramResult {
365 crate::check::check_writable(self.account_view(index)?)
366 }
367
368 /// Validate that account at `index` is owned by this program.
369 #[inline(always)]
370 pub fn require_owned(&self, index: usize) -> ProgramResult {
371 crate::check::check_owner(self.account_view(index)?, self.program_id)
372 }
373
374 /// Validate signer + writable (common pattern for authority accounts).
375 #[inline(always)]
376 pub fn require_authority(&self, index: usize) -> ProgramResult {
377 let view = self.account_view(index)?;
378 crate::check::check_signer(view)?;
379 crate::check::check_writable(view)?;
380 Ok(())
381 }
382
383 /// Validate two accounts are unique.
384 #[inline(always)]
385 pub fn require_unique(&self, a: usize, b: usize) -> ProgramResult {
386 let va = self.account_view(a)?;
387 let vb = self.account_view(b)?;
388 crate::check::check_accounts_unique(va, vb)
389 }
390
391 /// Require an account matches a specific program address.
392 #[inline(always)]
393 pub fn require_program(&self, index: usize, program: &Address) -> ProgramResult {
394 crate::check::check_address(self.account_view(index)?, program)
395 }
396}
397
398/// Immutable account view within a Frame.
399pub struct FrameAccount<'a> {
400 view: &'a AccountView,
401}
402
403impl<'a> FrameAccount<'a> {
404 /// The underlying AccountView.
405 #[inline(always)]
406 pub fn view(&self) -> &AccountView {
407 self.view
408 }
409
410 /// The account's address.
411 #[inline(always)]
412 pub fn address(&self) -> &Address {
413 self.view.address()
414 }
415
416 /// Borrow account data (read-only).
417 #[inline(always)]
418 pub fn data(&self) -> Result<Ref<'a, [u8]>, ProgramError> {
419 self.view.try_borrow()
420 }
421
422 /// Lamports balance.
423 #[inline(always)]
424 pub fn lamports(&self) -> u64 {
425 self.view.lamports()
426 }
427
428 /// Is this account a signer?
429 #[inline(always)]
430 pub fn is_signer(&self) -> bool {
431 self.view.is_signer()
432 }
433
434 /// Is this account writable?
435 #[inline(always)]
436 pub fn is_writable(&self) -> bool {
437 self.view.is_writable()
438 }
439}
440
441/// Mutable account view within a Frame.
442///
443/// When this is dropped, the mutable borrow tracking bit is cleared,
444/// allowing the account to be re-borrowed.
445pub struct FrameAccountMut<'a> {
446 view: &'a AccountView,
447 borrow_mask: &'a mut u64,
448 bit: u64,
449}
450
451impl<'a> FrameAccountMut<'a> {
452 /// The underlying AccountView.
453 #[inline(always)]
454 pub fn view(&self) -> &AccountView {
455 self.view
456 }
457
458 /// The account's address.
459 #[inline(always)]
460 pub fn address(&self) -> &Address {
461 self.view.address()
462 }
463
464 /// Borrow account data (read-only).
465 #[inline(always)]
466 pub fn data(&self) -> Result<Ref<'a, [u8]>, ProgramError> {
467 self.view.try_borrow()
468 }
469
470 /// Borrow account data (mutable).
471 #[inline(always)]
472 pub fn data_mut(&self) -> Result<RefMut<'a, [u8]>, ProgramError> {
473 self.view.try_borrow_mut()
474 }
475
476 /// Lamports balance.
477 #[inline(always)]
478 pub fn lamports(&self) -> u64 {
479 self.view.lamports()
480 }
481}
482
483impl<'a> Drop for FrameAccountMut<'a> {
484 fn drop(&mut self) {
485 // Release the borrow tracking bit.
486 *self.borrow_mask &= !self.bit;
487 }
488}
489
490// ══════════════════════════════════════════════════════════════════════
491// Audit regression tests
492// ══════════════════════════════════════════════════════════════════════
493//
494// Lock in the Hopper Safety Audit's top-priority fix: Frame's segment
495// accessors now hand back `Ref<T>` / `RefMut<T>` that keep the
496// underlying account borrow alive for their full lifetime. The
497// pre-audit version dropped the byte-slice guard before returning the
498// typed reference, which is silent UB. These tests prove the guard is
499// still live at use time.
500#[cfg(all(test, feature = "hopper-native-backend"))]
501mod audit_tests {
502 use super::*;
503 use hopper_native::{
504 AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
505 };
506
507 #[repr(C)]
508 #[derive(Clone, Copy)]
509 struct Counter {
510 value: u64,
511 }
512
513 unsafe impl hopper_runtime::__hopper_native::bytemuck::Zeroable for Counter {}
514 unsafe impl hopper_runtime::__hopper_native::bytemuck::Pod for Counter {}
515 unsafe impl hopper_runtime::Pod for Counter {}
516
517 impl crate::account::FixedLayout for Counter {
518 const SIZE: usize = 8;
519 }
520
521 fn make_account(data_len: usize, seed: u8) -> (std::vec::Vec<u8>, AccountView) {
522 let mut backing = std::vec![0u8; RuntimeAccount::SIZE + data_len];
523 let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
524 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
525 unsafe {
526 raw.write(RuntimeAccount {
527 borrow_state: NOT_BORROWED,
528 is_signer: 1,
529 is_writable: 1,
530 executable: 0,
531 resize_delta: 0,
532 address: NativeAddress::new_from_array([seed; 32]),
533 owner: NativeAddress::new_from_array([2; 32]),
534 lamports: 42,
535 data_len: data_len as u64,
536 });
537 }
538 // Zero the Hopper header region so the frame doesn't trip on
539 // uninitialized bytes later.
540 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
541 let backend = unsafe { NativeAccountView::new_unchecked(raw) };
542 let view = unsafe { core::mem::transmute::<NativeAccountView, AccountView>(backend) };
543 (backing, view)
544 }
545
546 fn new_frame<'a>(program_id: &'a Address, accounts: &'a [AccountView]) -> Frame<'a> {
547 Frame::new(program_id, accounts, &[]).unwrap()
548 }
549
550 #[test]
551 fn frame_segment_mut_writes_through_ref_mut() {
552 // This test is the ground-truth for the audit fix: the fact
553 // that we can write through `RefMut<Counter>` returned by
554 // `Frame::segment_mut` and see the write persist proves the
555 // projection and guard release are now correctly tied together.
556 // Pre-audit this same code compiled but the byte-slice guard
557 // had already been dropped when `segment_mut` returned, any
558 // overlapping borrow tracking was racing against stale state.
559 let (_backing, account) = make_account(HEADER_LEN + 8, 1);
560 let program_id = NativeAddress::new_from_array([9; 32]);
561 let hopper_program_id =
562 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
563 unsafe { core::mem::transmute::<NativeAddress, Address>(program_id) };
564 let accounts = [account];
565 let mut frame = new_frame(&hopper_program_id, &accounts);
566
567 {
568 let mut counter: SegRefMut<'_, Counter> = frame.segment_mut::<Counter>(0, 0).unwrap();
569 counter.value = 7;
570 // counter (SegRefMut with byte-slice guard AND registry
571 // lease) drops here, releasing both.
572 }
573
574 // Reopen the account through the account-view path; the
575 // segment registry already recorded the write for the whole
576 // instruction, so we confirm persistence by rereading the raw
577 // bytes via the underlying account view.
578 let bytes = frame.account(0).unwrap().data().unwrap();
579 let slice: &[u8] = &*bytes;
580 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
581 let raw_u64 =
582 unsafe { core::ptr::read_unaligned(slice.as_ptr().add(HEADER_LEN) as *const u64) };
583 assert_eq!(raw_u64, 7);
584 }
585
586 #[test]
587 fn frame_segment_ref_returns_live_guard() {
588 // Seed the counter via direct byte access, then verify a
589 // `segment_ref` returned guard lets us read that value. The
590 // crucial property this exercises: `Ref<'_, Counter>` deref
591 // into `Counter` after `segment_ref` returns, which pre-audit
592 // would have been reading through a dropped byte-slice guard.
593 let (_backing, account) = make_account(HEADER_LEN + 8, 2);
594 {
595 let mut bytes = account.try_borrow_mut().unwrap();
596 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
597 let slot = unsafe { bytes.as_bytes_mut_ptr().add(HEADER_LEN) as *mut u64 };
598 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
599 unsafe { core::ptr::write_unaligned(slot, 99) };
600 }
601 let program_id = NativeAddress::new_from_array([9; 32]);
602 let hopper_program_id =
603 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
604 unsafe { core::mem::transmute::<NativeAddress, Address>(program_id) };
605 let accounts = [account];
606 let mut frame = new_frame(&hopper_program_id, &accounts);
607
608 let reader: SegRef<'_, Counter> = frame.segment_ref::<Counter>(0, 0).unwrap();
609 assert_eq!(reader.value, 99);
610 }
611
612 /// Audit regression: post-fix, dropping a `SegRefMut` from
613 /// `Frame::segment_mut` must release the segment-registry lease so
614 /// a sequential re-acquire on the same region succeeds. Pre-audit
615 /// the sticky ledger blocked this for the rest of the instruction.
616 #[test]
617 fn frame_segment_lease_releases_on_drop() {
618 let (_backing, account) = make_account(HEADER_LEN + 8, 3);
619 let program_id = NativeAddress::new_from_array([9; 32]);
620 let hopper_program_id =
621 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
622 unsafe { core::mem::transmute::<NativeAddress, Address>(program_id) };
623 let accounts = [account];
624 let mut frame = new_frame(&hopper_program_id, &accounts);
625
626 // First write.
627 {
628 let mut w: SegRefMut<'_, Counter> = frame.segment_mut::<Counter>(0, 0).unwrap();
629 w.value = 50;
630 }
631 assert_eq!(frame.segment_borrows().len(), 0);
632
633 // Second write on the same region, pre-audit this returned
634 // `AccountBorrowFailed`; now it succeeds because the prior
635 // lease has been released.
636 {
637 let mut w: SegRefMut<'_, Counter> = frame.segment_mut::<Counter>(0, 0).unwrap();
638 assert_eq!(w.value, 50);
639 w.value = 77;
640 }
641 assert_eq!(frame.segment_borrows().len(), 0);
642
643 let r: SegRef<'_, Counter> = frame.segment_ref::<Counter>(0, 0).unwrap();
644 assert_eq!(r.value, 77);
645 }
646}