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 /// ownership or application authority to close the account. Writability
605 /// and outstanding data borrows are checked locally. Solana's runtime
606 /// also enforces account modification rules, but
607 /// higher-level APIs (e.g. `hopper_runtime::AccountView::close_to`)
608 /// should pre-check those rules. See `account.rs::close_to` for
609 /// the safe wrapper.
610 #[inline(always)]
611 pub fn close(&self) -> ProgramResult {
612 // Zeroing the data region below would mutate bytes a live borrow
613 // still references; refuse rather than invalidate it.
614 self.require_writable()?;
615 self.check_borrow_mut()?;
616 self.set_lamports(0);
617 // SAFETY: no data borrow is outstanding (checked above); `data_ptr_unchecked`
618 // and `data_len` describe this account's SVM-owned data region.
619 unsafe {
620 let len = self.data_len();
621 if len > 0 {
622 // Use the SVM's JIT-compiled memset for optimal CU cost.
623 crate::mem::memset(self.data_ptr_unchecked(), 0, len);
624 }
625 (*self.raw).data_len = 0;
626 (*self.raw).owner = Self::SYSTEM_PROGRAM_ID;
627 }
628 Ok(())
629 }
630
631 /// Close without borrow checks.
632 ///
633 /// # Safety
634 ///
635 /// The caller must ensure no active borrows exist.
636 #[inline(always)]
637 pub unsafe fn close_unchecked(&self) {
638 // 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.
639 unsafe {
640 (*self.raw).lamports = 0;
641 (*self.raw).data_len = 0;
642 (*self.raw).owner = Self::SYSTEM_PROGRAM_ID;
643 }
644 }
645
646 // ── Raw pointers ─────────────────────────────────────────────────
647
648 /// Raw pointer to the `RuntimeAccount` header.
649 #[inline(always)]
650 pub const fn account_ptr(&self) -> *const RuntimeAccount {
651 self.raw as *const RuntimeAccount
652 }
653
654 /// Raw pointer to the first byte of account data.
655 ///
656 /// The data starts immediately after the 88-byte `RuntimeAccount` header.
657 /// This is an expert-only substrate escape hatch: constructing the pointer
658 /// is safe, but dereferencing it is unsafe and bypasses Hopper Native's
659 /// borrow-state checks, segment registry, and writable checks. Normal code
660 /// should use `try_borrow`, `try_borrow_mut`, `segment_ref`, or
661 /// `segment_mut`. Framework code should route user-facing raw access
662 /// through the documented unsafe runtime APIs (`Context::as_mut_ptr` /
663 /// `Context::as_ptr`) instead of exposing this method directly.
664 #[doc(hidden)]
665 #[inline(always)]
666 pub fn data_ptr_unchecked(&self) -> *mut u8 {
667 // SAFETY: Adding the struct size to the base pointer yields the
668 // first data byte. The runtime guarantees this memory is valid.
669 unsafe { (self.raw as *mut u8).add(core::mem::size_of::<RuntimeAccount>()) }
670 }
671
672 // ── Hopper Innovations ───────────────────────────────────────────
673
674 /// Validate that this account is a signer, returning a typed error.
675 #[inline(always)]
676 pub fn require_signer(&self) -> ProgramResult {
677 if self.is_signer() {
678 Ok(())
679 } else {
680 Err(ProgramError::MissingRequiredSignature)
681 }
682 }
683
684 /// Validate that this account is writable.
685 #[inline(always)]
686 pub fn require_writable(&self) -> ProgramResult {
687 if self.is_writable() {
688 Ok(())
689 } else {
690 Err(ProgramError::Immutable)
691 }
692 }
693
694 /// Validate that this account is owned by the given program.
695 #[inline(always)]
696 pub fn require_owned_by(&self, program: &Address) -> ProgramResult {
697 if self.owned_by(program) {
698 Ok(())
699 } else {
700 Err(ProgramError::IncorrectProgramId)
701 }
702 }
703
704 /// Validate signer + writable (common "payer" pattern).
705 #[inline(always)]
706 pub fn require_payer(&self) -> ProgramResult {
707 self.require_signer()?;
708 self.require_writable()
709 }
710
711 /// Read the Hopper account discriminator (first byte of data).
712 ///
713 /// Returns 0 if the account has no data.
714 #[inline(always)]
715 pub fn disc(&self) -> u8 {
716 if self.data_len() == 0 {
717 return 0;
718 }
719 // 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.
720 unsafe { *self.data_ptr_unchecked() }
721 }
722
723 /// Read the Hopper account version (second byte of data).
724 ///
725 /// Returns 0 if the account has fewer than 2 bytes.
726 #[inline(always)]
727 pub fn version(&self) -> u8 {
728 if self.data_len() < 2 {
729 return 0;
730 }
731 // 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.
732 unsafe { *self.data_ptr_unchecked().add(1) }
733 }
734
735 /// Read the 8-byte layout_id from the Hopper account header
736 /// (bytes 4..12 of account data, per the canonical header format).
737 ///
738 /// Returns `None` if the account has fewer than 12 bytes.
739 #[inline(always)]
740 pub fn layout_id(&self) -> Option<&[u8; 8]> {
741 if self.data_len() < 12 {
742 return None;
743 }
744 // 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.
745 unsafe { Some(&*(self.data_ptr_unchecked().add(4) as *const [u8; 8])) }
746 }
747
748 /// Verify that this account has the given discriminator.
749 #[inline(always)]
750 pub fn require_disc(&self, expected: u8) -> ProgramResult {
751 if self.disc() == expected {
752 Ok(())
753 } else {
754 Err(ProgramError::InvalidAccountData)
755 }
756 }
757
758 // -- Chainable validation ---------------
759 //
760 // Return `Result<&Self>` so callers can chain:
761 //
762 // account
763 // .check_signer()?
764 // .check_writable()?
765 // .check_owned_by(&MY_PROGRAM_ID)?;
766 //
767 // Validated once, used everywhere. This pattern exists in Steel but
768 // not in pinocchio, Anchor, or Quasar.
769
770 /// Chainable signer check.
771 #[inline(always)]
772 pub fn check_signer(&self) -> Result<&Self, ProgramError> {
773 if self.is_signer() {
774 Ok(self)
775 } else {
776 Err(ProgramError::MissingRequiredSignature)
777 }
778 }
779
780 /// Chainable writable check.
781 #[inline(always)]
782 pub fn check_writable(&self) -> Result<&Self, ProgramError> {
783 if self.is_writable() {
784 Ok(self)
785 } else {
786 Err(ProgramError::Immutable)
787 }
788 }
789
790 /// Chainable ownership check.
791 #[inline(always)]
792 pub fn check_owned_by(&self, program: &Address) -> Result<&Self, ProgramError> {
793 if self.owned_by(program) {
794 Ok(self)
795 } else {
796 Err(ProgramError::IncorrectProgramId)
797 }
798 }
799
800 /// Chainable discriminator check.
801 #[inline(always)]
802 pub fn check_disc(&self, expected: u8) -> Result<&Self, ProgramError> {
803 if self.disc() == expected {
804 Ok(self)
805 } else {
806 Err(ProgramError::InvalidAccountData)
807 }
808 }
809
810 /// Chainable non-empty data check.
811 #[inline(always)]
812 pub fn check_has_data(&self) -> Result<&Self, ProgramError> {
813 if !self.is_data_empty() {
814 Ok(self)
815 } else {
816 Err(ProgramError::AccountDataTooSmall)
817 }
818 }
819
820 /// Chainable executable check.
821 #[inline(always)]
822 pub fn check_executable(&self) -> Result<&Self, ProgramError> {
823 if self.executable() {
824 Ok(self)
825 } else {
826 Err(ProgramError::InvalidArgument)
827 }
828 }
829
830 /// Chainable address check.
831 #[inline(always)]
832 pub fn check_address(&self, expected: &Address) -> Result<&Self, ProgramError> {
833 if address_eq(self.address(), expected) {
834 Ok(self)
835 } else {
836 Err(ProgramError::InvalidArgument)
837 }
838 }
839
840 /// Chainable minimum data length check.
841 #[inline(always)]
842 pub fn check_data_len(&self, min_len: usize) -> Result<&Self, ProgramError> {
843 if self.data_len() >= min_len {
844 Ok(self)
845 } else {
846 Err(ProgramError::AccountDataTooSmall)
847 }
848 }
849
850 // -- Safe owner access ---------------------------------------------
851
852 /// Read the owner address as a copy (32-byte value).
853 ///
854 /// Unlike `owner()` (which is unsafe due to reference invalidation
855 /// if `assign()` is called), this returns a copy that is always safe.
856 /// Costs 32 bytes of stack space but eliminates aliasing hazards.
857 #[inline(always)]
858 pub fn read_owner(&self) -> Address {
859 // 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.
860 unsafe { (*self.raw).owner.clone() }
861 }
862
863 // -- Packed flags --------------------------------------------------
864
865 /// Read the first 4 bytes of the account header as a single u32.
866 ///
867 /// Layout (little-endian): `[borrow_state, is_signer, is_writable, executable]`
868 ///
869 /// This is the fastest way to extract multiple account properties at once
870 ///, a single aligned u32 read instead of 3-4 separate byte loads.
871 #[inline(always)]
872 fn header_u32(&self) -> u32 {
873 // SAFETY: RuntimeAccount is #[repr(C)] with first 4 bytes as
874 // u8 fields; read_unaligned imposes no alignment requirement.
875 unsafe { core::ptr::read_unaligned(self.raw as *const u32) }
876 }
877
878 /// Pack the account's boolean flags into a single byte for fast
879 /// comparison.
880 ///
881 /// Bit layout:
882 /// - bit 0: is_signer
883 /// - bit 1: is_writable
884 /// - bit 2: executable
885 /// - bit 3: has data (data_len > 0)
886 ///
887 /// Use with `expect_flags()` for single-instruction multi-check:
888 ///
889 /// ```ignore
890 /// // Require: signer + writable + has data
891 /// account.expect_flags(0b1011)?;
892 /// ```
893 #[inline(always)]
894 pub fn flags(&self) -> u8 {
895 // Single u32 read extracts [borrow_state, is_signer, is_writable, executable].
896 // On little-endian: is_signer = bits 8-15, is_writable = bits 16-23, executable = bits 24-31.
897 let h = self.header_u32();
898 let mut f: u8 = 0;
899 if h & 0x0000_FF00 != 0 {
900 f |= 0b0001;
901 } // is_signer
902 if h & 0x00FF_0000 != 0 {
903 f |= 0b0010;
904 } // is_writable
905 if h & 0xFF00_0000 != 0 {
906 f |= 0b0100;
907 } // executable
908 if !self.is_data_empty() {
909 f |= 0b1000;
910 }
911 f
912 }
913
914 /// Check that the account's flags contain all the required bits.
915 ///
916 /// `required` is a bitmask of flags that must be set. See `flags()`.
917 #[inline(always)]
918 pub fn expect_flags(&self, required: u8) -> ProgramResult {
919 if self.flags() & required == required {
920 Ok(())
921 } else {
922 Err(ProgramError::InvalidArgument)
923 }
924 }
925
926 /// Fast fused signer/writable predicate over the packed header word.
927 ///
928 /// Answers "are the requested signer/writable bytes both set" with a
929 /// single 4-byte header read and one masked compare
930 /// (`(header_u32 & mask) == expected`), never touching `data_len`, unlike
931 /// [`flags`](Self::flags), which also folds in the has-data bit via
932 /// `is_data_empty()`. `need_signer`/`need_writable` are compile-time
933 /// literals at every call site and this is `#[inline(always)]`, so `mask`
934 /// and `expected` fold to constants and the whole check is one `and` plus
935 /// one `cmp`.
936 ///
937 /// Behaviourally identical to today's `flags()`-based signer/writable
938 /// gate: the loader serializes `is_signer`/`is_writable` as exactly `0` or
939 /// `1`, so for those bytes "equals the expected `1` pattern" and
940 /// "byte non-zero" coincide. Callers that need the precise per-condition
941 /// error must fall back to `require_signer`/`require_writable` on a `false`
942 /// return (see `hopper_runtime::AccountView::expect_signer_writable`).
943 #[inline(always)]
944 pub fn is_signer_writable(&self, need_signer: bool, need_writable: bool) -> bool {
945 let h = self.header_u32();
946 let mut mask: u32 = 0;
947 let mut expected: u32 = 0;
948 if need_signer {
949 // is_signer occupies bits 8..16 (little-endian byte 1).
950 mask |= 0x0000_FF00;
951 expected |= 0x0000_0100;
952 }
953 if need_writable {
954 // is_writable occupies bits 16..24 (little-endian byte 2).
955 mask |= 0x00FF_0000;
956 expected |= 0x0001_0000;
957 }
958 (h & mask) == expected
959 }
960}
961
962impl<'info> core::fmt::Debug for AccountView<'info> {
963 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
964 f.debug_struct("AccountView")
965 .field("address", self.address())
966 .field("lamports", &self.lamports())
967 .field("data_len", &self.data_len())
968 .field("is_signer", &self.is_signer())
969 .field("is_writable", &self.is_writable())
970 .finish()
971 }
972}
973
974// ── RemainingAccounts ────────────────────────────────────────────────
975
976/// Iterator over remaining (unstructured) accounts after the known ones.
977pub struct RemainingAccounts<'a> {
978 accounts: &'a [AccountView<'a>],
979 cursor: usize,
980}
981
982impl<'a> RemainingAccounts<'a> {
983 /// Create from a slice of the remaining accounts.
984 #[inline(always)]
985 pub fn new(accounts: &'a [AccountView<'a>]) -> Self {
986 Self {
987 accounts,
988 cursor: 0,
989 }
990 }
991
992 /// Number of accounts remaining.
993 #[inline(always)]
994 pub fn remaining(&self) -> usize {
995 self.accounts.len() - self.cursor
996 }
997
998 /// Take the next account, or return `NotEnoughAccountKeys`.
999 ///
1000 /// This is a fallible cursor advance, not an `Iterator::next`: it yields a
1001 /// `Result` so a missing account is a program error rather than a silent
1002 /// `None`, which is the wrong shape for the `Iterator` trait.
1003 #[allow(clippy::should_implement_trait)]
1004 #[inline(always)]
1005 pub fn next(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1006 if self.cursor >= self.accounts.len() {
1007 return Err(ProgramError::NotEnoughAccountKeys);
1008 }
1009 let account = &self.accounts[self.cursor];
1010 self.cursor += 1;
1011 Ok(account)
1012 }
1013
1014 /// Take the next account that is a signer.
1015 #[inline(always)]
1016 pub fn next_signer(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1017 let account = self.next()?;
1018 account.require_signer()?;
1019 Ok(account)
1020 }
1021
1022 /// Take the next account that is writable.
1023 #[inline(always)]
1024 pub fn next_writable(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
1025 let account = self.next()?;
1026 account.require_writable()?;
1027 Ok(account)
1028 }
1029
1030 /// Take the next account owned by the given program.
1031 #[inline(always)]
1032 pub fn next_owned_by(
1033 &mut self,
1034 program: &Address,
1035 ) -> Result<&'a AccountView<'a>, ProgramError> {
1036 let account = self.next()?;
1037 account.require_owned_by(program)?;
1038 Ok(account)
1039 }
1040}