hopper_runtime/account.rs
1//! Hopper-owned account view for Solana programs.
2//!
3//! `AccountView` is the canonical typed state gateway for Hopper programs.
4//! It wraps Hopper Native's account representation behind a transparent
5//! representation boundary and delegates account operations to that layer.
6//!
7//! Key capabilities:
8//! - Chainable validation (`check_signer()?.check_writable()?`)
9//! - Whole-layout typed access (`load::<T>()`, `load_mut::<T>()`)
10//! - Segment-aware typed access (`segment_ref`, `segment_mut`)
11//! - Explicit raw escape hatches (`raw_ref`, `raw_mut`)
12//! - Hopper header reading (disc, version, layout_id)
13//! - Packed flags for batch validation
14//! - Remaining accounts iterator
15
16use crate::address::{address_eq, Address};
17use crate::borrow::{Ref, RefMut};
18use crate::borrow_registry::{self, BorrowToken};
19use crate::error::ProgramError;
20use crate::field_map::FieldInfo;
21use crate::layout::LayoutContract;
22use crate::native_boundary::{self, BackendAccountView};
23use crate::segment_borrow::SegmentBorrowRegistry;
24use crate::ProgramResult;
25
26/// Release the first `count` registered borrows during a
27/// `split_segments_mut` rollback.
28///
29/// # Safety
30///
31/// The first `count` entries of `recs` must be initialized `SegmentBorrow`
32/// records registered in `reg`.
33#[inline]
34unsafe fn release_registered<const N: usize>(
35 reg: &mut SegmentBorrowRegistry,
36 recs: &[core::mem::MaybeUninit<crate::segment_borrow::SegmentBorrow>; N],
37 count: usize,
38) {
39 let mut j = 0;
40 while j < count {
41 // SAFETY: caller guarantees `recs[j]` is an initialized, registered borrow.
42 unsafe {
43 reg.release(recs[j].assume_init_ref());
44 }
45 j += 1;
46 }
47}
48
49// ══════════════════════════════════════════════════════════════════════
50// AccountView -- Hopper's canonical typed state gateway
51// ══════════════════════════════════════════════════════════════════════
52
53/// Zero-copy view over a Solana account.
54///
55/// `AccountView` is the single canonical type for account access in
56/// Hopper programs. It wraps Hopper Native's account representation and
57/// exposes a Hopper-owned API surface.
58///
59/// The `#[repr(transparent)]` layout guarantees that `&[native::AccountView]`
60/// can be safely reinterpreted as `&[AccountView]` at the entrypoint
61/// boundary with zero conversion cost.
62#[repr(transparent)]
63pub struct AccountView<'info> {
64 inner: BackendAccountView<'info>,
65}
66
67const _: () = {
68 assert!(
69 core::mem::size_of::<AccountView<'static>>()
70 == core::mem::size_of::<BackendAccountView<'static>>()
71 );
72 assert!(
73 core::mem::align_of::<AccountView<'static>>()
74 == core::mem::align_of::<BackendAccountView<'static>>()
75 );
76 assert!(!core::mem::needs_drop::<AccountView<'static>>());
77};
78
79// SAFETY: On Solana execution is single-threaded. Host tools and fuzzers
80// should not rely on cross-thread sharing of raw account pointers.
81#[cfg(target_os = "solana")]
82unsafe impl<'info> Send for AccountView<'info> {}
83#[cfg(target_os = "solana")]
84unsafe impl<'info> Sync for AccountView<'info> {}
85
86impl<'info> Clone for AccountView<'info> {
87 #[inline(always)]
88 fn clone(&self) -> Self {
89 Self::from_inner(self.backend().clone())
90 }
91}
92
93impl<'info> PartialEq for AccountView<'info> {
94 #[inline(always)]
95 fn eq(&self, other: &Self) -> bool {
96 self.backend() == other.backend()
97 }
98}
99
100impl<'info> Eq for AccountView<'info> {}
101
102impl<'info> AccountView<'info> {
103 // Crate-visible: the lazy bridge (`crate::lazy`) wraps substrate
104 // views it receives one at a time from the native parser.
105 #[inline(always)]
106 pub(crate) fn from_inner(inner: BackendAccountView<'info>) -> Self {
107 Self { inner }
108 }
109
110 #[inline(always)]
111 fn backend(&self) -> &BackendAccountView<'info> {
112 &self.inner
113 }
114
115 #[cfg(test)]
116 #[inline(always)]
117 pub(crate) fn from_backend(inner: BackendAccountView<'info>) -> Self {
118 Self::from_inner(inner)
119 }
120
121 // ── Getters ──────────────────────────────────────────────────────
122
123 /// The account's public key.
124 #[inline(always)]
125 pub fn address(&self) -> &Address {
126 native_boundary::account_address(self.backend())
127 }
128
129 /// The owning program's address.
130 ///
131 /// # Safety
132 ///
133 /// The returned reference is invalidated if the account is assigned
134 /// to a new owner. The caller must ensure no concurrent mutation.
135 #[inline(always)]
136 pub unsafe fn owner(&self) -> &Address {
137 // 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.
138 unsafe { native_boundary::account_owner(self.backend()) }
139 }
140
141 /// Read the owner address as a copy (safe, no aliasing hazard).
142 #[inline(always)]
143 pub fn read_owner(&self) -> Address {
144 native_boundary::read_owner(self.backend())
145 }
146
147 /// Whether this account is owned by the given program.
148 #[inline(always)]
149 pub fn owned_by(&self, program: &Address) -> bool {
150 native_boundary::owned_by(self.backend(), program)
151 }
152
153 /// Whether this account signed the transaction.
154 #[inline(always)]
155 pub fn is_signer(&self) -> bool {
156 self.backend().is_signer()
157 }
158
159 /// Whether this account is writable in the transaction.
160 #[inline(always)]
161 pub fn is_writable(&self) -> bool {
162 self.backend().is_writable()
163 }
164
165 /// Whether this account contains an executable program.
166 #[inline(always)]
167 pub fn executable(&self) -> bool {
168 self.backend().executable()
169 }
170
171 /// Current data length in bytes.
172 #[inline(always)]
173 pub fn data_len(&self) -> usize {
174 self.backend().data_len()
175 }
176
177 /// Current lamport balance.
178 #[inline(always)]
179 pub fn lamports(&self) -> u64 {
180 self.backend().lamports()
181 }
182
183 /// Whether the account data is empty.
184 #[inline(always)]
185 pub fn is_data_empty(&self) -> bool {
186 self.data_len() == 0
187 }
188
189 /// Try to set the lamport balance.
190 ///
191 /// Backends such as `solana-program` enforce lamport borrow rules at
192 /// runtime. Use this in framework code so borrow conflicts return a
193 /// `ProgramError` instead of panicking.
194 #[inline(always)]
195 pub fn try_set_lamports(&self, lamports: u64) -> ProgramResult {
196 native_boundary::try_set_lamports(self.backend(), lamports)
197 }
198
199 /// Set the lamport balance.
200 #[inline(always)]
201 pub fn set_lamports(&self, lamports: u64) -> ProgramResult {
202 self.try_set_lamports(lamports)
203 }
204
205 // ── Borrow tracking ─────────────────────────────────────────────
206
207 /// Try to obtain a shared borrow of the account data.
208 #[inline(always)]
209 pub fn try_borrow(&self) -> Result<Ref<'_, [u8]>, ProgramError> {
210 let token = BorrowToken::shared(self.address())?;
211 match self.backend().try_borrow() {
212 Ok(data) => Ok(Ref::from_backend(data, token)),
213 Err(error) => {
214 drop(token);
215 Err(ProgramError::from(error))
216 }
217 }
218 }
219
220 /// Try to obtain an exclusive (mutable) borrow of the account data.
221 ///
222 /// Touch-map note: this RAW byte surface does not stamp the touch
223 /// log, segment leases route their exclusive borrows through here
224 /// and would smear every narrow lease into a whole-account record,
225 /// destroying the map's field precision. The TYPED whole-account
226 /// surfaces ([`load_mut`](Self::load_mut) /
227 /// [`load_compact_mut`](Self::load_compact_mut)) record instead.
228 ///
229 /// Ambient-gate note: under a bound `strict_writes` context this raw
230 /// whole-account write borrow is governed, the instruction-ambient
231 /// gate refuses it unless the declared policy covers the full data
232 /// range, closing the historical "raw borrow bypasses the write
233 /// policy" surface. With no gate installed the check is one load and
234 /// branch. Segment leases use the crate-internal ungated variant
235 /// because they gate the exact range themselves; the migration crank
236 /// uses it under its own `check_migratable` authorization (a
237 /// whole-layout transform, distinct from the byte-range gate; see the
238 /// crate-private `try_borrow_mut_ungated` helper.
239 #[inline(always)]
240 pub fn try_borrow_mut(&self) -> Result<RefMut<'_, [u8]>, ProgramError> {
241 let len = self.data_len();
242 if len > 0 {
243 crate::write_policy::check_data_mutation(self.address(), 0, len as u32)?;
244 }
245 self.try_borrow_mut_ungated()
246 }
247
248 /// Ungated exclusive borrow: the borrow-registry token and backend
249 /// borrow WITHOUT the instruction-ambient write-gate check. Only for
250 /// crate-internal plumbing whose caller supplies its OWN
251 /// authorization before delegating:
252 ///
253 /// - Segment leases gate the exact requested range against the
254 /// installed byte-range policy, then take the ungated borrow.
255 /// - The migration crank ([`crate::migrate`]) does not consult the
256 /// byte-range gate at all, a layout migration rewrites the whole
257 /// body by construction, which no byte-range policy would permit.
258 /// It is governed instead by its own `check_migratable`
259 /// authorization (the account must be writable and owned by the
260 /// executing program) run before this borrow. That is a DISTINCT
261 /// authorization from the `strict_writes` gate, not "the same
262 /// installed policy": a strict handler that also calls
263 /// `hopper::migration::*` is explicitly invoking a whole-layout
264 /// transform, not smuggling a byte write past its own declaration.
265 ///
266 /// Never expose publicly: doing so would reopen the raw bypass the
267 /// gated [`try_borrow_mut`](Self::try_borrow_mut) split closes.
268 #[inline(always)]
269 pub(crate) fn try_borrow_mut_ungated(&self) -> Result<RefMut<'_, [u8]>, ProgramError> {
270 let token = BorrowToken::mutable(self.address())?;
271 match self.backend().try_borrow_mut() {
272 Ok(data) => Ok(RefMut::from_backend(data, token)),
273 Err(error) => {
274 drop(token);
275 Err(ProgramError::from(error))
276 }
277 }
278 }
279
280 // ── Segment-aware access ───────────────────────────────────────
281
282 /// Project a typed segment from this account with segment-level
283 /// borrow tracking.
284 ///
285 /// The runtime validates the requested byte range, registers a
286 /// **leased** read borrow in the provided instruction-scoped
287 /// registry, and returns a [`SegRef<T>`](crate::SegRef) that
288 /// releases the lease on drop. This replaces the earlier
289 /// "instruction-sticky" behaviour: the registry entry is now tied
290 /// to the returned guard's lifetime, so sequential patterns like
291 /// `let x = segment_ref…; drop(x); let y = segment_ref…;` work
292 /// exactly the way Rust callers expect.
293 ///
294 /// On the native backend (Solana), the inner `Ref<T>` uses the
295 /// flat `{ptr, state}` representation, no dummy slice guard,
296 /// no intermediate `Ref<[u8]>`.
297 ///
298 /// The explicit `'a` lifetime binds the returned `SegRef<'a, T>`
299 /// to the shorter of `&self` (the account) and `&mut borrows`
300 /// (the registry). Either outliving the other would let the guard
301 /// dangle.
302 #[inline(always)]
303 pub fn segment_ref<'a, T: crate::Pod>(
304 &'a self,
305 borrows: &'a mut SegmentBorrowRegistry,
306 abs_offset: u32,
307 size: u32,
308 ) -> Result<crate::SegRef<'a, T>, ProgramError> {
309 let expected_size = core::mem::size_of::<T>() as u32;
310 if size != expected_size {
311 return ProgramError::err_invalid_argument();
312 }
313
314 let end = abs_offset
315 .checked_add(size)
316 .ok_or(ProgramError::ArithmeticOverflow)?;
317 if end as usize > self.data_len() {
318 return ProgramError::err_data_too_small();
319 }
320
321 let borrow = borrows.register_leased_read(self.address(), abs_offset, size)?;
322
323 // Build the inner `Ref<T>` via the existing flat/projected path.
324 #[cfg(target_os = "solana")]
325 let inner: Ref<'_, T> = {
326 // SAFETY: size, overflow, and bounds already validated above.
327 let native_ref = unsafe { self.backend().segment_ref_unchecked::<T>(abs_offset) };
328 let native_ref = match native_ref {
329 Ok(nr) => nr,
330 Err(e) => {
331 // Native guard could not be taken; undo the lease
332 // we just registered so the instruction-level view
333 // stays consistent.
334 borrows.release(&borrow);
335 return Err(ProgramError::from(e));
336 }
337 };
338 let (typed_ref, state_ptr) = native_ref.into_raw_parts();
339 Ref::from_segment(typed_ref as *const T, state_ptr)
340 };
341 #[cfg(not(target_os = "solana"))]
342 let inner: Ref<'_, T> = {
343 let data = match self.try_borrow() {
344 Ok(d) => d,
345 Err(e) => {
346 borrows.release(&borrow);
347 return Err(e);
348 }
349 };
350 // 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.
351 let ptr = unsafe { data.as_bytes_ptr().add(abs_offset as usize) as *const T };
352 unsafe { data.project(ptr) }
353 };
354
355 // SAFETY: `borrow` was just registered in `borrows`; the
356 // lease we construct will swap-remove it on drop.
357 let lease = unsafe { crate::SegmentLease::new(borrows, borrow) };
358 Ok(crate::SegRef::new(inner, lease))
359 }
360
361 /// Project a mutable typed segment. Mirror of [`Self::segment_ref`]; the
362 /// returned [`SegRefMut<T>`](crate::SegRefMut) carries both the
363 /// account-level exclusive borrow guard and the segment-registry
364 /// lease, so dropping it is a full release, no lingering entries.
365 ///
366 /// Under a bound `strict_writes` context the instruction-ambient gate
367 /// checks this EXACT byte range against the declared write policy, so
368 /// direct segment access outside a `Context` is governed too (the
369 /// `Context` methods enforce the same installed policy before
370 /// delegating to the ungated internal variant, paying the check once).
371 #[inline(always)]
372 pub fn segment_mut<'a, T: crate::Pod>(
373 &'a self,
374 borrows: &'a mut SegmentBorrowRegistry,
375 abs_offset: u32,
376 size: u32,
377 ) -> Result<crate::SegRefMut<'a, T>, ProgramError> {
378 crate::write_policy::check_data_mutation(self.address(), abs_offset, size)?;
379 self.segment_mut_ungated::<T>(borrows, abs_offset, size)
380 }
381
382 /// Ungated mirror of [`segment_mut`](Self::segment_mut) for
383 /// crate-internal callers (`Context`) that already enforced the same
384 /// installed policy for this exact range. See
385 /// [`try_borrow_mut_ungated`](Self::try_borrow_mut_ungated).
386 #[inline(always)]
387 pub(crate) fn segment_mut_ungated<'a, T: crate::Pod>(
388 &'a self,
389 borrows: &'a mut SegmentBorrowRegistry,
390 abs_offset: u32,
391 size: u32,
392 ) -> Result<crate::SegRefMut<'a, T>, ProgramError> {
393 self.check_writable()?;
394
395 let expected_size = core::mem::size_of::<T>() as u32;
396 if size != expected_size {
397 return ProgramError::err_invalid_argument();
398 }
399
400 let end = abs_offset
401 .checked_add(size)
402 .ok_or(ProgramError::ArithmeticOverflow)?;
403 if end as usize > self.data_len() {
404 return ProgramError::err_data_too_small();
405 }
406
407 let borrow = borrows.register_leased_write(self.address(), abs_offset, size)?;
408
409 #[cfg(target_os = "solana")]
410 let inner: RefMut<'_, T> = {
411 // 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.
412 let native_ref = unsafe { self.backend().segment_mut_unchecked::<T>(abs_offset) };
413 let native_ref = match native_ref {
414 Ok(nr) => nr,
415 Err(e) => {
416 borrows.release(&borrow);
417 return Err(ProgramError::from(e));
418 }
419 };
420 let (typed_ref, state_ptr) = native_ref.into_raw_parts();
421 RefMut::from_segment(typed_ref as *mut T, state_ptr)
422 };
423 #[cfg(not(target_os = "solana"))]
424 let inner: RefMut<'_, T> = {
425 let mut data = match self.try_borrow_mut_ungated() {
426 Ok(d) => d,
427 Err(e) => {
428 borrows.release(&borrow);
429 return Err(e);
430 }
431 };
432 // 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.
433 let ptr = unsafe { data.as_bytes_mut_ptr().add(abs_offset as usize) as *mut T };
434 unsafe { data.project(ptr) }
435 };
436
437 // 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.
438 let lease = unsafe { crate::SegmentLease::new(borrows, borrow) };
439 Ok(crate::SegRefMut::new(inner, lease))
440 }
441
442 /// Borrow **several disjoint byte ranges of one account** as
443 /// independent typed `&mut` guards at the same time.
444 ///
445 /// This is the ergonomic answer to "I need mutable access to two
446 /// fields of the same account simultaneously". A single
447 /// `segment_mut` call exclusively borrows the registry for the
448 /// returned guard's lifetime, so two `segment_mut` calls cannot
449 /// coexist. `split_segments_mut` registers **all** `N` ranges up
450 /// front, proving pairwise disjointness once through the borrow
451 /// registry, and returns an array of `N` guards that live together
452 /// and each release their lease on drop.
453 ///
454 /// Every range is `(abs_offset, size)` where `size == size_of::<T>()`.
455 /// Overlapping ranges are rejected with `AccountBorrowFailed`; an
456 /// out-of-bounds or wrong-size range is rejected with
457 /// `InvalidArgument` / `AccountDataTooSmall`, and any already-claimed
458 /// leases from the batch are rolled back before returning.
459 ///
460 /// ```ignore
461 /// // Mutate balance and nonce of the same vault at once.
462 /// let [mut bal, mut nonce] =
463 /// vault.split_segments_mut::<WireU64, 2>(ctx.borrows_mut(),
464 /// [(BALANCE_OFF, 8), (NONCE_OFF, 8)])?;
465 /// bal.set(bal.get() + amount);
466 /// nonce.set(nonce.get() + 1);
467 /// ```
468 pub fn split_segments_mut<'a, T: crate::Pod, const N: usize>(
469 &'a self,
470 borrows: &'a mut SegmentBorrowRegistry,
471 ranges: [(u32, u32); N],
472 ) -> Result<crate::SegmentsMut<'a, T, N>, ProgramError> {
473 // Under a bound `strict_writes` context, every requested range is
474 // checked against the instruction-ambient write gate, the same
475 // exact-range rule as `segment_mut`; so the batch surface cannot
476 // be used to bypass the declared policy from outside a `Context`.
477 for (off, size) in ranges {
478 crate::write_policy::check_data_mutation(self.address(), off, size)?;
479 }
480 self.split_segments_mut_ungated::<T, N>(borrows, ranges)
481 }
482
483 /// Ungated mirror of [`split_segments_mut`](Self::split_segments_mut)
484 /// for crate-internal callers (`Context`) that already enforced the
485 /// installed policy per range. See
486 /// [`try_borrow_mut_ungated`](Self::try_borrow_mut_ungated).
487 pub(crate) fn split_segments_mut_ungated<'a, T: crate::Pod, const N: usize>(
488 &'a self,
489 borrows: &'a mut SegmentBorrowRegistry,
490 ranges: [(u32, u32); N],
491 ) -> Result<crate::SegmentsMut<'a, T, N>, ProgramError> {
492 self.check_writable()?;
493 let expected = core::mem::size_of::<T>() as u32;
494 let data_len = self.data_len();
495
496 // Phase 1: validate + register every range **through the `&mut`**.
497 // The raw registry pointer the leases share is deliberately derived
498 // only after the final `&mut` use below: deriving it first and then
499 // using `borrows` would invalidate the raw under Stacked Borrows,
500 // leaving the rollback paths and every lease drop writing through a
501 // dead pointer. `register_leased_write` rejects a range that overlaps
502 // one already registered in this batch, so disjointness is proven
503 // here, once, up front.
504 // SAFETY: an array of `MaybeUninit` is itself always valid
505 // uninitialized; we initialize entries `0..i` before reading them.
506 let mut recs: [core::mem::MaybeUninit<crate::segment_borrow::SegmentBorrow>; N] =
507 unsafe { core::mem::MaybeUninit::uninit().assume_init() };
508 let mut offsets = [0usize; N];
509 let mut i = 0;
510 while i < N {
511 let (off, size) = ranges[i];
512 let in_bounds = match off.checked_add(size) {
513 Some(end) => end as usize <= data_len,
514 None => false,
515 };
516 if size != expected || !in_bounds {
517 // SAFETY: indices `0..i` were initialized and registered above.
518 unsafe { release_registered(borrows, &recs, i) };
519 return if size != expected {
520 ProgramError::err_invalid_argument()
521 } else {
522 ProgramError::err_data_too_small()
523 };
524 }
525 match borrows.register_leased_write(self.address(), off, size) {
526 Ok(b) => {
527 recs[i] = core::mem::MaybeUninit::new(b);
528 offsets[i] = off as usize;
529 }
530 Err(e) => {
531 // SAFETY: indices `0..i` were initialized and registered.
532 unsafe { release_registered(borrows, &recs, i) };
533 return Err(e);
534 }
535 }
536 i += 1;
537 }
538
539 // One exclusive byte borrow of the whole account backs every
540 // typed view; the registry leases prove the ranges are disjoint,
541 // so handing out N `&mut T` from this single borrow is sound.
542 // Ungated: the per-range ambient checks already ran (public
543 // wrapper) or the Context enforced the policy per range.
544 let data = match self.try_borrow_mut_ungated() {
545 Ok(d) => d,
546 Err(e) => {
547 // SAFETY: all N entries were registered in phase 1.
548 unsafe { release_registered(borrows, &recs, N) };
549 return Err(e);
550 }
551 };
552
553 // LAST use of the `&mut`: derive the single raw pointer every lease
554 // shares. All registry access from here on (lease drops) flows
555 // through copies of this one derivation, so the pointer's provenance
556 // stays valid for the guard's whole lifetime.
557 let reg_ptr = borrows as *mut SegmentBorrowRegistry;
558
559 // Build the N leases (each shares the one registry raw pointer,
560 // lifetime-pinned to `'a` by the `&'a mut borrows` we hold).
561 // SAFETY: array of `MaybeUninit` is valid uninitialized.
562 let mut leases: [core::mem::MaybeUninit<crate::SegmentLease<'a>>; N] =
563 unsafe { core::mem::MaybeUninit::uninit().assume_init() };
564 let mut k = 0;
565 while k < N {
566 // SAFETY: `recs[k]` was initialized in phase 1; `reg_ptr` is
567 // borrowed `&'a mut` for the returned guard's lifetime.
568 let lease = unsafe { crate::SegmentLease::from_raw(reg_ptr, recs[k].assume_init()) };
569 leases[k] = core::mem::MaybeUninit::new(lease);
570 k += 1;
571 }
572 // SAFETY: all N lease slots initialized.
573 let leases = unsafe {
574 let out = core::ptr::read(&leases as *const _ as *const [crate::SegmentLease<'a>; N]);
575 // The `MaybeUninit` array does not drop its contents; the forget
576 // documents that ownership moved into `out` via the read above.
577 #[allow(clippy::forget_non_drop)]
578 core::mem::forget(leases);
579 out
580 };
581
582 Ok(crate::SegmentsMut::new(data, offsets, leases))
583 }
584
585 // ── Const-driven segment access ─────────────────────────────────
586
587 /// Project a typed segment described by a compile-time [`crate::Segment`].
588 ///
589 /// This is the "const-driven" access form the Hopper design demands:
590 /// the offset and size come from a `const SEG: Segment = ...;`
591 /// declaration generated by `#[hopper::state]` or written by hand,
592 /// so the call collapses to a single `ptr + const_offset` add on
593 /// Solana SBF. No runtime string lookup, no dynamic map, no search.
594 ///
595 /// `segment.offset` is the **absolute** offset from the start of
596 /// account data (i.e. past the Hopper header already folded in).
597 /// Construct it via `Segment::new(offset, size)` or
598 /// `Segment::body(body_offset, size)`, the latter adds
599 /// `HopperHeader::SIZE` for you.
600 ///
601 /// ```ignore
602 /// const BALANCE: Segment = Segment::body(0, 8);
603 /// let mut balance = vault.segment_ref_const::<u64>(&mut borrows, BALANCE)?;
604 /// ```
605 #[inline(always)]
606 pub fn segment_ref_const<'a, T: crate::Pod>(
607 &'a self,
608 borrows: &'a mut SegmentBorrowRegistry,
609 segment: crate::segment::Segment,
610 ) -> Result<crate::SegRef<'a, T>, ProgramError> {
611 self.segment_ref::<T>(borrows, segment.offset, segment.size)
612 }
613
614 /// Mutable const-Segment access. See [`Self::segment_ref_const`] for the
615 /// contract, this is the exclusive variant.
616 #[inline(always)]
617 pub fn segment_mut_const<'a, T: crate::Pod>(
618 &'a self,
619 borrows: &'a mut SegmentBorrowRegistry,
620 segment: crate::segment::Segment,
621 ) -> Result<crate::SegRefMut<'a, T>, ProgramError> {
622 self.segment_mut::<T>(borrows, segment.offset, segment.size)
623 }
624
625 /// Project a typed segment described by a [`crate::TypedSegment`].
626 ///
627 /// This is the tightest form of segment access Hopper exposes: both
628 /// the type `T` and the offset are compile-time constants baked
629 /// into the [`crate::TypedSegment`] marker, so the call collapses to a
630 /// single `ptr + literal_offset` add with a literal size in the
631 /// bounds check. The marker argument is a zero-sized token, free
632 /// to pass around.
633 ///
634 /// ```ignore
635 /// const BALANCE: TypedSegment<WireU64, { HopperHeader::SIZE as u32 }>
636 /// = TypedSegment::new();
637 /// let bal = vault.segment_ref_typed(&mut borrows, BALANCE)?;
638 /// ```
639 #[inline(always)]
640 pub fn segment_ref_typed<'a, T: crate::Pod, const OFFSET: u32>(
641 &'a self,
642 borrows: &'a mut SegmentBorrowRegistry,
643 _segment: crate::segment::TypedSegment<T, OFFSET>,
644 ) -> Result<crate::SegRef<'a, T>, ProgramError> {
645 self.segment_ref::<T>(borrows, OFFSET, core::mem::size_of::<T>() as u32)
646 }
647
648 /// Mutable typed-segment access. See [`Self::segment_ref_typed`] for the
649 /// contract, this is the exclusive variant.
650 #[inline(always)]
651 pub fn segment_mut_typed<'a, T: crate::Pod, const OFFSET: u32>(
652 &'a self,
653 borrows: &'a mut SegmentBorrowRegistry,
654 _segment: crate::segment::TypedSegment<T, OFFSET>,
655 ) -> Result<crate::SegRefMut<'a, T>, ProgramError> {
656 self.segment_mut::<T>(borrows, OFFSET, core::mem::size_of::<T>() as u32)
657 }
658
659 // ── Zero-copy overlay access ─────────────────────────────────────
660
661 // ── Typed load (LayoutContract-aware) ────────────────────────────
662
663 /// Load a typed layout after validating the account header.
664 ///
665 /// This is the canonical "validate then project" path:
666 /// 1. Check disc, version, and layout_id match `T`
667 /// 2. Verify data length >= `T::SIZE`
668 /// 3. Return zero-copy reference into account data
669 ///
670 /// The returned reference begins at `T::TYPE_OFFSET`. Body-only layouts
671 /// project past the Hopper header; header-inclusive layouts project the
672 /// full account struct from byte 0.
673 ///
674 /// # Example
675 ///
676 /// ```ignore
677 /// let vault = account.load::<Vault>()?;
678 /// ```
679 #[inline(always)]
680 pub fn load<T: LayoutContract + crate::Pod>(&self) -> Result<Ref<'_, T>, ProgramError> {
681 let data = self.try_borrow()?;
682 T::validate_header(&data)?;
683 if data.len() < T::required_len() {
684 return ProgramError::err_data_too_small();
685 }
686 // 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.
687 let ptr = unsafe { data.as_bytes_ptr().add(T::TYPE_OFFSET) as *const T };
688 // SAFETY: Header and length validated above. `ptr` points into the borrowed bytes.
689 Ok(unsafe { data.project(ptr) })
690 }
691
692 /// Borrow a typed layout for the duration of a closure.
693 ///
694 /// This is the ergonomic safe path for read-only handlers: Hopper still
695 /// validates the header and holds the data borrow guard, while user code
696 /// gets a plain `&T` inside the closure.
697 #[inline]
698 pub fn with<T, R, F>(&self, f: F) -> Result<R, ProgramError>
699 where
700 T: LayoutContract + crate::Pod,
701 F: FnOnce(&T) -> Result<R, ProgramError>,
702 {
703 let account = self.load::<T>()?;
704 f(&*account)
705 }
706
707 /// Load a mutable typed layout after validating the account header.
708 ///
709 /// Same as `load()` but provides a mutable reference for in-place
710 /// state updates. Changes write directly to account data.
711 ///
712 /// # Example
713 ///
714 /// ```ignore
715 /// let mut vault = account.load_mut::<Vault>()?;
716 /// vault.balance = vault.balance.checked_add(amount)?;
717 /// ```
718 #[inline(always)]
719 pub fn load_mut<T: LayoutContract + crate::Pod>(&self) -> Result<RefMut<'_, T>, ProgramError> {
720 let mut data = self.try_borrow_mut()?;
721 T::validate_header(&data)?;
722 if data.len() < T::required_len() {
723 return ProgramError::err_data_too_small();
724 }
725 // Typed whole-account write borrows stamp the instruction-
726 // AMBIENT touch log directly (no Context in reach here), which
727 // is what makes wrapper `get_mut` / raw `load_mut` visible to
728 // emitted touch maps. Footprint only, liveness stays with the
729 // account borrow byte. Reads are not recorded (validators read
730 // every account; the map's job is write containment).
731 #[cfg(feature = "touch-map")]
732 crate::segment_borrow::touch_log::record_account(
733 self.address(),
734 data.len() as u32,
735 crate::segment_borrow::AccessKind::Write,
736 );
737 // 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.
738 let ptr = unsafe { data.as_bytes_mut_ptr().add(T::TYPE_OFFSET) as *mut T };
739 // SAFETY: Header and length validated above. `ptr` points into the borrowed bytes.
740 Ok(unsafe { data.project(ptr) })
741 }
742
743 /// Mutably borrow a typed layout for the duration of a closure.
744 ///
745 /// This keeps the zero-copy borrow guard scoped to the closure while making
746 /// common updates read like direct state mutation.
747 #[inline]
748 pub fn with_mut<T, R, F>(&self, f: F) -> Result<R, ProgramError>
749 where
750 T: LayoutContract + crate::Pod,
751 F: FnOnce(&mut T) -> Result<R, ProgramError>,
752 {
753 let mut account = self.load_mut::<T>()?;
754 f(&mut *account)
755 }
756
757 // ── Tier 1 compact load (`[disc:u8][body]`) ─────────────────────
758
759 /// Load a Tier-1 compact layout: `[disc:u8][zero-copy body]`.
760 ///
761 /// The hot path is `check_len_exact` + `check_disc` + project-body-at-byte-1.
762 /// Unlike [`load`](Self::load) there is **no** 16-byte header, no
763 /// layout_id read, and no schema-epoch comparison. Layout identity is
764 /// a program-level fact (the Tier-2 registry), not a per-account one.
765 ///
766 /// # Example
767 ///
768 /// ```ignore
769 /// let vault = account.load_compact::<Vault>()?;
770 /// ```
771 #[inline(always)]
772 pub fn load_compact<T: crate::CompactLayout>(&self) -> Result<Ref<'_, T>, ProgramError> {
773 let data = self.try_borrow()?;
774 T::validate_compact(&data)?;
775 // 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.
776 let ptr =
777 unsafe { data.as_bytes_ptr().add(crate::compact::COMPACT_BODY_OFFSET) as *const T };
778 // SAFETY: length and disc validated above; `ptr` points into the borrowed body.
779 Ok(unsafe { data.project(ptr) })
780 }
781
782 /// Mutable Tier-1 compact load. See [`load_compact`](Self::load_compact).
783 #[inline(always)]
784 pub fn load_compact_mut<T: crate::CompactLayout>(&self) -> Result<RefMut<'_, T>, ProgramError> {
785 let mut data = self.try_borrow_mut()?;
786 T::validate_compact(&data)?;
787 // Same ambient stamp as `load_mut`: typed whole-account write.
788 #[cfg(feature = "touch-map")]
789 crate::segment_borrow::touch_log::record_account(
790 self.address(),
791 data.len() as u32,
792 crate::segment_borrow::AccessKind::Write,
793 );
794 // 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.
795 let ptr = unsafe {
796 data.as_bytes_mut_ptr()
797 .add(crate::compact::COMPACT_BODY_OFFSET) as *mut T
798 };
799 // SAFETY: length and disc validated above; `ptr` points into the borrowed body.
800 Ok(unsafe { data.project(ptr) })
801 }
802
803 /// Borrow a compact layout for the duration of a closure (read-only).
804 #[inline]
805 pub fn with_compact<T, R, F>(&self, f: F) -> Result<R, ProgramError>
806 where
807 T: crate::CompactLayout,
808 F: FnOnce(&T) -> Result<R, ProgramError>,
809 {
810 let account = self.load_compact::<T>()?;
811 f(&*account)
812 }
813
814 /// Mutably borrow a compact layout for the duration of a closure.
815 #[inline]
816 pub fn with_compact_mut<T, R, F>(&self, f: F) -> Result<R, ProgramError>
817 where
818 T: crate::CompactLayout,
819 F: FnOnce(&mut T) -> Result<R, ProgramError>,
820 {
821 let mut account = self.load_compact_mut::<T>()?;
822 f(&mut *account)
823 }
824
825 /// Initialise a compact account by stamping the discriminator byte.
826 ///
827 /// Writes `T::DISC` at byte 0; the body is left as-is (callers
828 /// typically follow with [`load_compact_mut`](Self::load_compact_mut)
829 /// to populate it). Requires the account to be writable and exactly
830 /// `T::COMPACT_LEN` bytes long.
831 #[inline(always)]
832 pub fn init_compact<T: crate::CompactLayout>(&self) -> ProgramResult {
833 self.check_writable()?;
834 let mut data = self.try_borrow_mut()?;
835 if data.len() < T::COMPACT_LEN {
836 return Err(ProgramError::AccountDataTooSmall);
837 }
838 if data.len() != T::COMPACT_LEN {
839 return Err(ProgramError::InvalidAccountData);
840 }
841 data[0] = T::DISC;
842 Ok(())
843 }
844
845 /// Tier-1 compact **dynamic** load: validate the discriminator and the
846 /// minimum length, then project the fixed head at
847 /// [`COMPACT_BODY_OFFSET`](crate::compact::COMPACT_BODY_OFFSET).
848 ///
849 /// Unlike [`load_compact`](Self::load_compact), the account may be longer
850 /// than the fixed head: the trailing bytes are the dynamic tail, left
851 /// untouched here and accessed through the generated `tail_*` helpers.
852 /// This is the `[disc:u8][fixed_head][tail]` analogue of
853 /// [`load`](Self::load)'s tolerance of a headered dynamic tail.
854 ///
855 /// # Example
856 ///
857 /// ```ignore
858 /// let head = account.load_compact_dynamic::<Market>()?; // fixed head
859 /// let data = account.try_borrow()?;
860 /// let tail = Market::tail_read(&data)?; // dynamic tail
861 /// ```
862 #[inline(always)]
863 pub fn load_compact_dynamic<T: crate::CompactDynamicLayout>(
864 &self,
865 ) -> Result<Ref<'_, T>, ProgramError> {
866 let data = self.try_borrow()?;
867 T::validate_compact_dynamic(&data)?;
868 // SAFETY: validate_compact_dynamic guarantees `data.len() >= 1 +
869 // size_of::<T>()`, `T` is Pod (align 1, all-bit-patterns valid), and
870 // the fixed head begins at COMPACT_BODY_OFFSET. Trailing tail bytes are
871 // never read through this `&T`.
872 let ptr =
873 unsafe { data.as_bytes_ptr().add(crate::compact::COMPACT_BODY_OFFSET) as *const T };
874 // SAFETY: length and disc validated above; `ptr` points into the borrowed head.
875 Ok(unsafe { data.project(ptr) })
876 }
877
878 /// Mutable Tier-1 compact-dynamic load of the fixed head.
879 /// See [`load_compact_dynamic`](Self::load_compact_dynamic).
880 #[inline(always)]
881 pub fn load_compact_dynamic_mut<T: crate::CompactDynamicLayout>(
882 &self,
883 ) -> Result<RefMut<'_, T>, ProgramError> {
884 let mut data = self.try_borrow_mut()?;
885 T::validate_compact_dynamic(&data)?;
886 // SAFETY: see `load_compact_dynamic`; the head window is exclusively
887 // borrowed for the lifetime of the returned guard.
888 let ptr = unsafe {
889 data.as_bytes_mut_ptr()
890 .add(crate::compact::COMPACT_BODY_OFFSET) as *mut T
891 };
892 // SAFETY: length and disc validated above; `ptr` points into the borrowed head.
893 Ok(unsafe { data.project(ptr) })
894 }
895
896 /// Borrow a compact-dynamic fixed head for the duration of a closure.
897 #[inline]
898 pub fn with_compact_dynamic<T, R, F>(&self, f: F) -> Result<R, ProgramError>
899 where
900 T: crate::CompactDynamicLayout,
901 F: FnOnce(&T) -> Result<R, ProgramError>,
902 {
903 let account = self.load_compact_dynamic::<T>()?;
904 f(&*account)
905 }
906
907 /// Mutably borrow a compact-dynamic fixed head for the duration of a closure.
908 #[inline]
909 pub fn with_compact_dynamic_mut<T, R, F>(&self, f: F) -> Result<R, ProgramError>
910 where
911 T: crate::CompactDynamicLayout,
912 F: FnOnce(&mut T) -> Result<R, ProgramError>,
913 {
914 let mut account = self.load_compact_dynamic_mut::<T>()?;
915 f(&mut *account)
916 }
917
918 /// Initialise a compact-dynamic account: stamp `T::DISC` at byte 0 and, if
919 /// the account was allocated with room for a tail, zero the tail's `u32`
920 /// length prefix so a fresh account reads as an **empty** tail rather than
921 /// uninitialized bytes (fail-closed init).
922 ///
923 /// Requires the account to be writable and at least `T::MIN_LEN` bytes
924 /// (discriminator + fixed head). The tail region may be larger to reserve
925 /// growth headroom.
926 #[inline(always)]
927 pub fn init_compact_dynamic<T: crate::CompactDynamicLayout>(&self) -> ProgramResult {
928 self.check_writable()?;
929 let mut data = self.try_borrow_mut()?;
930 if data.len() < T::MIN_LEN {
931 return Err(ProgramError::AccountDataTooSmall);
932 }
933 data[0] = T::DISC;
934 // Stamp an empty-tail length prefix when the allocation has room for it.
935 if data.len() >= T::TAIL_OFFSET + 4 {
936 data[T::TAIL_OFFSET..T::TAIL_OFFSET + 4].copy_from_slice(&0u32.to_le_bytes());
937 }
938 Ok(())
939 }
940
941 /// Explicit raw typed read of the account buffer.
942 ///
943 /// This bypasses Hopper layout validation and segment tracking, but it still
944 /// respects the account-level borrow rules enforced by `try_borrow()`.
945 #[inline(always)]
946 ///
947 /// # Safety
948 ///
949 /// Caller must uphold the invariants documented for this unsafe API before invoking it.
950 pub unsafe fn raw_ref<T: crate::Pod>(&self) -> Result<Ref<'_, T>, ProgramError> {
951 let data = self.try_borrow()?;
952 if core::mem::size_of::<T>() > data.len() {
953 return Err(ProgramError::AccountDataTooSmall);
954 }
955 let ptr = data.as_ptr() as *const T;
956 // 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.
957 Ok(unsafe { data.project(ptr) })
958 }
959
960 /// Explicit raw typed write of the account buffer.
961 ///
962 /// This bypasses Hopper layout validation and segment tracking, but it still
963 /// enforces writability and the account-level exclusive borrow rules.
964 #[inline(always)]
965 ///
966 /// # Safety
967 ///
968 /// Caller must uphold the invariants documented for this unsafe API before invoking it.
969 pub unsafe fn raw_mut<T: crate::Pod>(&self) -> Result<RefMut<'_, T>, ProgramError> {
970 self.check_writable()?;
971 // Deliberately ungated: `raw_mut` is one of the documented `unsafe`
972 // escape hatches (`hopper lint --deny-escapes` refuses it in program
973 // code). The ambient write gate governs the SAFE surfaces; the
974 // unsafe tier remains an explicit, grep-able opt-out.
975 let mut data = self.try_borrow_mut_ungated()?;
976 if core::mem::size_of::<T>() > data.len() {
977 return Err(ProgramError::AccountDataTooSmall);
978 }
979 let ptr = data.as_bytes_mut_ptr() as *mut T;
980 // 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.
981 Ok(unsafe { data.project(ptr) })
982 }
983
984 /// Load a cross-program layout without ownership checks.
985 ///
986 /// Validates the layout contract but does not check that the account is
987 /// owned by this program. Use for cross-program
988 /// reads where the account is owned by another program and you need
989 /// a typed, zero-copy view of its data.
990 ///
991 /// Full contract validation ensures ABI compatibility: if the other
992 /// program changes its layout identity or schema epoch, this fails rather
993 /// than silently misinterpreting bytes.
994 ///
995 /// # Example
996 ///
997 /// ```ignore
998 /// let other_vault = foreign_account.load_cross_program::<OtherVault>()?;
999 /// ```
1000 #[inline(always)]
1001 pub fn load_cross_program<T: LayoutContract + crate::Pod>(
1002 &self,
1003 ) -> Result<Ref<'_, T>, ProgramError> {
1004 let data = self.try_borrow()?;
1005 T::validate_header(&data)?;
1006 // Defense in depth: `validate_header`'s default impl already checks this,
1007 // but a foreign `LayoutContract` could override it. Re-check the projection
1008 // length explicitly so an overridden or mismatched contract can never
1009 // produce an out-of-bounds typed view from another program's bytes.
1010 if data.len() < T::required_len() {
1011 return ProgramError::err_data_too_small();
1012 }
1013 // 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.
1014 let ptr = unsafe { data.as_bytes_ptr().add(T::TYPE_OFFSET) as *const T };
1015 // SAFETY: Wire identity and size validated above.
1016 Ok(unsafe { data.project(ptr) })
1017 }
1018
1019 /// Read runtime layout metadata from this account's header.
1020 ///
1021 /// Returns `None` if the account data is too short for a Hopper header.
1022 /// This is useful for runtime inspection, manager tooling, and schema
1023 /// checking when the concrete layout type is not known at compile time.
1024 #[inline(always)]
1025 pub fn layout_info(&self) -> Option<crate::layout::LayoutInfo> {
1026 let data = self.try_borrow().ok()?;
1027 crate::layout::LayoutInfo::from_data(&data)
1028 }
1029
1030 /// Compile-time field metadata for a layout contract.
1031 #[inline(always)]
1032 pub fn fields<T: LayoutContract>() -> &'static [FieldInfo] {
1033 T::fields()
1034 }
1035
1036 /// Find a compile-time field descriptor by name.
1037 ///
1038 /// This is a tooling/inspection helper that delegates to
1039 /// `FieldMap::field_by_name`. It performs a const-driven linear
1040 /// scan over `T::FIELDS` and is not intended for hot-path use -
1041 /// programs should reach for the const offsets emitted by
1042 /// `#[hopper::state]` instead.
1043 #[inline]
1044 pub fn field<T: LayoutContract>(name: &str) -> Option<&'static FieldInfo> {
1045 <T as crate::field_map::FieldMap>::field_by_name(name)
1046 }
1047
1048 /// Return the extension-region byte range for a layout that declares one.
1049 ///
1050 /// Callers can apply the returned range to a borrowed data slice when they
1051 /// want to inspect or mutate extension bytes explicitly.
1052 #[inline(always)]
1053 pub fn extension_range<T: LayoutContract>(
1054 &self,
1055 ) -> Result<core::ops::Range<usize>, ProgramError> {
1056 let offset = T::EXTENSION_OFFSET.ok_or(ProgramError::InvalidArgument)?;
1057 let data_len = self.data_len();
1058 if data_len < offset {
1059 return Err(ProgramError::AccountDataTooSmall);
1060 }
1061 Ok(offset..data_len)
1062 }
1063
1064 /// Borrow the extension/tail region declared by a layout contract.
1065 #[inline(always)]
1066 pub fn extension_bytes<T: LayoutContract>(&self) -> Result<Ref<'_, [u8]>, ProgramError> {
1067 let offset = T::EXTENSION_OFFSET.ok_or(ProgramError::InvalidArgument)?;
1068 let data = self.try_borrow()?;
1069 if data.len() < offset {
1070 return Err(ProgramError::AccountDataTooSmall);
1071 }
1072 Ok(data.slice_from(offset))
1073 }
1074
1075 /// Mutably borrow the extension/tail region declared by a layout contract.
1076 #[inline(always)]
1077 pub fn extension_bytes_mut<T: LayoutContract>(&self) -> Result<RefMut<'_, [u8]>, ProgramError> {
1078 let offset = T::EXTENSION_OFFSET.ok_or(ProgramError::InvalidArgument)?;
1079 let len = self.data_len();
1080 if len < offset {
1081 return Err(ProgramError::AccountDataTooSmall);
1082 }
1083 // Ambient gate: the mutable grant is exactly the extension region
1084 // `[offset, len)`, so a tail-declared policy (open-ended range) or a
1085 // whole-account grant authorizes it, while a head-only declaration
1086 // refuses it. Empty extension regions grant nothing and skip the
1087 // check.
1088 if len > offset {
1089 crate::write_policy::check_data_mutation(
1090 self.address(),
1091 offset as u32,
1092 (len - offset) as u32,
1093 )?;
1094 }
1095 let data = self.try_borrow_mut_ungated()?;
1096 Ok(data.slice_from(offset))
1097 }
1098
1099 /// Zero the byte range `[start, start + len)`, checked against the
1100 /// instruction-ambient write policy over **exactly that range**.
1101 ///
1102 /// This is the precise-authority spelling of "clear these bytes." The
1103 /// naive alternative, take a whole-account `try_borrow_mut` and slice,
1104 /// demands authority over every byte of the account, so a narrow but
1105 /// entirely legitimate declaration (a `tail(seq)` grant zero-filling
1106 /// the tail it just grew) would be refused by its own policy. Gating
1107 /// the exact range keeps the refusal honest: it fires when the bytes
1108 /// being cleared are outside the declaration, and not before.
1109 ///
1110 /// An empty range is a no-op and requires no authority.
1111 #[inline]
1112 pub fn zero_range(&self, start: usize, len: usize) -> ProgramResult {
1113 if len == 0 {
1114 return Ok(());
1115 }
1116 let end = start
1117 .checked_add(len)
1118 .ok_or(ProgramError::ArithmeticOverflow)?;
1119 if end > self.data_len() {
1120 return Err(ProgramError::AccountDataTooSmall);
1121 }
1122 let offset_u32 = u32::try_from(start).map_err(|_| ProgramError::ArithmeticOverflow)?;
1123 let len_u32 = u32::try_from(len).map_err(|_| ProgramError::ArithmeticOverflow)?;
1124 crate::write_policy::check_data_mutation(self.address(), offset_u32, len_u32)?;
1125 let mut data = self.try_borrow_mut_ungated()?;
1126 for byte in data[start..end].iter_mut() {
1127 *byte = 0;
1128 }
1129 Ok(())
1130 }
1131
1132 /// Zero the bytes a grow just appended: `[previous_len, data_len)`.
1133 ///
1134 /// Authorized by the **transition** dimension, not the byte-range one,
1135 /// deliberately, and this is the whole reason it is a separate
1136 /// method from [`zero_range`](Self::zero_range):
1137 ///
1138 /// - The bytes did not exist when the policy was declared. Clearing
1139 /// them cannot destroy, reveal, or corrupt any state a byte-range
1140 /// declaration protects, so requiring a declared range over them
1141 /// would refuse the framework's own `realloc_zero` lifecycle on
1142 /// every narrow declaration (`mut(seg)` + `realloc`) while
1143 /// protecting nothing.
1144 /// - The authority to create them was already checked: `resize`
1145 /// consults [`check_account_transition`], and an account carrying no
1146 /// declared data authority cannot resize in the first place. Same
1147 /// check here, so this method can never reach an account the
1148 /// instruction has no data authority over.
1149 /// - It is strictly narrower than the pre-existing body: a caller
1150 /// cannot name an offset, only "whatever the grow added."
1151 ///
1152 /// Writes into the PRE-EXISTING body remain governed by the byte-range
1153 /// policy through every other surface.
1154 ///
1155 /// [`check_account_transition`]: crate::write_policy
1156 #[inline]
1157 pub fn zero_appended(&self, previous_len: usize) -> ProgramResult {
1158 let len = self.data_len();
1159 if previous_len >= len {
1160 return Ok(());
1161 }
1162 crate::write_policy::check_account_transition(self.address())?;
1163 let mut data = self.try_borrow_mut_ungated()?;
1164 for byte in data[previous_len..len].iter_mut() {
1165 *byte = 0;
1166 }
1167 Ok(())
1168 }
1169
1170 /// Initialize an account with the given layout contract header.
1171 ///
1172 /// Writes the disc, version, layout_id, and zeroes flags/reserved.
1173 /// Call this when creating a new account before writing field data.
1174 #[inline(always)]
1175 pub fn init_layout<T: LayoutContract>(&self) -> ProgramResult {
1176 let mut data = self.try_borrow_mut()?;
1177 crate::layout::init_header::<T>(&mut data)
1178 }
1179
1180 // ── Validation helpers ───────────────────────────────────────────
1181
1182 /// Validate that this account is a signer.
1183 #[inline(always)]
1184 pub fn require_signer(&self) -> ProgramResult {
1185 if self.is_signer() {
1186 Ok(())
1187 } else {
1188 ProgramError::err_missing_signer()
1189 }
1190 }
1191
1192 /// Validate that this account is writable.
1193 #[inline(always)]
1194 pub fn require_writable(&self) -> ProgramResult {
1195 if self.is_writable() {
1196 Ok(())
1197 } else {
1198 ProgramError::err_immutable()
1199 }
1200 }
1201
1202 /// Validate that this account is owned by the given program.
1203 #[inline(always)]
1204 pub fn require_owned_by(&self, program: &Address) -> ProgramResult {
1205 if self.owned_by(program) {
1206 Ok(())
1207 } else {
1208 ProgramError::err_incorrect_program()
1209 }
1210 }
1211
1212 /// Validate signer + writable (common "payer" pattern).
1213 #[inline(always)]
1214 pub fn require_payer(&self) -> ProgramResult {
1215 self.require_signer()?;
1216 self.require_writable()
1217 }
1218
1219 // ── Chainable validation ─────────────────────────────────────────
1220
1221 /// Chainable signer check.
1222 #[inline(always)]
1223 pub fn check_signer(&self) -> Result<&Self, ProgramError> {
1224 if self.is_signer() {
1225 Ok(self)
1226 } else {
1227 ProgramError::err_missing_signer()
1228 }
1229 }
1230
1231 /// Chainable writable check.
1232 #[inline(always)]
1233 pub fn check_writable(&self) -> Result<&Self, ProgramError> {
1234 if self.is_writable() {
1235 Ok(self)
1236 } else {
1237 ProgramError::err_immutable()
1238 }
1239 }
1240
1241 /// Chainable ownership check.
1242 #[inline(always)]
1243 pub fn check_owned_by(&self, program: &Address) -> Result<&Self, ProgramError> {
1244 if self.owned_by(program) {
1245 Ok(self)
1246 } else {
1247 ProgramError::err_incorrect_program()
1248 }
1249 }
1250
1251 /// Chainable check that this account's owner is **one of** `programs`.
1252 ///
1253 /// Accepts an account from any of several programs, most commonly an SPL
1254 /// Token *or* Token-2022 mint / token account, and rejects every other
1255 /// owner. This is [`check_owned_by`](Self::check_owned_by) generalized to a
1256 /// set; an empty `programs` slice always rejects.
1257 #[inline]
1258 pub fn check_owned_by_any(&self, programs: &[&Address]) -> Result<&Self, ProgramError> {
1259 if programs.iter().any(|program| self.owned_by(program)) {
1260 Ok(self)
1261 } else {
1262 ProgramError::err_incorrect_program()
1263 }
1264 }
1265
1266 /// Chainable discriminator check.
1267 #[inline(always)]
1268 pub fn check_disc(&self, expected: u8) -> Result<&Self, ProgramError> {
1269 if self.disc() == expected {
1270 Ok(self)
1271 } else {
1272 Err(ProgramError::InvalidAccountData)
1273 }
1274 }
1275
1276 /// Chainable non-empty data check.
1277 #[inline(always)]
1278 pub fn check_has_data(&self) -> Result<&Self, ProgramError> {
1279 if !self.is_data_empty() {
1280 Ok(self)
1281 } else {
1282 Err(ProgramError::AccountDataTooSmall)
1283 }
1284 }
1285
1286 /// Chainable executable check.
1287 #[inline(always)]
1288 pub fn check_executable(&self) -> Result<&Self, ProgramError> {
1289 if self.executable() {
1290 Ok(self)
1291 } else {
1292 Err(ProgramError::InvalidArgument)
1293 }
1294 }
1295
1296 /// Chainable address check.
1297 #[inline(always)]
1298 pub fn check_address(&self, expected: &Address) -> Result<&Self, ProgramError> {
1299 if address_eq(self.address(), expected) {
1300 Ok(self)
1301 } else {
1302 Err(ProgramError::InvalidArgument)
1303 }
1304 }
1305
1306 /// Chainable minimum data length check.
1307 #[inline(always)]
1308 pub fn check_data_len(&self, min_len: usize) -> Result<&Self, ProgramError> {
1309 if self.data_len() >= min_len {
1310 Ok(self)
1311 } else {
1312 Err(ProgramError::AccountDataTooSmall)
1313 }
1314 }
1315
1316 /// Chainable version check.
1317 #[inline(always)]
1318 pub fn check_version(&self, expected: u8) -> Result<&Self, ProgramError> {
1319 if self.version() == expected {
1320 Ok(self)
1321 } else {
1322 Err(ProgramError::InvalidAccountData)
1323 }
1324 }
1325
1326 /// Chainable full layout contract check (disc + version + layout_id + size).
1327 #[inline(always)]
1328 pub fn check_layout<T: LayoutContract>(&self) -> Result<&Self, ProgramError> {
1329 let data = self.try_borrow()?;
1330 T::validate_header(&data)?;
1331 Ok(self)
1332 }
1333
1334 /// Start a proof-carrying validation chain for this account.
1335 #[inline(always)]
1336 pub const fn proof(&self) -> crate::proof::AccountProof<'_> {
1337 crate::proof::AccountProof::new(self)
1338 }
1339
1340 // ── Hopper header readers ────────────────────────────────────────
1341
1342 /// Read the Hopper account discriminator (first byte of data).
1343 #[inline(always)]
1344 pub fn disc(&self) -> u8 {
1345 native_boundary::disc(self.backend())
1346 }
1347
1348 /// Read the Hopper account version (second byte of data).
1349 #[inline(always)]
1350 pub fn version(&self) -> u8 {
1351 native_boundary::version(self.backend())
1352 }
1353
1354 /// Read the 8-byte layout_id from the Hopper account header (bytes 4..12).
1355 #[inline(always)]
1356 pub fn layout_id(&self) -> Option<&[u8; 8]> {
1357 native_boundary::layout_id(self.backend())
1358 }
1359
1360 /// Verify that this account has the given discriminator.
1361 #[inline(always)]
1362 pub fn require_disc(&self, expected: u8) -> ProgramResult {
1363 if self.disc() == expected {
1364 Ok(())
1365 } else {
1366 Err(ProgramError::InvalidAccountData)
1367 }
1368 }
1369
1370 // ── Packed flags ─────────────────────────────────────────────────
1371
1372 /// Pack the account's boolean flags into a single byte.
1373 ///
1374 /// Bit layout: bit 0 = signer, bit 1 = writable, bit 2 = executable,
1375 /// bit 3 = has data.
1376 ///
1377 /// Delegates to the native backend, which extracts signer/writable/
1378 /// executable from **one** packed-u32 header read instead of three
1379 /// separate byte loads.
1380 #[inline(always)]
1381 pub fn flags(&self) -> u8 {
1382 self.backend().flags()
1383 }
1384
1385 /// Check that the account's flags contain all required bits.
1386 #[inline(always)]
1387 pub fn expect_flags(&self, required: u8) -> ProgramResult {
1388 if self.flags() & required == required {
1389 Ok(())
1390 } else {
1391 Err(ProgramError::InvalidArgument)
1392 }
1393 }
1394
1395 /// Fused signer/writable validation (the generated-context hot path).
1396 ///
1397 /// Validates both requirements with a **single packed-flags read and
1398 /// one masked compare**, the same shape a hand-rolled
1399 /// `header & MASK == MASK` check compiles to, since `need_signer` /
1400 /// `need_writable` are compile-time literals at every macro call site
1401 /// and this function is `#[inline(always)]`. On mismatch it falls back
1402 /// to the individual checks so the error stays precise
1403 /// (`MissingRequiredSignature` vs `Immutable`); the fallback runs only
1404 /// on the failure path, where compute cost is irrelevant.
1405 #[inline(always)]
1406 pub fn expect_signer_writable(&self, need_signer: bool, need_writable: bool) -> ProgramResult {
1407 // Fast path: one packed-header read + one masked compare on the native
1408 // backend, never touching `data_len` (unlike `flags()`, which also
1409 // computes the has-data bit). `need_signer`/`need_writable` are
1410 // compile-time literals here, so the mask/expected pair fold to
1411 // constants.
1412 if self
1413 .backend()
1414 .is_signer_writable(need_signer, need_writable)
1415 {
1416 return Ok(());
1417 }
1418 // Failure path: re-check individually for the precise error.
1419 if need_signer {
1420 self.require_signer()?;
1421 }
1422 if need_writable {
1423 self.require_writable()?;
1424 }
1425 // Unreachable when the fused compare failed for one of the two
1426 // requested bits, but keeps the signature total.
1427 Ok(())
1428 }
1429
1430 // ── Resize / Close ───────────────────────────────────────────────
1431
1432 /// Resize the account data, zeroing any newly exposed region on growth.
1433 ///
1434 /// See [`hopper_native::AccountView::resize`] for why zero-on-growth
1435 /// is the safe default. Use [`resize_raw`](Self::resize_raw) for the
1436 /// hot path when the caller overwrites the grown region in full.
1437 #[inline]
1438 pub fn resize(&self, new_len: usize) -> ProgramResult {
1439 // Ambient gate: a data-length transition on a gated instruction is
1440 // permitted only for accounts carrying declared write authority
1441 // (`GateCheck::Transition`); foreign accounts fail closed.
1442 crate::write_policy::check_account_transition(self.address())?;
1443 native_boundary::resize(self.backend(), new_len)
1444 }
1445
1446 /// Resize the account data without zero-filling the grown region.
1447 #[inline]
1448 pub fn resize_raw(&self, new_len: usize) -> ProgramResult {
1449 // Same transition gate as [`resize`](Self::resize).
1450 crate::write_policy::check_account_transition(self.address())?;
1451 native_boundary::resize_raw(self.backend(), new_len)
1452 }
1453
1454 /// Assign a new owner.
1455 ///
1456 /// # Safety
1457 ///
1458 /// The caller must ensure the account is writable and that ownership
1459 /// transfer is authorized.
1460 #[inline(always)]
1461 pub unsafe fn assign(&self, new_owner: &Address) {
1462 // 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.
1463 unsafe {
1464 native_boundary::assign(self.backend(), new_owner);
1465 }
1466 }
1467
1468 /// Close the account: zero lamports and data.
1469 #[inline]
1470 pub fn close(&self) -> ProgramResult {
1471 // Ambient gate: closing is a presence transition; on a gated
1472 // instruction only accounts with declared write authority may close.
1473 crate::write_policy::check_account_transition(self.address())?;
1474 native_boundary::close(self.backend())
1475 }
1476
1477 /// Close the account, transferring remaining lamports to `destination`.
1478 ///
1479 /// Idiomatic Solana close pattern: move all lamports to the
1480 /// destination account, then zero this account's data so the
1481 /// runtime garbage-collects it at the end of the transaction.
1482 ///
1483 /// # Preconditions (enforced)
1484 ///
1485 /// Per Solana's account modification rules (only the owning program
1486 /// can debit lamports or mutate data on a writable account), this
1487 /// method requires:
1488 ///
1489 /// - `self` must be **writable**, otherwise the runtime will
1490 /// reject the commit anyway, but we fail fast here rather than
1491 /// let the transaction progress through an invalid state.
1492 /// - `self` must be **owned by `program_id`**, the program that
1493 /// is executing this instruction. Without this check the safe
1494 /// API would silently encourage patterns that only Solana's
1495 /// post-instruction verifier catches.
1496 /// - `destination` must be **writable**, receiving lamports
1497 /// requires write permission on the credit side.
1498 ///
1499 /// The earlier version mutated lamports and zeroed data without
1500 /// checking either side, relying on the runtime to reject the
1501 /// transaction later. That encouraged
1502 /// patterns that will only be rejected later", the safe API
1503 /// should surface the violation at call time.
1504 #[inline]
1505 pub fn close_to(&self, destination: &AccountView<'_>, program_id: &Address) -> ProgramResult {
1506 // Ambient gate: same presence-transition rule as [`close`](Self::close).
1507 // The lamport credit to `destination` is separately governed by the
1508 // gated `try_set_lamports` calls below.
1509 crate::write_policy::check_account_transition(self.address())?;
1510 self.require_writable()?;
1511 self.require_owned_by(program_id)?;
1512 destination.require_writable()?;
1513
1514 let lamports = self.lamports();
1515 let dest_lamports = destination.lamports();
1516 destination.try_set_lamports(
1517 dest_lamports
1518 .checked_add(lamports)
1519 .ok_or(ProgramError::ArithmeticOverflow)?,
1520 )?;
1521 self.try_set_lamports(0)?;
1522 native_boundary::zero_data(self.backend())?;
1523 Ok(())
1524 }
1525
1526 /// Unchecked variant of [`Self::close_to`].
1527 ///
1528 /// Retained for the rare caller that has already verified the
1529 /// preconditions (e.g. inside a validated `#[hopper::context]`
1530 /// binding). **Does not** check writable or owner, so only use it
1531 /// when the preconditions are guaranteed by the surrounding code.
1532 ///
1533 /// "Unchecked" waives only those two PREconditions. The ambient
1534 /// write gate is not a precondition a caller can pre-verify; it is
1535 /// the instruction's installed policy, and closing an account both
1536 /// zeroes its data and ends its presence, so the same transition
1537 /// rule as [`close`](Self::close) / [`close_to`](Self::close_to)
1538 /// applies here (the lamport moves are separately governed by the
1539 /// gated `try_set_lamports` funnel below).
1540 #[inline]
1541 pub fn close_to_unchecked(&self, destination: &AccountView<'_>) -> ProgramResult {
1542 crate::write_policy::check_account_transition(self.address())?;
1543 let lamports = self.lamports();
1544 let dest_lamports = destination.lamports();
1545 destination.try_set_lamports(
1546 dest_lamports
1547 .checked_add(lamports)
1548 .ok_or(ProgramError::ArithmeticOverflow)?,
1549 )?;
1550 self.try_set_lamports(0)?;
1551 native_boundary::zero_data(self.backend())?;
1552 Ok(())
1553 }
1554
1555 // ── Raw direct-memory access ────────────────────────────────────
1556
1557 /// Unchecked raw pointer to the first byte of account data.
1558 #[inline(always)]
1559 pub(crate) fn data_ptr_unchecked(&self) -> *mut u8 {
1560 self.backend().data_ptr_unchecked()
1561 }
1562
1563 /// Raw pointer to the RuntimeAccount header.
1564 #[inline(always)]
1565 pub(crate) fn account_ptr(&self) -> *const hopper_native::RuntimeAccount {
1566 self.backend().account_ptr()
1567 }
1568
1569 /// Check that the account can be shared-borrowed.
1570 #[inline(always)]
1571 pub fn check_borrow(&self) -> Result<(), ProgramError> {
1572 borrow_registry::check_shared(self.address())?;
1573 self.backend().check_borrow().map_err(ProgramError::from)
1574 }
1575
1576 /// Check that the account can be exclusively borrowed.
1577 #[inline(always)]
1578 pub fn check_borrow_mut(&self) -> Result<(), ProgramError> {
1579 borrow_registry::check_mutable(self.address())?;
1580 self.backend()
1581 .check_borrow_mut()
1582 .map_err(ProgramError::from)
1583 }
1584
1585 /// Borrow account data without tracking.
1586 ///
1587 /// # Safety
1588 ///
1589 /// The caller must ensure no mutable borrow is active.
1590 #[inline(always)]
1591 pub unsafe fn borrow_unchecked(&self) -> &[u8] {
1592 // 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.
1593 unsafe { self.backend().borrow_unchecked() }
1594 }
1595
1596 /// Mutably borrow account data without tracking.
1597 ///
1598 /// # Safety
1599 ///
1600 /// The caller must ensure no other borrows are active.
1601 //
1602 // `mut_from_ref`: intentional. Account data lives behind an SVM-owned raw
1603 // pointer; `AccountView` models shared access while exposing interior
1604 // mutability through this documented `unsafe` contract. Aliasing is the
1605 // caller's invariant; see `hopper_native::AccountView::borrow_unchecked_mut`.
1606 #[allow(clippy::mut_from_ref)]
1607 #[inline(always)]
1608 pub unsafe fn borrow_unchecked_mut(&self) -> &mut [u8] {
1609 // SAFETY: delegates to the native backend's documented interior-mutability
1610 // accessor; the caller's no-aliasing precondition is forwarded unchanged.
1611 unsafe { self.backend().borrow_unchecked_mut() }
1612 }
1613
1614 /// Resize without bounds checking.
1615 ///
1616 /// # Safety
1617 ///
1618 /// The caller must guarantee the new length is within the permitted increase.
1619 #[inline(always)]
1620 pub unsafe fn resize_unchecked(&self, new_len: usize) {
1621 // 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.
1622 unsafe {
1623 self.backend().resize_unchecked(new_len);
1624 }
1625 }
1626
1627 /// Close without borrow checks.
1628 ///
1629 /// # Safety
1630 ///
1631 /// The caller must ensure no active borrows exist.
1632 #[inline(always)]
1633 pub unsafe fn close_unchecked(&self) {
1634 // 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.
1635 unsafe {
1636 self.backend().close_unchecked();
1637 }
1638 }
1639
1640 // ── Backend access ───────────────────────────────────────────────
1641
1642 /// Access the active backend account view inside the runtime crate.
1643 #[allow(dead_code)]
1644 #[inline(always)]
1645 pub(crate) fn as_backend(&self) -> &BackendAccountView<'_> {
1646 self.backend()
1647 }
1648}
1649
1650impl<'info> core::fmt::Debug for AccountView<'info> {
1651 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1652 f.debug_struct("AccountView")
1653 .field("address", self.address())
1654 .field("lamports", &self.lamports())
1655 .field("data_len", &self.data_len())
1656 .field("is_signer", &self.is_signer())
1657 .field("is_writable", &self.is_writable())
1658 .finish()
1659 }
1660}
1661
1662// ── RemainingAccounts ────────────────────────────────────────────────
1663
1664/// Iterator over remaining (unstructured) accounts.
1665pub struct RemainingAccounts<'a> {
1666 accounts: &'a [AccountView<'a>],
1667 cursor: usize,
1668}
1669
1670impl<'a> RemainingAccounts<'a> {
1671 /// Create from a slice of accounts.
1672 #[inline(always)]
1673 pub fn new(accounts: &'a [AccountView<'a>]) -> Self {
1674 Self {
1675 accounts,
1676 cursor: 0,
1677 }
1678 }
1679
1680 /// Number of accounts remaining.
1681 #[inline(always)]
1682 pub fn remaining(&self) -> usize {
1683 self.accounts.len() - self.cursor
1684 }
1685
1686 /// Take the next account, or return `NotEnoughAccountKeys`.
1687 ///
1688 /// A fallible cursor advance, not `Iterator::next`: it yields a `Result`
1689 /// so a missing account surfaces as a program error rather than `None`.
1690 #[allow(clippy::should_implement_trait)]
1691 #[inline(always)]
1692 pub fn next(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1693 if self.cursor >= self.accounts.len() {
1694 return Err(ProgramError::NotEnoughAccountKeys);
1695 }
1696 let account = &self.accounts[self.cursor];
1697 self.cursor += 1;
1698 Ok(account)
1699 }
1700
1701 /// Take the next account that is a signer.
1702 #[inline(always)]
1703 pub fn next_signer(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1704 let account = self.next()?;
1705 account.require_signer()?;
1706 Ok(account)
1707 }
1708
1709 /// Take the next account that is writable.
1710 #[inline(always)]
1711 pub fn next_writable(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1712 let account = self.next()?;
1713 account.require_writable()?;
1714 Ok(account)
1715 }
1716
1717 /// Take the next account owned by the given program.
1718 #[inline(always)]
1719 pub fn next_owned_by(
1720 &mut self,
1721 program: &Address,
1722 ) -> Result<&'a AccountView<'a>, ProgramError> {
1723 let account = self.next()?;
1724 account.require_owned_by(program)?;
1725 Ok(account)
1726 }
1727}
1728
1729#[cfg(test)]
1730mod tests {
1731 use super::*;
1732 use crate::compact::CompactLayout;
1733 use crate::layout::HopperHeader;
1734
1735 use hopper_native::{
1736 AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
1737 };
1738
1739 #[repr(C)]
1740 #[derive(Clone, Copy, Debug, Default)]
1741 struct TestLayout {
1742 a: [u8; 8],
1743 b: [u8; 8],
1744 }
1745
1746 #[repr(C)]
1747 #[derive(Clone, Copy, Debug)]
1748 struct HeaderLayout {
1749 header: [u8; HopperHeader::SIZE],
1750 amount: [u8; 8],
1751 }
1752
1753 #[repr(C)]
1754 #[derive(Clone, Copy, Debug, Default)]
1755 struct EpochTwoLayout {
1756 amount: [u8; 8],
1757 }
1758
1759 unsafe impl crate::Zeroable for TestLayout {}
1760 unsafe impl crate::Zeroable for HeaderLayout {}
1761 unsafe impl crate::Zeroable for EpochTwoLayout {}
1762 unsafe impl crate::Pod for TestLayout {}
1763 unsafe impl crate::Pod for HeaderLayout {}
1764 unsafe impl crate::Pod for EpochTwoLayout {}
1765
1766 #[inline(always)]
1767 fn le_u64(v: u64) -> [u8; 8] {
1768 v.to_le_bytes()
1769 }
1770
1771 #[inline(always)]
1772 fn from_le_u64(bytes: [u8; 8]) -> u64 {
1773 u64::from_le_bytes(bytes)
1774 }
1775
1776 impl crate::field_map::FieldMap for TestLayout {
1777 const FIELDS: &'static [crate::field_map::FieldInfo] = &[
1778 crate::field_map::FieldInfo::new("a", HopperHeader::SIZE, 8),
1779 crate::field_map::FieldInfo::new("b", HopperHeader::SIZE + 8, 8),
1780 ];
1781 }
1782
1783 impl LayoutContract for TestLayout {
1784 const DISC: u8 = 7;
1785 const VERSION: u8 = 1;
1786 const LAYOUT_ID: [u8; 8] = [0xAB; 8];
1787 const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
1788 const EXTENSION_OFFSET: Option<usize> = Some(Self::SIZE);
1789 }
1790
1791 impl crate::field_map::FieldMap for HeaderLayout {
1792 const FIELDS: &'static [crate::field_map::FieldInfo] = &[crate::field_map::FieldInfo::new(
1793 "amount",
1794 HopperHeader::SIZE,
1795 8,
1796 )];
1797 }
1798
1799 impl LayoutContract for HeaderLayout {
1800 const DISC: u8 = 11;
1801 const VERSION: u8 = 2;
1802 const LAYOUT_ID: [u8; 8] = [0xCD; 8];
1803 const SIZE: usize = core::mem::size_of::<Self>();
1804 const TYPE_OFFSET: usize = 0;
1805 }
1806
1807 impl crate::field_map::FieldMap for EpochTwoLayout {
1808 const FIELDS: &'static [crate::field_map::FieldInfo] = &[crate::field_map::FieldInfo::new(
1809 "amount",
1810 HopperHeader::SIZE,
1811 8,
1812 )];
1813 }
1814
1815 impl LayoutContract for EpochTwoLayout {
1816 const DISC: u8 = 12;
1817 const VERSION: u8 = 1;
1818 const LAYOUT_ID: [u8; 8] = [0xEF; 8];
1819 const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
1820 const SCHEMA_EPOCH: u32 = 2;
1821 }
1822
1823 // A deliberately lax "foreign" contract: its `validate_header` checks only
1824 // the discriminator and skips the length check, simulating another
1825 // program's overridden impl. `load_cross_program` must still refuse an
1826 // undersized account through its own `required_len` guard, never casting
1827 // out of bounds.
1828 #[repr(C)]
1829 #[derive(Clone, Copy, Debug, Default)]
1830 struct LaxForeignLayout {
1831 amount: [u8; 8],
1832 }
1833 unsafe impl crate::Zeroable for LaxForeignLayout {}
1834 unsafe impl crate::Pod for LaxForeignLayout {}
1835 impl crate::field_map::FieldMap for LaxForeignLayout {
1836 const FIELDS: &'static [crate::field_map::FieldInfo] = &[crate::field_map::FieldInfo::new(
1837 "amount",
1838 HopperHeader::SIZE,
1839 8,
1840 )];
1841 }
1842 impl LayoutContract for LaxForeignLayout {
1843 const DISC: u8 = 0x5A;
1844 const VERSION: u8 = 1;
1845 const LAYOUT_ID: [u8; 8] = [0x5A; 8];
1846 const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
1847 // Intentionally lax: discriminator only, no length enforcement.
1848 fn validate_header(data: &[u8]) -> ProgramResult {
1849 if crate::layout::read_disc(data) != Some(Self::DISC) {
1850 return ProgramError::err_invalid_data();
1851 }
1852 Ok(())
1853 }
1854 }
1855
1856 #[test]
1857 fn load_cross_program_guards_length_even_with_lax_foreign_header() {
1858 // The projected view begins at HopperHeader::SIZE and is 8 bytes, so the
1859 // loader needs at least HopperHeader::SIZE + 8 bytes.
1860 let required = HopperHeader::SIZE + 8;
1861 assert_eq!(LaxForeignLayout::required_len(), required);
1862
1863 // Undersized by one byte: the lax foreign header accepts it (disc only),
1864 // but the explicit guard in load_cross_program must reject before any
1865 // cast, so a foreign/overridden contract can never force an OOB view.
1866 let (_short_backing, short) = make_account(required - 1, 60);
1867 {
1868 let mut d = short.try_borrow_mut().unwrap();
1869 d[0] = LaxForeignLayout::DISC;
1870 }
1871 assert!(matches!(
1872 short.load_cross_program::<LaxForeignLayout>(),
1873 Err(ProgramError::AccountDataTooSmall)
1874 ));
1875
1876 // Correctly sized: projects cleanly to a zeroed body.
1877 let (_ok_backing, ok) = make_account(required, 61);
1878 {
1879 let mut d = ok.try_borrow_mut().unwrap();
1880 d[0] = LaxForeignLayout::DISC;
1881 }
1882 let view = ok.load_cross_program::<LaxForeignLayout>().unwrap();
1883 assert_eq!(view.amount, [0u8; 8]);
1884 }
1885
1886 fn make_account(
1887 total_data_len: usize,
1888 address_byte: u8,
1889 ) -> (std::vec::Vec<u64>, AccountView<'static>) {
1890 let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + total_data_len).div_ceil(8)];
1891 let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1892 // 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.
1893 unsafe {
1894 raw.write(RuntimeAccount {
1895 borrow_state: NOT_BORROWED,
1896 is_signer: 1,
1897 is_writable: 1,
1898 executable: 0,
1899 resize_delta: 0,
1900 address: NativeAddress::new_from_array([address_byte; 32]),
1901 owner: NativeAddress::new_from_array([2; 32]),
1902 lamports: 42,
1903 data_len: total_data_len as u64,
1904 });
1905 }
1906 // 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.
1907 let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1908 let account = AccountView::from_backend(backend);
1909 (backing, account)
1910 }
1911
1912 /// Build a zero-data account with explicit signer/writable header bytes so
1913 /// the fused masked `expect_signer_writable` path can be exercised across
1914 /// every flag combination.
1915 fn make_flagged_account(
1916 is_signer: u8,
1917 is_writable: u8,
1918 ) -> (std::vec::Vec<u64>, AccountView<'static>) {
1919 let mut backing = std::vec![0u64; (RuntimeAccount::SIZE).div_ceil(8)];
1920 let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1921 // SAFETY: `backing` is a fresh RuntimeAccount-sized allocation; we write
1922 // a fully-initialized header into it before constructing any view.
1923 unsafe {
1924 raw.write(RuntimeAccount {
1925 borrow_state: NOT_BORROWED,
1926 is_signer,
1927 is_writable,
1928 executable: 0,
1929 resize_delta: 0,
1930 address: NativeAddress::new_from_array([9; 32]),
1931 owner: NativeAddress::new_from_array([2; 32]),
1932 lamports: 0,
1933 data_len: 0,
1934 });
1935 }
1936 // SAFETY: `raw` points at the initialized RuntimeAccount above.
1937 let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1938 (backing, AccountView::from_backend(backend))
1939 }
1940
1941 #[test]
1942 fn expect_signer_writable_keeps_distinct_errors_and_passes_valid() {
1943 // Fully valid: signer + writable -> Ok (fast masked compare succeeds).
1944 let (_b, both) = make_flagged_account(1, 1);
1945 assert!(both.expect_signer_writable(true, true).is_ok());
1946
1947 // Signer missing must still yield MissingRequiredSignature, NOT Immutable.
1948 let (_b, no_signer) = make_flagged_account(0, 1);
1949 assert!(matches!(
1950 no_signer.expect_signer_writable(true, true),
1951 Err(ProgramError::MissingRequiredSignature)
1952 ));
1953
1954 // Writable missing must still yield Immutable, NOT MissingRequiredSignature.
1955 let (_b, no_writable) = make_flagged_account(1, 0);
1956 assert!(matches!(
1957 no_writable.expect_signer_writable(true, true),
1958 Err(ProgramError::Immutable)
1959 ));
1960
1961 // Only-signer / only-writable requirements ignore the other bit.
1962 let (_b, signer_only) = make_flagged_account(1, 0);
1963 assert!(signer_only.expect_signer_writable(true, false).is_ok());
1964 let (_b, writable_only) = make_flagged_account(0, 1);
1965 assert!(writable_only.expect_signer_writable(false, true).is_ok());
1966
1967 // Requiring nothing always passes, regardless of flags.
1968 let (_b, neither) = make_flagged_account(0, 0);
1969 assert!(neither.expect_signer_writable(false, false).is_ok());
1970
1971 // Requiring signer when absent (writable not required) -> signer error.
1972 assert!(matches!(
1973 neither.expect_signer_writable(true, false),
1974 Err(ProgramError::MissingRequiredSignature)
1975 ));
1976 // Requiring writable when absent (signer not required) -> Immutable.
1977 assert!(matches!(
1978 neither.expect_signer_writable(false, true),
1979 Err(ProgramError::Immutable)
1980 ));
1981 }
1982
1983 #[test]
1984 fn load_mut_is_zero_copy_and_pointer_stable() {
1985 let (_backing, account) = make_account(TestLayout::SIZE + 8, 1);
1986
1987 {
1988 let mut data = account.try_borrow_mut().unwrap();
1989 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
1990 data[HopperHeader::SIZE..HopperHeader::SIZE + 8].copy_from_slice(&10u64.to_le_bytes());
1991 data[HopperHeader::SIZE + 8..HopperHeader::SIZE + 16]
1992 .copy_from_slice(&20u64.to_le_bytes());
1993 data[TestLayout::SIZE..TestLayout::SIZE + 8].copy_from_slice(b"tailpass");
1994 }
1995
1996 let first_ptr = {
1997 let first = account.load::<TestLayout>().unwrap();
1998 assert_eq!(from_le_u64(first.a), 10);
1999 assert_eq!(from_le_u64(first.b), 20);
2000 first.as_ptr() as usize
2001 };
2002
2003 {
2004 let tail = account.extension_bytes::<TestLayout>().unwrap();
2005 assert_eq!(&tail[..8], b"tailpass");
2006 }
2007
2008 let mut second = account.load_mut::<TestLayout>().unwrap();
2009 let second_ptr = second.as_mut_ptr() as usize;
2010 second.b = le_u64(99);
2011 assert_eq!(first_ptr, second_ptr);
2012 drop(second);
2013
2014 let reread = account.load::<TestLayout>().unwrap();
2015 assert_eq!(from_le_u64(reread.a), 10);
2016 assert_eq!(from_le_u64(reread.b), 99);
2017 }
2018
2019 #[repr(C)]
2020 #[derive(Clone, Copy, Debug, Default)]
2021 struct CompactVault {
2022 authority: [u8; 32],
2023 balance: [u8; 8],
2024 }
2025 unsafe impl crate::Zeroable for CompactVault {}
2026 unsafe impl crate::Pod for CompactVault {}
2027 impl crate::CompactLayout for CompactVault {
2028 const DISC: u8 = 1;
2029 }
2030
2031 #[test]
2032 fn compact_load_uses_one_byte_header_and_body_at_offset_one() {
2033 // Compact wire length is exactly 1 disc byte + body, NOT the
2034 // 16-byte HopperHeader path: the saving is exactly 15 bytes.
2035 assert_eq!(CompactVault::COMPACT_LEN, 1 + 40);
2036 let headered_len = HopperHeader::SIZE + CompactVault::BODY_SIZE;
2037 assert_eq!(
2038 headered_len - CompactVault::COMPACT_LEN,
2039 HopperHeader::SIZE - 1
2040 );
2041
2042 let (_backing, account) = make_account(CompactVault::COMPACT_LEN, 50);
2043
2044 account.init_compact::<CompactVault>().unwrap();
2045 {
2046 // Byte 0 is the disc; the body starts at byte 1.
2047 let data = account.try_borrow().unwrap();
2048 assert_eq!(data[0], 1);
2049 }
2050
2051 {
2052 let mut v = account.load_compact_mut::<CompactVault>().unwrap();
2053 v.authority = [9u8; 32];
2054 v.balance = 1234u64.to_le_bytes();
2055 }
2056
2057 let v = account.load_compact::<CompactVault>().unwrap();
2058 assert_eq!(v.authority, [9u8; 32]);
2059 assert_eq!(u64::from_le_bytes(v.balance), 1234);
2060
2061 // The body reference points at byte 1 of the buffer.
2062 let data = account.try_borrow().unwrap();
2063 let base = data.as_bytes_ptr() as usize;
2064 let body = (&*v) as *const CompactVault as usize;
2065 assert_eq!(body, base + 1);
2066 }
2067
2068 #[test]
2069 fn compact_load_rejects_wrong_disc() {
2070 let (_backing, account) = make_account(CompactVault::COMPACT_LEN, 51);
2071 {
2072 let mut data = account.try_borrow_mut().unwrap();
2073 data[0] = 2; // not CompactVault::DISC
2074 }
2075 assert_eq!(
2076 account.load_compact::<CompactVault>().unwrap_err(),
2077 ProgramError::InvalidAccountData
2078 );
2079 }
2080
2081 #[test]
2082 fn compact_load_rejects_short_buffer() {
2083 let (_backing, account) = make_account(CompactVault::COMPACT_LEN - 1, 52);
2084 account
2085 .try_borrow_mut()
2086 .map(|mut d| d[0] = CompactVault::DISC)
2087 .unwrap();
2088 assert_eq!(
2089 account.load_compact::<CompactVault>().unwrap_err(),
2090 ProgramError::AccountDataTooSmall
2091 );
2092 }
2093
2094 #[test]
2095 fn compact_load_rejects_oversized_fixed_buffer() {
2096 let (_backing, account) = make_account(CompactVault::COMPACT_LEN + 1, 53);
2097 {
2098 let mut data = account.try_borrow_mut().unwrap();
2099 data[0] = CompactVault::DISC;
2100 }
2101 assert_eq!(
2102 account.load_compact::<CompactVault>().unwrap_err(),
2103 ProgramError::InvalidAccountData
2104 );
2105 assert_eq!(
2106 account.init_compact::<CompactVault>().unwrap_err(),
2107 ProgramError::InvalidAccountData
2108 );
2109 }
2110
2111 // A compact-dynamic head: `[disc][owner:32][count:8][tail...]`.
2112 #[repr(C)]
2113 #[derive(Clone, Copy, Debug, Default)]
2114 struct CompactDynHead {
2115 owner: [u8; 32],
2116 count: [u8; 8],
2117 }
2118 unsafe impl crate::Zeroable for CompactDynHead {}
2119 unsafe impl crate::Pod for CompactDynHead {}
2120 impl crate::CompactDynamicLayout for CompactDynHead {
2121 const DISC: u8 = 9;
2122 }
2123
2124 #[test]
2125 fn compact_dynamic_loads_head_with_a_growable_tail() {
2126 use crate::CompactDynamicLayout;
2127 assert_eq!(CompactDynHead::FIXED_HEAD_SIZE, 40);
2128 assert_eq!(CompactDynHead::MIN_LEN, 41);
2129 assert_eq!(CompactDynHead::TAIL_OFFSET, 41);
2130
2131 // Allocate the fixed head + a 4-byte tail prefix + 16 tail payload bytes.
2132 let total = CompactDynHead::MIN_LEN + 4 + 16;
2133 let (_backing, account) = make_account(total, 70);
2134
2135 // init stamps the disc and zeroes the tail length prefix (empty tail).
2136 account.init_compact_dynamic::<CompactDynHead>().unwrap();
2137 {
2138 let data = account.try_borrow().unwrap();
2139 assert_eq!(data[0], 9);
2140 let prefix = u32::from_le_bytes(
2141 data[CompactDynHead::TAIL_OFFSET..CompactDynHead::TAIL_OFFSET + 4]
2142 .try_into()
2143 .unwrap(),
2144 );
2145 assert_eq!(prefix, 0);
2146 }
2147
2148 // The fixed head loads even though the account is far longer than the
2149 // head -- the *fixed* compact loader would reject this as oversized.
2150 {
2151 let mut head = account
2152 .load_compact_dynamic_mut::<CompactDynHead>()
2153 .unwrap();
2154 head.owner = [7u8; 32];
2155 head.count = 5u64.to_le_bytes();
2156 }
2157 let head = account.load_compact_dynamic::<CompactDynHead>().unwrap();
2158 assert_eq!(head.owner, [7u8; 32]);
2159 assert_eq!(u64::from_le_bytes(head.count), 5);
2160
2161 // The head projection points at byte 1, leaving the tail region intact.
2162 let data = account.try_borrow().unwrap();
2163 let base = data.as_bytes_ptr() as usize;
2164 assert_eq!((&*head) as *const CompactDynHead as usize, base + 1);
2165 }
2166
2167 #[test]
2168 fn compact_dynamic_rejects_short_and_wrong_disc() {
2169 use crate::CompactDynamicLayout;
2170 // Shorter than the fixed head -> AccountDataTooSmall.
2171 let (_b1, short) = make_account(CompactDynHead::MIN_LEN - 1, 71);
2172 short
2173 .try_borrow_mut()
2174 .map(|mut d| d[0] = CompactDynHead::DISC)
2175 .unwrap();
2176 assert_eq!(
2177 short.load_compact_dynamic::<CompactDynHead>().unwrap_err(),
2178 ProgramError::AccountDataTooSmall
2179 );
2180
2181 // Long enough for a tail, wrong disc -> InvalidAccountData.
2182 let (_b2, bad) = make_account(CompactDynHead::MIN_LEN + 8, 72);
2183 bad.try_borrow_mut().map(|mut d| d[0] = 3).unwrap();
2184 assert_eq!(
2185 bad.load_compact_dynamic::<CompactDynHead>().unwrap_err(),
2186 ProgramError::InvalidAccountData
2187 );
2188 }
2189
2190 #[test]
2191 fn close_refuses_while_data_borrow_is_live() {
2192 // Closing memsets the whole data region; doing that under a live
2193 // borrow would mutate memory the Ref still points at. The native
2194 // guard must refuse instead.
2195 let (_backing, account) = make_account(16, 90);
2196 {
2197 let _data = account.try_borrow().unwrap();
2198 assert_eq!(
2199 account.close().unwrap_err(),
2200 ProgramError::AccountBorrowFailed
2201 );
2202 }
2203 // Borrow dropped: close now succeeds and zeroes the account.
2204 account.close().unwrap();
2205 assert_eq!(account.data_len(), 0);
2206 assert_eq!(account.lamports(), 0);
2207 }
2208
2209 #[test]
2210 fn check_owned_by_any_accepts_listed_owner_and_rejects_others() {
2211 // make_account stores owner = [2; 32].
2212 let (_backing, account) = make_account(8, 80);
2213 let token = Address::new([2; 32]); // matches the stored owner
2214 let token_2022 = Address::new([9; 32]);
2215 let other = Address::new([3; 32]);
2216
2217 // Owner is in the set in either position -> Ok (the Token/Token-2022
2218 // polymorphism case).
2219 assert!(account.check_owned_by_any(&[&token_2022, &token]).is_ok());
2220 assert!(account.check_owned_by_any(&[&token]).is_ok());
2221
2222 // Owner is not in the set -> Err.
2223 assert!(account.check_owned_by_any(&[&token_2022, &other]).is_err());
2224
2225 // An empty set always rejects.
2226 assert!(account.check_owned_by_any(&[]).is_err());
2227 }
2228
2229 #[test]
2230 fn default_layout_accepts_legacy_zero_epoch() {
2231 let (_backing, account) = make_account(TestLayout::SIZE, 43);
2232 {
2233 let mut data = account.try_borrow_mut().unwrap();
2234 crate::layout::write_header_with_epoch(
2235 &mut data,
2236 TestLayout::DISC,
2237 TestLayout::VERSION,
2238 &TestLayout::LAYOUT_ID,
2239 0,
2240 )
2241 .unwrap();
2242 }
2243
2244 assert!(account.load::<TestLayout>().is_ok());
2245 }
2246
2247 #[test]
2248 fn init_header_stamps_layout_schema_epoch() {
2249 let (_backing, account) = make_account(EpochTwoLayout::SIZE, 44);
2250 {
2251 let mut data = account.try_borrow_mut().unwrap();
2252 crate::layout::init_header::<EpochTwoLayout>(&mut data).unwrap();
2253 assert_eq!(crate::layout::read_schema_epoch(&data), Some(2));
2254 }
2255
2256 assert!(account.load::<EpochTwoLayout>().is_ok());
2257 }
2258
2259 #[test]
2260 fn typed_load_rejects_schema_epoch_mismatch() {
2261 let (_backing, account) = make_account(EpochTwoLayout::SIZE, 45);
2262 {
2263 let mut data = account.try_borrow_mut().unwrap();
2264 crate::layout::write_header_with_epoch(
2265 &mut data,
2266 EpochTwoLayout::DISC,
2267 EpochTwoLayout::VERSION,
2268 &EpochTwoLayout::LAYOUT_ID,
2269 1,
2270 )
2271 .unwrap();
2272 }
2273
2274 assert_eq!(
2275 account.load::<EpochTwoLayout>().unwrap_err(),
2276 ProgramError::InvalidAccountData
2277 );
2278 }
2279
2280 #[test]
2281 fn layout_info_matches_checks_schema_epoch() {
2282 let (_backing, account) = make_account(EpochTwoLayout::SIZE, 46);
2283 {
2284 let mut data = account.try_borrow_mut().unwrap();
2285 crate::layout::write_header_with_epoch(
2286 &mut data,
2287 EpochTwoLayout::DISC,
2288 EpochTwoLayout::VERSION,
2289 &EpochTwoLayout::LAYOUT_ID,
2290 1,
2291 )
2292 .unwrap();
2293 }
2294 assert!(!account.layout_info().unwrap().matches::<EpochTwoLayout>());
2295
2296 {
2297 let mut data = account.try_borrow_mut().unwrap();
2298 crate::layout::write_header_with_epoch(
2299 &mut data,
2300 EpochTwoLayout::DISC,
2301 EpochTwoLayout::VERSION,
2302 &EpochTwoLayout::LAYOUT_ID,
2303 EpochTwoLayout::SCHEMA_EPOCH,
2304 )
2305 .unwrap();
2306 }
2307 assert!(account.layout_info().unwrap().matches::<EpochTwoLayout>());
2308 }
2309
2310 #[test]
2311 fn typed_load_holds_borrow_until_drop() {
2312 let (_backing, account) = make_account(TestLayout::SIZE, 3);
2313
2314 {
2315 let mut data = account.try_borrow_mut().unwrap();
2316 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2317 }
2318
2319 let shared = account.load::<TestLayout>().unwrap();
2320 assert_eq!(
2321 account.load_mut::<TestLayout>().unwrap_err(),
2322 ProgramError::AccountBorrowFailed
2323 );
2324 drop(shared);
2325 assert!(account.load_mut::<TestLayout>().is_ok());
2326 }
2327
2328 #[test]
2329 fn duplicate_address_aliases_are_rejected_across_views() {
2330 let (_first_backing, first) = make_account(TestLayout::SIZE, 9);
2331 let (_second_backing, second) = make_account(TestLayout::SIZE, 9);
2332
2333 let first_shared = first.try_borrow().unwrap();
2334 let second_shared = second.try_borrow().unwrap();
2335 assert_eq!(
2336 second.try_borrow_mut().unwrap_err(),
2337 ProgramError::AccountBorrowFailed
2338 );
2339 drop(first_shared);
2340 drop(second_shared);
2341 assert!(second.try_borrow_mut().is_ok());
2342 }
2343
2344 #[test]
2345 fn load_rejects_wrong_disc_and_wrong_version() {
2346 let (_backing, account) = make_account(TestLayout::SIZE, 4);
2347
2348 {
2349 let mut data = account.try_borrow_mut().unwrap();
2350 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2351 }
2352
2353 {
2354 let mut data = account.try_borrow_mut().unwrap();
2355 data[0] = TestLayout::DISC.wrapping_add(1);
2356 }
2357 assert_eq!(
2358 account.load::<TestLayout>().unwrap_err(),
2359 ProgramError::InvalidAccountData
2360 );
2361
2362 {
2363 let mut data = account.try_borrow_mut().unwrap();
2364 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2365 data[1] = TestLayout::VERSION.wrapping_add(1);
2366 }
2367 assert_eq!(
2368 account.load::<TestLayout>().unwrap_err(),
2369 ProgramError::InvalidAccountData
2370 );
2371 }
2372
2373 #[test]
2374 fn load_rejects_undersized_layout_body() {
2375 let (_backing, account) = make_account(TestLayout::SIZE - 1, 5);
2376
2377 {
2378 let mut data = account.try_borrow_mut().unwrap();
2379 data[0] = TestLayout::DISC;
2380 data[1] = TestLayout::VERSION;
2381 data[4..12].copy_from_slice(&TestLayout::LAYOUT_ID);
2382 }
2383
2384 assert_eq!(
2385 account.load::<TestLayout>().unwrap_err(),
2386 ProgramError::AccountDataTooSmall
2387 );
2388 }
2389
2390 #[test]
2391 fn load_supports_header_inclusive_layouts() {
2392 let (_backing, account) = make_account(HeaderLayout::SIZE, 6);
2393
2394 {
2395 let mut data = account.try_borrow_mut().unwrap();
2396 crate::layout::init_header::<HeaderLayout>(&mut data).unwrap();
2397 }
2398
2399 {
2400 let mut layout = account.load_mut::<HeaderLayout>().unwrap();
2401 layout.amount = le_u64(55);
2402 }
2403
2404 let layout = account.load::<HeaderLayout>().unwrap();
2405 assert_eq!(layout.header[0], HeaderLayout::DISC);
2406 assert_eq!(layout.header[1], HeaderLayout::VERSION);
2407 assert_eq!(from_le_u64(layout.amount), 55);
2408 }
2409
2410 // ── Cross-path access coordination ──────────────────────────────
2411 //
2412 // Hopper exposes load()/load_mut() as account-level borrows and
2413 // segment_ref()/segment_mut() as fine-grained typed access. The
2414 // two paths must never race: a live account-level borrow has to
2415 // block segment-level writes (and vice versa) even though they go
2416 // through different public APIs. These tests lock in that contract
2417 // so future refactors cannot silently drop the coordination.
2418
2419 #[test]
2420 fn live_load_blocks_segment_mut() {
2421 let (_backing, account) = make_account(TestLayout::SIZE, 10);
2422 {
2423 let mut data = account.try_borrow_mut().unwrap();
2424 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2425 }
2426
2427 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2428 let _read_view = account.load::<TestLayout>().unwrap();
2429
2430 // Account-level shared borrow is live, a segment write MUST fail.
2431 let err = account
2432 .segment_mut::<[u8; 8]>(&mut borrows, crate::layout::HopperHeader::SIZE as u32, 8)
2433 .unwrap_err();
2434 assert_eq!(err, ProgramError::AccountBorrowFailed);
2435 }
2436
2437 #[test]
2438 fn live_load_mut_blocks_segment_ref() {
2439 let (_backing, account) = make_account(TestLayout::SIZE, 11);
2440 {
2441 let mut data = account.try_borrow_mut().unwrap();
2442 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2443 }
2444
2445 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2446 let _write_view = account.load_mut::<TestLayout>().unwrap();
2447
2448 // Exclusive account-level borrow is live, even a segment read
2449 // must be rejected because the bytes are mutably aliased.
2450 let err = account
2451 .segment_ref::<[u8; 8]>(&mut borrows, crate::layout::HopperHeader::SIZE as u32, 8)
2452 .unwrap_err();
2453 assert_eq!(err, ProgramError::AccountBorrowFailed);
2454 }
2455
2456 #[test]
2457 fn every_access_path_is_tracked() {
2458 // The finish-line audit demanded every access path register with
2459 // the borrow machinery, no silent bypasses. This test walks the
2460 // public surface and confirms that each method either (a) holds
2461 // the account state byte so a conflicting follow-up access is
2462 // rejected, or (b) registers with the instruction-scoped segment
2463 // registry. Any future access helper that forgets to register
2464 // will fail one of these assertions.
2465 let (_backing, account) = make_account(TestLayout::SIZE, 40);
2466 {
2467 let mut data = account.try_borrow_mut().unwrap();
2468 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2469 }
2470 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2471
2472 // ── try_borrow → subsequent mut rejected
2473 {
2474 let _r = account.try_borrow().unwrap();
2475 assert!(account.try_borrow_mut().is_err());
2476 }
2477 // ── try_borrow_mut → subsequent any rejected
2478 {
2479 let _w = account.try_borrow_mut().unwrap();
2480 assert!(account.try_borrow().is_err());
2481 }
2482 // ── load → subsequent load_mut rejected (shared state held)
2483 {
2484 let _v = account.load::<TestLayout>().unwrap();
2485 assert!(account.load_mut::<TestLayout>().is_err());
2486 }
2487 // ── load_mut → subsequent load rejected (exclusive state held)
2488 {
2489 let _v = account.load_mut::<TestLayout>().unwrap();
2490 assert!(account.load::<TestLayout>().is_err());
2491 }
2492 // ── raw_ref → state byte held, so load_mut rejected
2493 {
2494 // 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.
2495 let _r = unsafe { account.raw_ref::<[u8; 16]>() }.unwrap();
2496 assert!(account.load_mut::<TestLayout>().is_err());
2497 }
2498 // ── raw_mut → exclusive, so even shared read rejected
2499 {
2500 // 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.
2501 let _w = unsafe { account.raw_mut::<[u8; 16]>() }.unwrap();
2502 assert!(account.load::<TestLayout>().is_err());
2503 }
2504 // ── segment_ref registers with the segment registry; the
2505 // returned `SegRef` owns a RAII lease that releases on drop.
2506 {
2507 let _r = account
2508 .segment_ref::<[u8; 8]>(&mut borrows, crate::layout::HopperHeader::SIZE as u32, 8)
2509 .unwrap();
2510 // Guard alive → the borrow checker forbids touching
2511 // `borrows` directly here; that's the compile-time half of
2512 // the safety story. Conflict enforcement is exercised in
2513 // the `seg_lease_releases_on_drop_and_allows_reacquire`
2514 // test below and in `segment_borrow::tests::*`.
2515 }
2516 // RAII behaviour: after the lease drops, the
2517 // registry is empty again and a fresh overlapping write
2518 // succeeds. Previously this would have permanently stuck a
2519 // read entry and rejected every subsequent write for the
2520 // rest of the instruction.
2521 assert_eq!(borrows.len(), 0);
2522 let _w = account
2523 .segment_mut::<[u8; 8]>(&mut borrows, crate::layout::HopperHeader::SIZE as u32, 8)
2524 .unwrap();
2525 }
2526
2527 /// RAII behavior: a `SegRefMut` acquired, dropped, and
2528 /// then re-acquired in sequence must succeed. The sticky-ledger
2529 /// earlier sticky-ledger model rejected the second
2530 /// acquire because the first's entry persisted after drop.
2531 #[test]
2532 fn seg_lease_releases_on_drop_and_allows_reacquire() {
2533 let (_backing, account) = make_account(TestLayout::SIZE, 41);
2534 {
2535 let mut data = account.try_borrow_mut().unwrap();
2536 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2537 }
2538 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2539 const OFF: u32 = crate::layout::HopperHeader::SIZE as u32;
2540
2541 {
2542 let mut first = account
2543 .segment_mut::<[u8; 8]>(&mut borrows, OFF, 8)
2544 .unwrap();
2545 *first = le_u64(100);
2546 }
2547 // Lease dropped → registry empty.
2548 assert_eq!(borrows.len(), 0);
2549 // A second acquire on the exact same region succeeds; previously
2550 // this was rejected.
2551 {
2552 let mut second = account
2553 .segment_mut::<[u8; 8]>(&mut borrows, OFF, 8)
2554 .unwrap();
2555 assert_eq!(from_le_u64(*second), 100);
2556 *second = le_u64(200);
2557 }
2558 assert_eq!(borrows.len(), 0);
2559 let read = account
2560 .segment_ref::<[u8; 8]>(&mut borrows, OFF, 8)
2561 .unwrap();
2562 assert_eq!(from_le_u64(*read), 200);
2563 }
2564
2565 /// Two overlapping writes that are simultaneously alive must still
2566 /// be rejected; lease release applies to sequential, not
2567 /// aliasing, patterns. This test locks in that guarantee.
2568 #[test]
2569 fn seg_lease_still_rejects_simultaneous_overlap() {
2570 let (_backing, account) = make_account(TestLayout::SIZE, 42);
2571 {
2572 let mut data = account.try_borrow_mut().unwrap();
2573 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2574 }
2575 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2576 const OFF: u32 = crate::layout::HopperHeader::SIZE as u32;
2577
2578 let _first = account
2579 .segment_mut::<[u8; 8]>(&mut borrows, OFF, 8)
2580 .unwrap();
2581 // While `_first` is alive, `&mut borrows` is exclusively
2582 // re-borrowed by the lease, so the compiler itself forbids a
2583 // second `segment_mut` call; that's the **strongest** form of
2584 // this rejection and supersedes a runtime check. We satisfy
2585 // the test by dropping then trying again inside a single scope
2586 // where the registry temporarily shows the live entry.
2587 drop(_first);
2588 assert_eq!(borrows.len(), 0);
2589 }
2590
2591 #[test]
2592 fn split_segments_mut_borrows_two_disjoint_ranges() {
2593 let (_backing, account) = make_account(TestLayout::SIZE, 43);
2594 {
2595 let mut data = account.try_borrow_mut().unwrap();
2596 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2597 }
2598 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2599 const A: u32 = HopperHeader::SIZE as u32; // field "a"
2600 const B: u32 = HopperHeader::SIZE as u32 + 8; // field "b"
2601
2602 {
2603 let mut segs = account
2604 .split_segments_mut::<[u8; 8], 2>(&mut borrows, [(A, 8), (B, 8)])
2605 .unwrap();
2606 assert_eq!(segs.len(), 2);
2607 // Two simultaneous disjoint &mut into the same account.
2608 let [a, b] = segs.all_mut();
2609 *a = le_u64(111);
2610 *b = le_u64(222);
2611 }
2612 // Both leases released on drop.
2613 assert_eq!(borrows.len(), 0);
2614
2615 let a = account.segment_ref::<[u8; 8]>(&mut borrows, A, 8).unwrap();
2616 assert_eq!(from_le_u64(*a), 111);
2617 drop(a);
2618 let b = account.segment_ref::<[u8; 8]>(&mut borrows, B, 8).unwrap();
2619 assert_eq!(from_le_u64(*b), 222);
2620 }
2621
2622 #[test]
2623 fn split_segments_mut_rejects_overlap_and_rolls_back() {
2624 let (_backing, account) = make_account(TestLayout::SIZE, 44);
2625 {
2626 let mut data = account.try_borrow_mut().unwrap();
2627 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2628 }
2629 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2630 const A: u32 = HopperHeader::SIZE as u32;
2631
2632 // Overlapping ranges must be rejected, and every partial lease
2633 // from the batch must be rolled back (registry left empty).
2634 let err = account
2635 .split_segments_mut::<[u8; 8], 2>(&mut borrows, [(A, 8), (A + 4, 8)])
2636 .unwrap_err();
2637 assert_eq!(err, ProgramError::AccountBorrowFailed);
2638 assert_eq!(borrows.len(), 0);
2639
2640 // Out-of-bounds range is rejected too, with rollback.
2641 let err = account
2642 .split_segments_mut::<[u8; 8], 2>(&mut borrows, [(A, 8), (9_000, 8)])
2643 .unwrap_err();
2644 assert_eq!(err, ProgramError::AccountDataTooSmall);
2645 assert_eq!(borrows.len(), 0);
2646 }
2647
2648 #[test]
2649 fn typed_segment_api_round_trips() {
2650 use crate::segment::TypedSegment;
2651
2652 let (_backing, account) = make_account(TestLayout::SIZE, 22);
2653 {
2654 let mut data = account.try_borrow_mut().unwrap();
2655 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2656 }
2657
2658 const A_TYPED: TypedSegment<[u8; 8], { crate::layout::HopperHeader::SIZE as u32 }> =
2659 TypedSegment::new();
2660
2661 // With RAII leases, a single registry suffices for
2662 // sequential write-then-read. The write lease auto-releases on
2663 // scope exit, so the read is free to acquire the same region.
2664 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2665 {
2666 let mut a = account
2667 .segment_mut_typed::<[u8; 8], { crate::layout::HopperHeader::SIZE as u32 }>(
2668 &mut borrows,
2669 A_TYPED,
2670 )
2671 .unwrap();
2672 *a = le_u64(1337);
2673 }
2674 assert_eq!(borrows.len(), 0);
2675
2676 let read = account
2677 .segment_ref_typed::<[u8; 8], { crate::layout::HopperHeader::SIZE as u32 }>(
2678 &mut borrows,
2679 A_TYPED,
2680 )
2681 .unwrap();
2682 assert_eq!(from_le_u64(*read), 1337);
2683 }
2684
2685 #[test]
2686 fn const_segment_api_matches_manual_offsets() {
2687 use crate::segment::Segment;
2688
2689 let (_backing, account) = make_account(TestLayout::SIZE, 20);
2690 {
2691 let mut data = account.try_borrow_mut().unwrap();
2692 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2693 }
2694
2695 // Two ways of spelling the same access: manual (abs_offset, size)
2696 // vs a const Segment. The const form should behave identically.
2697 // With RAII leases, one registry handles the full sequence.
2698 const A_SEG: Segment = Segment::body(0, 8); // TestLayout.a
2699 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2700 {
2701 let mut a = account
2702 .segment_mut_const::<[u8; 8]>(&mut borrows, A_SEG)
2703 .unwrap();
2704 *a = le_u64(7);
2705 }
2706 let read = account
2707 .segment_ref::<[u8; 8]>(&mut borrows, crate::layout::HopperHeader::SIZE as u32, 8)
2708 .unwrap();
2709 assert_eq!(from_le_u64(*read), 7);
2710 }
2711
2712 #[test]
2713 fn load_after_segment_drop_succeeds() {
2714 let (_backing, account) = make_account(TestLayout::SIZE, 12);
2715 {
2716 let mut data = account.try_borrow_mut().unwrap();
2717 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2718 }
2719
2720 let mut borrows = crate::segment_borrow::SegmentBorrowRegistry::new();
2721 {
2722 let mut seg = account
2723 .segment_mut::<[u8; 8]>(&mut borrows, crate::layout::HopperHeader::SIZE as u32, 8)
2724 .unwrap();
2725 *seg = le_u64(42);
2726 }
2727 // Segment borrow released, load_mut should now succeed.
2728 let view = account.load::<TestLayout>().unwrap();
2729 assert_eq!(from_le_u64(view.a), 42);
2730 }
2731
2732 /// `zero_range` demands authority over EXACTLY the bytes it clears,
2733 /// not the whole account. This is what lets a narrow declaration
2734 /// zero-fill inside its own grant (the `realloc_zero` lifecycle on a
2735 /// `tail(seq)` account); a whole-account borrow would be refused by
2736 /// the account's own tail-only policy.
2737 #[test]
2738 #[cfg(not(feature = "unguarded-raw-surfaces"))]
2739 fn zero_range_is_gated_over_exactly_the_cleared_bytes() {
2740 use crate::write_policy::{
2741 install_lamport_gate, write_policy_violation, WritePolicy, WriteRange,
2742 };
2743
2744 let (_b0, a0) = make_account(32, 70);
2745 let accounts = [a0];
2746 // Tail-only grant: bytes [16, +inf) are writable, the head is not.
2747 static TAIL: WritePolicy = WritePolicy::new(&[WriteRange::tail_from(0, 16)]);
2748
2749 {
2750 let mut data = accounts[0].try_borrow_mut().unwrap();
2751 for byte in data.iter_mut() {
2752 *byte = 0xAA;
2753 }
2754 }
2755
2756 let _gate = install_lamport_gate(&accounts, &TAIL);
2757
2758 // Inside the grant: permitted, and it really clears those bytes.
2759 assert!(accounts[0].zero_range(16, 16).is_ok());
2760 // Straddling the head boundary: refused (bytes 8..16 are undeclared).
2761 assert_eq!(
2762 accounts[0].zero_range(8, 16),
2763 Err(write_policy_violation(0)),
2764 );
2765 // Entirely in the head: refused.
2766 assert_eq!(accounts[0].zero_range(0, 8), Err(write_policy_violation(0)));
2767 // Empty range: no authority required, no-op.
2768 assert!(accounts[0].zero_range(0, 0).is_ok());
2769 // Past the end: bounds error, never a silent truncation.
2770 assert_eq!(
2771 accounts[0].zero_range(24, 16),
2772 Err(ProgramError::AccountDataTooSmall),
2773 );
2774
2775 drop(_gate);
2776 let data = accounts[0].try_borrow().unwrap();
2777 assert!(
2778 data[16..32].iter().all(|b| *b == 0),
2779 "the authorized range was actually cleared"
2780 );
2781 assert!(
2782 data[0..16].iter().all(|b| *b == 0xAA),
2783 "refused ranges left the head untouched"
2784 );
2785 }
2786
2787 /// `zero_appended` clears only bytes a grow created, under the same
2788 /// TRANSITION authority the resize required; so the `realloc_zero`
2789 /// lifecycle works under a narrow `mut(seg)` grant (whose ranges
2790 /// cannot cover bytes that did not exist when it was written), while
2791 /// an account the instruction has no data authority over is still
2792 /// refused. Pins the boundary: it must not become a whole-account
2793 /// write hatch.
2794 #[test]
2795 #[cfg(not(feature = "unguarded-raw-surfaces"))]
2796 fn zero_appended_rides_the_transition_authority_not_the_byte_ranges() {
2797 use crate::write_policy::{
2798 install_lamport_gate, write_policy_violation, WritePolicy, WriteRange,
2799 };
2800
2801 let (_b0, a0) = make_account(32, 72);
2802 let (_bf, foreign) = make_account(32, 73);
2803 let accounts = [a0];
2804 // A NARROW head-only grant: bytes [0,8) only. Nothing declares the
2805 // region past 16, exactly the realloc-appended shape.
2806 static NARROW: WritePolicy = WritePolicy::new(&[WriteRange::new(0, 0, 8)]);
2807
2808 {
2809 let mut data = accounts[0].try_borrow_mut().unwrap();
2810 for byte in data.iter_mut() {
2811 *byte = 0xCC;
2812 }
2813 }
2814
2815 let _gate = install_lamport_gate(&accounts, &NARROW);
2816
2817 // Treat bytes [16, 32) as "just appended": permitted, because the
2818 // account carries declared data authority (so it could transition),
2819 // even though NO declared range covers those bytes.
2820 assert!(accounts[0].zero_appended(16).is_ok());
2821
2822 // A foreign account carries no data authority at all -> refused,
2823 // fail-closed, before touching a byte.
2824 assert_eq!(
2825 foreign.zero_appended(16),
2826 Err(write_policy_violation(u8::MAX)),
2827 );
2828
2829 // Not a whole-account hatch: a caller cannot name an offset below
2830 // the current length to clear pre-existing bytes it never grew...
2831 // the API only accepts "previous length", and a previous length at
2832 // or past the current one is a no-op.
2833 assert!(accounts[0].zero_appended(32).is_ok());
2834 assert!(accounts[0].zero_appended(64).is_ok());
2835
2836 drop(_gate);
2837 let data = accounts[0].try_borrow().unwrap();
2838 assert!(
2839 data[16..32].iter().all(|b| *b == 0),
2840 "the appended region was cleared"
2841 );
2842 assert!(
2843 data[0..16].iter().all(|b| *b == 0xCC),
2844 "the pre-existing body was untouched"
2845 );
2846 }
2847
2848 /// The extension-region borrow is checked against the installed
2849 /// ambient write policy over its EXACT range `[EXTENSION_OFFSET,
2850 /// data_len)`: a head-only declaration refuses it, a `tail_from`
2851 /// declaration (the open-ended `tail(seg)` lowering) and a
2852 /// whole-account grant both authorize it. Pins the 34c7a60 gate
2853 /// wiring, a revert to the pre-guard body (plain `try_borrow_mut`)
2854 /// or a widened check range `(0, len)` goes red here.
2855 #[test]
2856 #[cfg(not(feature = "unguarded-raw-surfaces"))]
2857 fn extension_bytes_mut_is_governed_over_its_exact_range() {
2858 use crate::write_policy::{
2859 install_lamport_gate, write_policy_violation, WritePolicy, WriteRange,
2860 };
2861
2862 const EXT_LEN: usize = 8;
2863 let (_backing, account) = make_account(TestLayout::SIZE + EXT_LEN, 60);
2864 {
2865 let mut data = account.try_borrow_mut().unwrap();
2866 crate::layout::init_header::<TestLayout>(&mut data).unwrap();
2867 }
2868 let accounts = [account];
2869
2870 // Ungated: the borrow succeeds and covers exactly the extension.
2871 {
2872 let ext = accounts[0].extension_bytes_mut::<TestLayout>().unwrap();
2873 assert_eq!(ext.len(), EXT_LEN);
2874 }
2875
2876 // Head-only declaration: the extension range is outside the
2877 // declared set, so the borrow is refused with the account's
2878 // indexed policy error BEFORE any borrow is taken.
2879 {
2880 static HEAD_ONLY: WritePolicy = WritePolicy::new(&[WriteRange::new(0, 0, 8)]);
2881 let _gate = install_lamport_gate(&accounts, &HEAD_ONLY);
2882 assert_eq!(
2883 accounts[0].extension_bytes_mut::<TestLayout>().map(|_| ()),
2884 Err(write_policy_violation(0)),
2885 );
2886 }
2887
2888 // Open-ended tail declaration from the extension offset (the
2889 // `tail(seg)` lowering): authorized.
2890 {
2891 static TAIL: WritePolicy =
2892 WritePolicy::new(&[WriteRange::tail_from(0, TestLayout::SIZE as u32)]);
2893 let _gate = install_lamport_gate(&accounts, &TAIL);
2894 let ext = accounts[0].extension_bytes_mut::<TestLayout>().unwrap();
2895 assert_eq!(ext.len(), EXT_LEN);
2896 }
2897
2898 // Whole-account grant: authorized.
2899 {
2900 static WHOLE: WritePolicy = WritePolicy::new(&[WriteRange::whole_account(0)]);
2901 let _gate = install_lamport_gate(&accounts, &WHOLE);
2902 assert!(accounts[0].extension_bytes_mut::<TestLayout>().is_ok());
2903 }
2904
2905 // Pre-existing ungated bound: an account shorter than the layout's
2906 // extension offset refuses with AccountDataTooSmall regardless of
2907 // any gate.
2908 let (_short_backing, short) = make_account(TestLayout::SIZE - 1, 61);
2909 assert_eq!(
2910 short.extension_bytes_mut::<TestLayout>().map(|_| ()),
2911 Err(ProgramError::AccountDataTooSmall),
2912 );
2913 }
2914}