hopper_native/account_view.rs
1//! RuntimeAccount memory layout and AccountView zero-copy wrapper.
2//!
3//! `RuntimeAccount` maps 1:1 onto the BPF input buffer layout that the
4//! Solana runtime writes for each account. `AccountView` is a thin
5//! pointer to a `RuntimeAccount` in that buffer, providing safe accessors
6//! for address, owner, flags, lamports, and data.
7
8use core::marker::PhantomData;
9
10use crate::address::{address_eq, Address};
11use crate::borrow::{Ref, RefMut};
12use crate::error::ProgramError;
13use crate::raw_account::RuntimeAccount;
14use crate::{ProgramResult, MAX_PERMITTED_DATA_INCREASE, NOT_BORROWED};
15
16// ── AccountView ──────────────────────────────────────────────────────
17
18/// Zero-copy view over a Solana account in the BPF input buffer.
19///
20/// `AccountView` stores a raw pointer to the `RuntimeAccount` header.
21/// All accessor methods read directly from the input buffer with no copies.
22#[repr(C)]
23#[cfg_attr(feature = "copy", derive(Copy))]
24#[derive(Clone, PartialEq, Eq)]
25pub struct AccountView<'info> {
26 raw: *mut RuntimeAccount,
27 _marker: PhantomData<&'info RuntimeAccount>,
28}
29
30// SAFETY: On Solana execution is single-threaded. Host tools and fuzzers
31// should not rely on cross-thread sharing of raw account pointers.
32#[cfg(target_os = "solana")]
33unsafe impl<'info> Send for AccountView<'info> {}
34#[cfg(target_os = "solana")]
35unsafe impl<'info> Sync for AccountView<'info> {}
36
37impl<'info> AccountView<'info> {
38 /// Construct an AccountView from a raw pointer.
39 ///
40 /// # Safety
41 ///
42 /// `raw` must point to a valid `RuntimeAccount` in the BPF input buffer
43 /// (or a test allocation with the same layout), followed by at least
44 /// `(*raw).data_len` bytes of account data. Before any safe resize method
45 /// is called, the four-byte `resize_delta` ABI slot must contain the
46 /// little-endian data length from instruction entry. Hopper's entrypoint
47 /// parsers establish that baseline before returning a view; manual test or
48 /// harness allocations must populate it themselves.
49 #[inline(always)]
50 pub const unsafe fn new_unchecked(raw: *mut RuntimeAccount) -> Self {
51 Self {
52 raw,
53 _marker: PhantomData,
54 }
55 }
56
57 #[inline(always)]
58 pub(crate) const fn raw_ptr(&self) -> *mut RuntimeAccount {
59 self.raw
60 }
61
62 // ── Getters ──────────────────────────────────────────────────────
63
64 /// The account's public key.
65 #[inline(always)]
66 pub fn address(&self) -> &Address {
67 // SAFETY: raw always points to a valid RuntimeAccount.
68 unsafe { &(*self.raw).address }
69 }
70
71 /// The owning program's address.
72 ///
73 /// # Safety
74 ///
75 /// The returned reference is invalidated if the account is assigned
76 /// to a new owner or closed. The caller must ensure no concurrent
77 /// mutation occurs.
78 #[inline(always)]
79 pub unsafe fn owner(&self) -> &Address {
80 // SAFETY: raw is valid; caller promises no concurrent mutation.
81 unsafe { &(*self.raw).owner }
82 }
83
84 /// Whether this account signed the transaction.
85 #[inline(always)]
86 pub fn is_signer(&self) -> bool {
87 // SAFETY: raw is valid.
88 unsafe { (*self.raw).is_signer != 0 }
89 }
90
91 /// Whether this account is writable in the transaction.
92 #[inline(always)]
93 pub fn is_writable(&self) -> bool {
94 // 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.
95 unsafe { (*self.raw).is_writable != 0 }
96 }
97
98 /// Whether this account contains an executable program.
99 #[inline(always)]
100 pub fn executable(&self) -> bool {
101 // 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.
102 unsafe { (*self.raw).executable != 0 }
103 }
104
105 /// Current data length in bytes.
106 #[inline(always)]
107 pub fn data_len(&self) -> usize {
108 // 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.
109 unsafe { (*self.raw).data_len as usize }
110 }
111
112 /// Original data length captured by the entrypoint for this invocation.
113 ///
114 /// Solana reserves the four bytes at header offset 4 for this value. It
115 /// must remain unchanged across local resizes and CPI so every resize is
116 /// checked against one invocation-wide baseline.
117 #[inline(always)]
118 pub fn original_data_len(&self) -> usize {
119 // SAFETY: `raw` is valid. The entrypoint initializes this ABI padding
120 // slot from `data_len` before making the account view available.
121 u32::from_le(unsafe { (*self.raw).resize_delta }) as usize
122 }
123
124 /// Difference between the current and original data length.
125 #[inline(always)]
126 pub fn resize_delta(&self) -> i32 {
127 (self.data_len() as i64 - self.original_data_len() as i64) as i32
128 }
129
130 /// Capture the invocation-wide resize baseline in the ABI padding slot.
131 ///
132 /// # Safety
133 ///
134 /// This must run exactly while materializing a canonical account from the
135 /// loader input, before that account can be resized locally or through CPI.
136 #[inline(always)]
137 pub(crate) unsafe fn initialize_original_data_len(&self) {
138 // SAFETY: the caller guarantees `raw` is a canonical loader account
139 // header and initialization happens before the view escapes.
140 unsafe {
141 (*self.raw).resize_delta = ((*self.raw).data_len as u32).to_le();
142 }
143 }
144
145 /// Current lamport balance.
146 #[inline(always)]
147 pub fn lamports(&self) -> u64 {
148 // 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.
149 unsafe { (*self.raw).lamports }
150 }
151
152 /// Whether the account data is empty (data_len == 0).
153 #[inline(always)]
154 pub fn is_data_empty(&self) -> bool {
155 self.data_len() == 0
156 }
157
158 /// Set the lamport balance.
159 #[inline(always)]
160 pub fn set_lamports(&self, lamports: u64) {
161 // 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.
162 unsafe {
163 (*self.raw).lamports = lamports;
164 }
165 }
166
167 // ── Ownership ────────────────────────────────────────────────────
168
169 /// Check whether this account is owned by the given program.
170 #[inline(always)]
171 pub fn owned_by(&self, program: &Address) -> bool {
172 // SAFETY: owner field is valid for the lifetime of the input buffer.
173 unsafe { address_eq(&(*self.raw).owner, program) }
174 }
175
176 /// Assign a new owner.
177 ///
178 /// # Safety
179 ///
180 /// The caller must ensure the account is writable and that ownership
181 /// transfer is authorized by the current owner program.
182 #[inline(always)]
183 pub unsafe fn assign(&self, new_owner: &Address) {
184 // 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.
185 unsafe {
186 (*self.raw).owner = new_owner.clone();
187 }
188 }
189
190 // ── Borrow tracking ─────────────────────────────────────────────
191
192 /// Whether the account data is currently borrowed (shared or exclusive).
193 #[inline(always)]
194 pub fn is_borrowed(&self) -> bool {
195 // 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.
196 unsafe { (*self.raw).borrow_state != NOT_BORROWED }
197 }
198
199 /// Whether the account data is exclusively (mutably) borrowed.
200 #[inline(always)]
201 pub fn is_borrowed_mut(&self) -> bool {
202 // 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.
203 unsafe { (*self.raw).borrow_state == 0 }
204 }
205
206 /// Check that the account can be shared-borrowed.
207 #[inline(always)]
208 pub fn check_borrow(&self) -> Result<(), ProgramError> {
209 // 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.
210 let state = unsafe { (*self.raw).borrow_state };
211 if state == 0 {
212 // Exclusively borrowed -- cannot share.
213 Err(ProgramError::AccountBorrowFailed)
214 } else {
215 Ok(())
216 }
217 }
218
219 /// Check that the account can be exclusively borrowed.
220 #[inline(always)]
221 pub fn check_borrow_mut(&self) -> Result<(), ProgramError> {
222 // 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.
223 let state = unsafe { (*self.raw).borrow_state };
224 if state != NOT_BORROWED {
225 // Already borrowed (shared or exclusive).
226 Err(ProgramError::AccountBorrowFailed)
227 } else {
228 Ok(())
229 }
230 }
231
232 /// Acquire a shared data borrow, returning the borrow-state pointer for a
233 /// [`Ref`] guard to release on drop.
234 ///
235 /// Mirrors the `try_borrow` state transition (increment with the 254 cap
236 /// so the count can never wrap into a sentinel) without materializing the
237 /// data slice; used by projection/lens paths that form their own typed
238 /// reference into the data region.
239 #[inline(always)]
240 pub(crate) fn acquire_shared(&self) -> Result<*mut u8, ProgramError> {
241 self.check_borrow()?;
242 // SAFETY: `self.raw` is a valid `RuntimeAccount`; `borrow_state` is its
243 // first byte. Taking a `*mut u8` to it creates no aliasing reference.
244 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
245 // SAFETY: read/write of the borrow byte on the single-threaded SVM,
246 // after `check_borrow` confirmed a shared borrow is compatible.
247 let state = unsafe { *state_ptr };
248 let new_state = if state == NOT_BORROWED { 1 } else { state + 1 };
249 if new_state == 0 || new_state == NOT_BORROWED {
250 // See `try_borrow`: cap the shared count at 254.
251 return Err(ProgramError::AccountBorrowFailed);
252 }
253 // SAFETY: as above; the new count was just validated.
254 unsafe {
255 *state_ptr = new_state;
256 }
257 Ok(state_ptr)
258 }
259
260 // ── Unchecked data access ────────────────────────────────────────
261
262 /// Borrow account data without borrow tracking.
263 ///
264 /// # Safety
265 ///
266 /// The caller must ensure no mutable borrow is active.
267 #[inline(always)]
268 pub unsafe fn borrow_unchecked(&self) -> &[u8] {
269 let data_ptr = self.data_ptr_unchecked();
270 let len = self.data_len();
271 // 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.
272 unsafe { core::slice::from_raw_parts(data_ptr, len) }
273 }
274
275 /// Mutably borrow account data without borrow tracking.
276 ///
277 /// # Safety
278 ///
279 /// The caller must ensure no other borrows (shared or exclusive) are active.
280 //
281 // `mut_from_ref` fires because this returns `&mut [u8]` from `&self`. That
282 // is intentional: account data lives behind a raw pointer the SVM owns, so
283 // the `AccountView` only models shared access to that region while exposing
284 // interior mutability through the documented `unsafe` contract above
285 // (Pinocchio uses the same shape). Aliasing is the caller's invariant, not
286 // the borrow checker's, that is exactly what the `unsafe` marker conveys.
287 #[allow(clippy::mut_from_ref)]
288 #[inline(always)]
289 pub unsafe fn borrow_unchecked_mut(&self) -> &mut [u8] {
290 let data_ptr = self.data_ptr_unchecked();
291 let len = self.data_len();
292 // SAFETY: `data_ptr_unchecked()` and `data_len()` describe the exact
293 // SVM-owned data region for this account, and the caller guarantees no
294 // overlapping borrow is live for the returned lifetime.
295 unsafe { core::slice::from_raw_parts_mut(data_ptr, len) }
296 }
297
298 // ── Checked data access ──────────────────────────────────────────
299
300 /// Try to obtain a shared borrow of the account data.
301 ///
302 /// Returns `Err(AccountBorrowFailed)` if the data is exclusively borrowed.
303 #[inline(always)]
304 pub fn try_borrow(&self) -> Result<Ref<'_, [u8]>, ProgramError> {
305 self.check_borrow()?;
306 // SAFETY: `self.raw` is a valid `RuntimeAccount` for this account
307 // (entrypoint invariant); `borrow_state` is its first byte. Taking a
308 // `*mut u8` to it does not create an aliasing reference.
309 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
310 // SAFETY: `state_ptr` points at this account's borrow-state byte and is
311 // only read after `check_borrow()` confirmed the borrow is compatible.
312 let state = unsafe { *state_ptr };
313 let new_state = if state == NOT_BORROWED { 1 } else { state + 1 };
314 if new_state == 0 || new_state == NOT_BORROWED {
315 // `0` would alias the exclusive-borrow sentinel; `NOT_BORROWED`
316 // (0xFF) would silently reset tracking on the 255th concurrent
317 // shared borrow, after which a mutable borrow could be granted
318 // while shared refs are still live. Cap the count at 254.
319 return Err(ProgramError::AccountBorrowFailed);
320 }
321 // SAFETY: single-threaded SVM execution; we hold the only path that
322 // writes this byte and have just validated the new shared count.
323 unsafe {
324 *state_ptr = new_state;
325 }
326 // SAFETY: the shared count was incremented above, so no exclusive
327 // borrow is outstanding; the returned `Ref` decrements it on drop.
328 let data = unsafe { self.borrow_unchecked() };
329 Ok(Ref::new(data, state_ptr))
330 }
331
332 /// Try to obtain an exclusive (mutable) borrow of the account data.
333 ///
334 /// Returns `Err(AccountBorrowFailed)` if the data is already borrowed.
335 #[inline(always)]
336 pub fn try_borrow_mut(&self) -> Result<RefMut<'_, [u8]>, ProgramError> {
337 self.check_borrow_mut()?;
338 // SAFETY: `self.raw` is a valid `RuntimeAccount`; `borrow_state` is its
339 // first byte. The `*mut u8` does not create an aliasing reference.
340 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
341 // SAFETY: `check_borrow_mut()` confirmed the account was NOT_BORROWED,
342 // so writing the exclusive sentinel (0) cannot stomp a live borrow.
343 unsafe {
344 *state_ptr = 0;
345 } // Mark exclusive.
346 // SAFETY: state is now exclusive, so no other borrow is live; the
347 // returned `RefMut` restores NOT_BORROWED on drop.
348 let data = unsafe { self.borrow_unchecked_mut() };
349 Ok(RefMut::new(data, state_ptr))
350 }
351
352 // ── Typed segment and raw access ───────────────────────────────
353
354 /// Project a typed segment from account data with native borrow tracking.
355 #[inline(always)]
356 pub fn segment_ref<T: crate::pod::Pod>(
357 &self,
358 offset: u32,
359 size: u32,
360 ) -> Result<Ref<'_, T>, ProgramError> {
361 let expected_size = core::mem::size_of::<T>() as u32;
362 if size != expected_size {
363 return Err(ProgramError::InvalidArgument);
364 }
365
366 let end = offset
367 .checked_add(size)
368 .ok_or(ProgramError::ArithmeticOverflow)?;
369 if end as usize > self.data_len() {
370 return Err(ProgramError::AccountDataTooSmall);
371 }
372
373 self.check_borrow()?;
374 // 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.
375 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
376 let state = unsafe { *state_ptr };
377 let new_state = if state == NOT_BORROWED { 1 } else { state + 1 };
378 if new_state == 0 || new_state == NOT_BORROWED {
379 // See `try_borrow`: cap the shared count at 254 so it can never
380 // wrap into the NOT_BORROWED sentinel.
381 return Err(ProgramError::AccountBorrowFailed);
382 }
383 // 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.
384 unsafe {
385 *state_ptr = new_state;
386 }
387
388 // 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.
389 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *const T };
390 Ok(Ref::new(unsafe { &*ptr }, state_ptr))
391 }
392
393 /// Acquire a shared segment borrow without size/bounds validation.
394 ///
395 /// # Safety
396 ///
397 /// The caller must have already verified:
398 /// - `offset + size_of::<T>()` does not overflow
399 /// - `offset + size_of::<T>() <= data_len()`
400 /// - no exclusive borrow overlapping `[offset, offset + size_of::<T>())`
401 /// is live for the returned reference's lifetime (this method performs
402 /// no borrow tracking)
403 #[inline(always)]
404 pub unsafe fn segment_ref_unchecked<T: crate::pod::Pod>(
405 &self,
406 offset: u32,
407 ) -> Result<Ref<'_, T>, ProgramError> {
408 // 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.
409 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *const T };
410 Ok(Ref::new_external(unsafe { &*ptr }))
411 }
412
413 /// Project a mutable typed segment from account data with native borrow tracking.
414 #[inline(always)]
415 pub fn segment_mut<T: crate::pod::Pod>(
416 &self,
417 offset: u32,
418 size: u32,
419 ) -> Result<RefMut<'_, T>, ProgramError> {
420 self.require_writable()?;
421
422 let expected_size = core::mem::size_of::<T>() as u32;
423 if size != expected_size {
424 return Err(ProgramError::InvalidArgument);
425 }
426
427 let end = offset
428 .checked_add(size)
429 .ok_or(ProgramError::ArithmeticOverflow)?;
430 if end as usize > self.data_len() {
431 return Err(ProgramError::AccountDataTooSmall);
432 }
433
434 self.check_borrow_mut()?;
435 // 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.
436 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
437 unsafe {
438 *state_ptr = 0;
439 }
440
441 // 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.
442 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *mut T };
443 Ok(RefMut::new(unsafe { &mut *ptr }, state_ptr))
444 }
445
446 /// Acquire an exclusive segment borrow without size/bounds/writable validation.
447 ///
448 /// # Safety
449 ///
450 /// The caller must have already verified:
451 /// - The account is writable
452 /// - `offset + size_of::<T>()` does not overflow
453 /// - `offset + size_of::<T>() <= data_len()`
454 /// - no other borrow (shared or exclusive) overlapping
455 /// `[offset, offset + size_of::<T>())` is live for the returned
456 /// reference's lifetime (this method performs no borrow tracking)
457 #[inline(always)]
458 pub unsafe fn segment_mut_unchecked<T: crate::pod::Pod>(
459 &self,
460 offset: u32,
461 ) -> Result<RefMut<'_, T>, ProgramError> {
462 // 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.
463 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *mut T };
464 Ok(RefMut::new_external(unsafe { &mut *ptr }))
465 }
466
467 /// Explicit raw typed read of the account buffer.
468 #[inline(always)]
469 ///
470 /// # Safety
471 ///
472 /// Caller must uphold the invariants documented for this unsafe API before invoking it.
473 pub unsafe fn raw_ref<T: crate::pod::Pod>(&self) -> Result<Ref<'_, T>, ProgramError> {
474 self.segment_ref::<T>(0, core::mem::size_of::<T>() as u32)
475 }
476
477 /// Explicit raw typed write of the account buffer.
478 #[inline(always)]
479 ///
480 /// # Safety
481 ///
482 /// Caller must uphold the invariants documented for this unsafe API before invoking it.
483 pub unsafe fn raw_mut<T: crate::pod::Pod>(&self) -> Result<RefMut<'_, T>, ProgramError> {
484 self.segment_mut::<T>(0, core::mem::size_of::<T>() as u32)
485 }
486
487 // ── Resize ───────────────────────────────────────────────────────
488
489 /// Check every precondition of [`resize`](Self::resize) without changing
490 /// the account: the account must be writable, no data borrow may be live,
491 /// and `new_len` may exceed the entry-time length by at most
492 /// [`MAX_PERMITTED_DATA_INCREASE`]. A no-op resize to the current length
493 /// always passes.
494 ///
495 /// Callers that move lamports before resizing (rent top-ups) run this
496 /// first so a refused resize cannot leave the transfer behind.
497 #[inline(always)]
498 pub fn check_resize(&self, new_len: usize) -> Result<(), ProgramError> {
499 if new_len == self.data_len() {
500 return Ok(());
501 }
502 self.require_writable()?;
503 self.check_borrow_mut()?;
504 if new_len.saturating_sub(self.original_data_len()) > MAX_PERMITTED_DATA_INCREASE {
505 return Err(ProgramError::InvalidRealloc);
506 }
507 Ok(())
508 }
509
510 /// Resize the account data to `new_len` bytes, zeroing any newly
511 /// exposed region.
512 ///
513 /// Returns `Err(InvalidRealloc)` if the new length exceeds the
514 /// permitted increase from the original allocation.
515 ///
516 /// When the account grows, the bytes in `[old_len, new_len)` are
517 /// zero-filled. The Solana loader zeroes the realloc reserve once at
518 /// the start of an instruction, but a shrink-then-grow within a
519 /// single instruction can re-expose previously written bytes; zeroing
520 /// on growth makes that impossible. Use [`resize_raw`](Self::resize_raw)
521 /// for the hot path when the caller will overwrite the grown region
522 /// in full and has measured the saved `memset`.
523 #[inline(always)]
524 pub fn resize(&self, new_len: usize) -> Result<(), ProgramError> {
525 let old_len = self.data_len();
526 if new_len == old_len {
527 return Ok(());
528 }
529
530 self.check_resize(new_len)?;
531 // SAFETY: `data_ptr_unchecked()` is the account data base; the loader
532 // guarantees `[old_len, new_len)` is within the realloc-reserve
533 // capacity once the `InvalidRealloc` bound above has passed.
534 unsafe {
535 if new_len > old_len {
536 crate::mem::memset(self.data_ptr_unchecked().add(old_len), 0, new_len - old_len);
537 }
538 (*self.raw).data_len = new_len as u64;
539 }
540 Ok(())
541 }
542
543 /// Resize without zero-filling the newly exposed region.
544 ///
545 /// Same bounds check as [`resize`](Self::resize) but skips the
546 /// zero-fill on growth. Prefer `resize` unless the caller immediately
547 /// overwrites the entire grown region; otherwise stale bytes from an
548 /// earlier shrink within the same instruction can leak into the new
549 /// region.
550 #[inline(always)]
551 pub fn resize_raw(&self, new_len: usize) -> Result<(), ProgramError> {
552 if new_len == self.data_len() {
553 return Ok(());
554 }
555
556 self.check_resize(new_len)?;
557 // SAFETY: bounds validated above; only header fields are written.
558 unsafe {
559 (*self.raw).data_len = new_len as u64;
560 }
561 Ok(())
562 }
563
564 /// Resize without bounds checking or zero-filling.
565 ///
566 /// # Safety
567 ///
568 /// The caller must guarantee that the account is writable, no data borrow
569 /// is live, and
570 /// `new_len.saturating_sub(original_data_len) <= MAX_PERMITTED_DATA_INCREASE`.
571 /// The caller is also responsible for any zero-fill of the grown region
572 /// (see [`resize`](Self::resize) for why that matters).
573 #[inline(always)]
574 pub unsafe fn resize_unchecked(&self, new_len: usize) {
575 // 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.
576 unsafe {
577 (*self.raw).data_len = new_len as u64;
578 }
579 }
580
581 // ── Close ────────────────────────────────────────────────────────
582
583 /// Solana System Program address (all-zero pubkey).
584 ///
585 /// Closing an account transfers ownership back to the System
586 /// Program, which is the canonical "no-owner" state on Solana.
587 /// The byte value `[0u8; 32]` and `Address::default()` are
588 /// equivalent, but using this named constant makes the intent
589 /// explicit and avoids the ambiguous `Address::default()` spelling.
590 pub const SYSTEM_PROGRAM_ID: Address = Address::new_from_array([0u8; 32]);
591
592 /// Close the account: zero lamports and data, reassign owner to
593 /// the System Program.
594 ///
595 /// Fails with `AccountBorrowFailed` if any data borrow (shared or
596 /// exclusive) is outstanding: closing memsets the entire data region,
597 /// which would mutate memory a live `Ref`/`RefMut` still points at.
598 /// Use [`close_unchecked`](Self::close_unchecked) only when the caller
599 /// can prove no borrow is live.
600 ///
601 /// # Caveat
602 ///
603 /// This low-level routine does **not** verify the caller has
604 /// authority to close the account, Solana's runtime enforces
605 /// owner/writable rules at transaction commit time regardless, but
606 /// higher-level APIs (e.g. `hopper_runtime::AccountView::close_to`)
607 /// should pre-check those rules. See `account.rs::close_to` for
608 /// the safe wrapper.
609 #[inline(always)]
610 pub fn close(&self) -> ProgramResult {
611 // Zeroing the data region below would mutate bytes a live borrow
612 // still references; refuse rather than invalidate it.
613 self.check_borrow_mut()?;
614 self.set_lamports(0);
615 // SAFETY: no data borrow is outstanding (checked above); `data_ptr_unchecked`
616 // and `data_len` describe this account's SVM-owned data region.
617 unsafe {
618 let len = self.data_len();
619 if len > 0 {
620 // Use the SVM's JIT-compiled memset for optimal CU cost.
621 crate::mem::memset(self.data_ptr_unchecked(), 0, len);
622 }
623 (*self.raw).data_len = 0;
624 (*self.raw).owner = Self::SYSTEM_PROGRAM_ID;
625 }
626 Ok(())
627 }
628
629 /// Close without borrow checks.
630 ///
631 /// # Safety
632 ///
633 /// The caller must ensure no active borrows exist.
634 #[inline(always)]
635 pub unsafe fn close_unchecked(&self) {
636 // 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.
637 unsafe {
638 (*self.raw).lamports = 0;
639 (*self.raw).data_len = 0;
640 (*self.raw).owner = Self::SYSTEM_PROGRAM_ID;
641 }
642 }
643
644 // ── Raw pointers ─────────────────────────────────────────────────
645
646 /// Raw pointer to the `RuntimeAccount` header.
647 #[inline(always)]
648 pub const fn account_ptr(&self) -> *const RuntimeAccount {
649 self.raw as *const RuntimeAccount
650 }
651
652 /// Raw pointer to the first byte of account data.
653 ///
654 /// The data starts immediately after the 88-byte `RuntimeAccount` header.
655 /// This is an expert-only substrate escape hatch: constructing the pointer
656 /// is safe, but dereferencing it is unsafe and bypasses Hopper Native's
657 /// borrow-state checks, segment registry, and writable checks. Normal code
658 /// should use `try_borrow`, `try_borrow_mut`, `segment_ref`, or
659 /// `segment_mut`. Framework code should route user-facing raw access
660 /// through the documented unsafe runtime APIs (`Context::as_mut_ptr` /
661 /// `Context::as_ptr`) instead of exposing this method directly.
662 #[doc(hidden)]
663 #[inline(always)]
664 pub fn data_ptr_unchecked(&self) -> *mut u8 {
665 // SAFETY: Adding the struct size to the base pointer yields the
666 // first data byte. The runtime guarantees this memory is valid.
667 unsafe { (self.raw as *mut u8).add(core::mem::size_of::<RuntimeAccount>()) }
668 }
669
670 // ── Hopper Innovations ───────────────────────────────────────────
671
672 /// Validate that this account is a signer, returning a typed error.
673 #[inline(always)]
674 pub fn require_signer(&self) -> ProgramResult {
675 if self.is_signer() {
676 Ok(())
677 } else {
678 Err(ProgramError::MissingRequiredSignature)
679 }
680 }
681
682 /// Validate that this account is writable.
683 #[inline(always)]
684 pub fn require_writable(&self) -> ProgramResult {
685 if self.is_writable() {
686 Ok(())
687 } else {
688 Err(ProgramError::Immutable)
689 }
690 }
691
692 /// Validate that this account is owned by the given program.
693 #[inline(always)]
694 pub fn require_owned_by(&self, program: &Address) -> ProgramResult {
695 if self.owned_by(program) {
696 Ok(())
697 } else {
698 Err(ProgramError::IncorrectProgramId)
699 }
700 }
701
702 /// Validate signer + writable (common "payer" pattern).
703 #[inline(always)]
704 pub fn require_payer(&self) -> ProgramResult {
705 self.require_signer()?;
706 self.require_writable()
707 }
708
709 /// Read the Hopper account discriminator (first byte of data).
710 ///
711 /// Returns 0 if the account has no data.
712 #[inline(always)]
713 pub fn disc(&self) -> u8 {
714 if self.data_len() == 0 {
715 return 0;
716 }
717 // 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.
718 unsafe { *self.data_ptr_unchecked() }
719 }
720
721 /// Read the Hopper account version (second byte of data).
722 ///
723 /// Returns 0 if the account has fewer than 2 bytes.
724 #[inline(always)]
725 pub fn version(&self) -> u8 {
726 if self.data_len() < 2 {
727 return 0;
728 }
729 // 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.
730 unsafe { *self.data_ptr_unchecked().add(1) }
731 }
732
733 /// Read the 8-byte layout_id from the Hopper account header
734 /// (bytes 4..12 of account data, per the canonical header format).
735 ///
736 /// Returns `None` if the account has fewer than 12 bytes.
737 #[inline(always)]
738 pub fn layout_id(&self) -> Option<&[u8; 8]> {
739 if self.data_len() < 12 {
740 return None;
741 }
742 // 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.
743 unsafe { Some(&*(self.data_ptr_unchecked().add(4) as *const [u8; 8])) }
744 }
745
746 /// Verify that this account has the given discriminator.
747 #[inline(always)]
748 pub fn require_disc(&self, expected: u8) -> ProgramResult {
749 if self.disc() == expected {
750 Ok(())
751 } else {
752 Err(ProgramError::InvalidAccountData)
753 }
754 }
755
756 // -- Chainable validation ---------------
757 //
758 // Return `Result<&Self>` so callers can chain:
759 //
760 // account
761 // .check_signer()?
762 // .check_writable()?
763 // .check_owned_by(&MY_PROGRAM_ID)?;
764 //
765 // Validated once, used everywhere. This pattern exists in Steel but
766 // not in pinocchio, Anchor, or Quasar.
767
768 /// Chainable signer check.
769 #[inline(always)]
770 pub fn check_signer(&self) -> Result<&Self, ProgramError> {
771 if self.is_signer() {
772 Ok(self)
773 } else {
774 Err(ProgramError::MissingRequiredSignature)
775 }
776 }
777
778 /// Chainable writable check.
779 #[inline(always)]
780 pub fn check_writable(&self) -> Result<&Self, ProgramError> {
781 if self.is_writable() {
782 Ok(self)
783 } else {
784 Err(ProgramError::Immutable)
785 }
786 }
787
788 /// Chainable ownership check.
789 #[inline(always)]
790 pub fn check_owned_by(&self, program: &Address) -> Result<&Self, ProgramError> {
791 if self.owned_by(program) {
792 Ok(self)
793 } else {
794 Err(ProgramError::IncorrectProgramId)
795 }
796 }
797
798 /// Chainable discriminator check.
799 #[inline(always)]
800 pub fn check_disc(&self, expected: u8) -> Result<&Self, ProgramError> {
801 if self.disc() == expected {
802 Ok(self)
803 } else {
804 Err(ProgramError::InvalidAccountData)
805 }
806 }
807
808 /// Chainable non-empty data check.
809 #[inline(always)]
810 pub fn check_has_data(&self) -> Result<&Self, ProgramError> {
811 if !self.is_data_empty() {
812 Ok(self)
813 } else {
814 Err(ProgramError::AccountDataTooSmall)
815 }
816 }
817
818 /// Chainable executable check.
819 #[inline(always)]
820 pub fn check_executable(&self) -> Result<&Self, ProgramError> {
821 if self.executable() {
822 Ok(self)
823 } else {
824 Err(ProgramError::InvalidArgument)
825 }
826 }
827
828 /// Chainable address check.
829 #[inline(always)]
830 pub fn check_address(&self, expected: &Address) -> Result<&Self, ProgramError> {
831 if address_eq(self.address(), expected) {
832 Ok(self)
833 } else {
834 Err(ProgramError::InvalidArgument)
835 }
836 }
837
838 /// Chainable minimum data length check.
839 #[inline(always)]
840 pub fn check_data_len(&self, min_len: usize) -> Result<&Self, ProgramError> {
841 if self.data_len() >= min_len {
842 Ok(self)
843 } else {
844 Err(ProgramError::AccountDataTooSmall)
845 }
846 }
847
848 // -- Safe owner access ---------------------------------------------
849
850 /// Read the owner address as a copy (32-byte value).
851 ///
852 /// Unlike `owner()` (which is unsafe due to reference invalidation
853 /// if `assign()` is called), this returns a copy that is always safe.
854 /// Costs 32 bytes of stack space but eliminates aliasing hazards.
855 #[inline(always)]
856 pub fn read_owner(&self) -> Address {
857 // 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.
858 unsafe { (*self.raw).owner.clone() }
859 }
860
861 // -- Packed flags --------------------------------------------------
862
863 /// Read the first 4 bytes of the account header as a single u32.
864 ///
865 /// Layout (little-endian): `[borrow_state, is_signer, is_writable, executable]`
866 ///
867 /// This is the fastest way to extract multiple account properties at once
868 ///, a single aligned u32 read instead of 3-4 separate byte loads.
869 #[inline(always)]
870 fn header_u32(&self) -> u32 {
871 // SAFETY: RuntimeAccount is #[repr(C)] with first 4 bytes as
872 // u8 fields; read_unaligned imposes no alignment requirement.
873 unsafe { core::ptr::read_unaligned(self.raw as *const u32) }
874 }
875
876 /// Pack the account's boolean flags into a single byte for fast
877 /// comparison.
878 ///
879 /// Bit layout:
880 /// - bit 0: is_signer
881 /// - bit 1: is_writable
882 /// - bit 2: executable
883 /// - bit 3: has data (data_len > 0)
884 ///
885 /// Use with `expect_flags()` for single-instruction multi-check:
886 ///
887 /// ```ignore
888 /// // Require: signer + writable + has data
889 /// account.expect_flags(0b1011)?;
890 /// ```
891 #[inline(always)]
892 pub fn flags(&self) -> u8 {
893 // Single u32 read extracts [borrow_state, is_signer, is_writable, executable].
894 // On little-endian: is_signer = bits 8-15, is_writable = bits 16-23, executable = bits 24-31.
895 let h = self.header_u32();
896 let mut f: u8 = 0;
897 if h & 0x0000_FF00 != 0 {
898 f |= 0b0001;
899 } // is_signer
900 if h & 0x00FF_0000 != 0 {
901 f |= 0b0010;
902 } // is_writable
903 if h & 0xFF00_0000 != 0 {
904 f |= 0b0100;
905 } // executable
906 if !self.is_data_empty() {
907 f |= 0b1000;
908 }
909 f
910 }
911
912 /// Check that the account's flags contain all the required bits.
913 ///
914 /// `required` is a bitmask of flags that must be set. See `flags()`.
915 #[inline(always)]
916 pub fn expect_flags(&self, required: u8) -> ProgramResult {
917 if self.flags() & required == required {
918 Ok(())
919 } else {
920 Err(ProgramError::InvalidArgument)
921 }
922 }
923
924 /// Fast fused signer/writable predicate over the packed header word.
925 ///
926 /// Answers "are the requested signer/writable bytes both set" with a
927 /// single 4-byte header read and one masked compare
928 /// (`(header_u32 & mask) == expected`), never touching `data_len`, unlike
929 /// [`flags`](Self::flags), which also folds in the has-data bit via
930 /// `is_data_empty()`. `need_signer`/`need_writable` are compile-time
931 /// literals at every call site and this is `#[inline(always)]`, so `mask`
932 /// and `expected` fold to constants and the whole check is one `and` plus
933 /// one `cmp`.
934 ///
935 /// Behaviourally identical to today's `flags()`-based signer/writable
936 /// gate: the loader serializes `is_signer`/`is_writable` as exactly `0` or
937 /// `1`, so for those bytes "equals the expected `1` pattern" and
938 /// "byte non-zero" coincide. Callers that need the precise per-condition
939 /// error must fall back to `require_signer`/`require_writable` on a `false`
940 /// return (see `hopper_runtime::AccountView::expect_signer_writable`).
941 #[inline(always)]
942 pub fn is_signer_writable(&self, need_signer: bool, need_writable: bool) -> bool {
943 let h = self.header_u32();
944 let mut mask: u32 = 0;
945 let mut expected: u32 = 0;
946 if need_signer {
947 // is_signer occupies bits 8..16 (little-endian byte 1).
948 mask |= 0x0000_FF00;
949 expected |= 0x0000_0100;
950 }
951 if need_writable {
952 // is_writable occupies bits 16..24 (little-endian byte 2).
953 mask |= 0x00FF_0000;
954 expected |= 0x0001_0000;
955 }
956 (h & mask) == expected
957 }
958}
959
960impl<'info> core::fmt::Debug for AccountView<'info> {
961 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
962 f.debug_struct("AccountView")
963 .field("address", self.address())
964 .field("lamports", &self.lamports())
965 .field("data_len", &self.data_len())
966 .field("is_signer", &self.is_signer())
967 .field("is_writable", &self.is_writable())
968 .finish()
969 }
970}
971
972// ── RemainingAccounts ────────────────────────────────────────────────
973
974/// Iterator over remaining (unstructured) accounts after the known ones.
975pub struct RemainingAccounts<'a> {
976 accounts: &'a [AccountView<'a>],
977 cursor: usize,
978}
979
980impl<'a> RemainingAccounts<'a> {
981 /// Create from a slice of the remaining accounts.
982 #[inline(always)]
983 pub fn new(accounts: &'a [AccountView<'a>]) -> Self {
984 Self {
985 accounts,
986 cursor: 0,
987 }
988 }
989
990 /// Number of accounts remaining.
991 #[inline(always)]
992 pub fn remaining(&self) -> usize {
993 self.accounts.len() - self.cursor
994 }
995
996 /// Take the next account, or return `NotEnoughAccountKeys`.
997 ///
998 /// This is a fallible cursor advance, not an `Iterator::next`: it yields a
999 /// `Result` so a missing account is a program error rather than a silent
1000 /// `None`, which is the wrong shape for the `Iterator` trait.
1001 #[allow(clippy::should_implement_trait)]
1002 #[inline(always)]
1003 pub fn next(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1004 if self.cursor >= self.accounts.len() {
1005 return Err(ProgramError::NotEnoughAccountKeys);
1006 }
1007 let account = &self.accounts[self.cursor];
1008 self.cursor += 1;
1009 Ok(account)
1010 }
1011
1012 /// Take the next account that is a signer.
1013 #[inline(always)]
1014 pub fn next_signer(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1015 let account = self.next()?;
1016 account.require_signer()?;
1017 Ok(account)
1018 }
1019
1020 /// Take the next account that is writable.
1021 #[inline(always)]
1022 pub fn next_writable(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1023 let account = self.next()?;
1024 account.require_writable()?;
1025 Ok(account)
1026 }
1027
1028 /// Take the next account owned by the given program.
1029 #[inline(always)]
1030 pub fn next_owned_by(
1031 &mut self,
1032 program: &Address,
1033 ) -> Result<&'a AccountView<'a>, ProgramError> {
1034 let account = self.next()?;
1035 account.require_owned_by(program)?;
1036 Ok(account)
1037 }
1038}