Skip to main content

hopper_native/
lazy.rs

1//! Lazy account parser -- on-demand account deserialization.
2//!
3//! The standard entrypoint parses every account upfront, burning CU even
4//! for accounts the instruction never touches. The lazy parser gives you
5//! instruction data and program ID immediately, then hands back an
6//! iterator that parses accounts one at a time ON DEMAND.
7//!
8//! Hopper's lazy path is distinct not because Pinocchio lacks lazy parsing,
9//! but because Hopper Native pre-scans the instruction tail, preserves
10//! canonical duplicate-account handling in `raw_input`, and then exposes a
11//! `LazyContext` that already knows `instruction_data` and `program_id`
12//! before the first account is materialized.
13//!
14//! # CU Savings
15//!
16//! Programs that dispatch on `instruction_data[0]` and only need a subset
17//! of accounts save measurable CU. A vault program that routes 8 instruction
18//! variants through a single entrypoint might only parse 2-3 of 10 accounts
19//! for a given variant.
20//!
21//! # Usage
22//!
23//! ```ignore
24//! use hopper_native::lazy::LazyContext;
25//! use hopper_native::hopper_lazy_entrypoint;
26//!
27//! hopper_lazy_entrypoint!(process);
28//!
29//! fn process(ctx: LazyContext) -> ProgramResult {
30//!     let disc = ctx.instruction_data().first().copied().unwrap_or(0);
31//!     match disc {
32//!         0 => {
33//!             let payer = ctx.next_account()?;
34//!             let vault = ctx.next_account()?;
35//!             // Remaining accounts are never parsed.
36//!             do_deposit(payer, vault, &ctx.instruction_data()[1..])
37//!         }
38//!         _ => Err(ProgramError::InvalidInstructionData),
39//!     }
40//! }
41//! ```
42
43use crate::account_view::AccountView;
44use crate::address::Address;
45use crate::error::ProgramError;
46use crate::raw_account::RuntimeAccount;
47use crate::MAX_PERMITTED_DATA_INCREASE;
48
49const BPF_ALIGN_OF_U128: usize = 8;
50
51/// Pre-parsed header from the BPF input buffer: instruction data +
52/// program ID, plus a cursor positioned at the first account.
53///
54/// Accounts are parsed lazily as you call `next_account()`.
55pub struct LazyContext {
56    /// Raw pointer into the BPF input buffer, positioned at the first
57    /// account (or past the account count if num_accounts == 0).
58    cursor: *mut u8,
59    /// Total number of accounts declared in the input.
60    total_accounts: usize,
61    /// Number of accounts already parsed.
62    parsed_count: usize,
63    /// Instruction data slice (lifetime tied to the BPF input buffer).
64    instruction_data: *const u8,
65    instruction_data_len: usize,
66    /// Program ID (32 bytes, copied from the input buffer tail).
67    program_id: Address,
68    /// Stack of already-parsed AccountViews so we can resolve duplicates
69    /// that reference earlier accounts. Fixed size = MAX_TX_ACCOUNTS.
70    resolved: [AccountView; 254],
71}
72
73// SAFETY: Single-threaded BPF runtime.
74unsafe impl Send for LazyContext {}
75unsafe impl Sync for LazyContext {}
76
77impl LazyContext {
78    /// Instruction data for this invocation.
79    #[inline(always)]
80    pub fn instruction_data(&self) -> &[u8] {
81        // SAFETY: instruction_data points into the BPF input buffer which
82        // outlives the entire instruction execution.
83        unsafe { core::slice::from_raw_parts(self.instruction_data, self.instruction_data_len) }
84    }
85
86    /// The program ID of this invocation.
87    #[inline(always)]
88    pub fn program_id(&self) -> &Address {
89        &self.program_id
90    }
91
92    /// Number of accounts declared in the transaction.
93    #[inline(always)]
94    pub fn total_accounts(&self) -> usize {
95        self.total_accounts
96    }
97
98    /// Number of accounts parsed so far.
99    #[inline(always)]
100    pub fn parsed_count(&self) -> usize {
101        self.parsed_count
102    }
103
104    /// Number of accounts remaining to be parsed.
105    #[inline(always)]
106    pub fn remaining(&self) -> usize {
107        self.total_accounts - self.parsed_count
108    }
109
110    /// Parse and return the next account from the input buffer.
111    ///
112    /// Each call advances the internal cursor by one account. Returns
113    /// `Err(NotEnoughAccountKeys)` if all accounts have been consumed.
114    #[inline]
115    pub fn next_account(&mut self) -> Result<AccountView, ProgramError> {
116        if self.parsed_count >= self.total_accounts {
117            return Err(ProgramError::NotEnoughAccountKeys);
118        }
119
120        let view = unsafe { self.parse_one_account() };
121        self.resolved[self.parsed_count] = view.clone();
122        self.parsed_count += 1;
123        Ok(view)
124    }
125
126    /// Parse the next account and validate it is a signer.
127    #[inline]
128    pub fn next_signer(&mut self) -> Result<AccountView, ProgramError> {
129        let acct = self.next_account()?;
130        acct.require_signer()?;
131        Ok(acct)
132    }
133
134    /// Parse the next account and validate it is writable.
135    #[inline]
136    pub fn next_writable(&mut self) -> Result<AccountView, ProgramError> {
137        let acct = self.next_account()?;
138        acct.require_writable()?;
139        Ok(acct)
140    }
141
142    /// Parse the next account and validate it is a writable signer (payer).
143    #[inline]
144    pub fn next_payer(&mut self) -> Result<AccountView, ProgramError> {
145        let acct = self.next_account()?;
146        acct.require_payer()?;
147        Ok(acct)
148    }
149
150    /// Parse the next account and validate it is owned by `program`.
151    #[inline]
152    pub fn next_owned_by(&mut self, program: &Address) -> Result<AccountView, ProgramError> {
153        let acct = self.next_account()?;
154        acct.require_owned_by(program)?;
155        Ok(acct)
156    }
157
158    /// Skip `n` accounts without returning them.
159    ///
160    /// Advances the cursor through the raw buffer without constructing
161    /// full AccountView values, only doing enough work to find account
162    /// boundaries.
163    #[inline]
164    pub fn skip(&mut self, n: usize) -> Result<(), ProgramError> {
165        for _ in 0..n {
166            if self.parsed_count >= self.total_accounts {
167                return Err(ProgramError::NotEnoughAccountKeys);
168            }
169            // Advance cursor past this account without storing it.
170            unsafe { self.advance_cursor() };
171            self.parsed_count += 1;
172        }
173        Ok(())
174    }
175
176    /// Collect all remaining accounts into a slice of the internal buffer.
177    ///
178    /// Parses all remaining accounts eagerly and returns them as a slice.
179    /// After this call, `remaining()` returns 0.
180    #[inline]
181    pub fn drain_remaining(&mut self) -> Result<&[AccountView], ProgramError> {
182        let start = self.parsed_count;
183        while self.parsed_count < self.total_accounts {
184            let view = unsafe { self.parse_one_account() };
185            self.resolved[self.parsed_count] = view;
186            self.parsed_count += 1;
187        }
188        Ok(&self.resolved[start..self.parsed_count])
189    }
190
191    /// Get an already-parsed account by index.
192    ///
193    /// Returns `None` if `index >= parsed_count`.
194    #[inline(always)]
195    pub fn get(&self, index: usize) -> Option<&AccountView> {
196        if index < self.parsed_count {
197            Some(&self.resolved[index])
198        } else {
199            None
200        }
201    }
202
203    /// Parse one account at the current cursor position and advance cursor.
204    ///
205    /// # Safety
206    ///
207    /// Caller must ensure `parsed_count < total_accounts` and that `cursor`
208    /// points to valid BPF input buffer data.
209    #[inline(always)]
210    unsafe fn parse_one_account(&mut self) -> AccountView {
211        unsafe {
212            let dup_marker = *self.cursor;
213
214            if dup_marker == u8::MAX {
215                // Non-duplicate: RuntimeAccount header starts here.
216                let raw = self.cursor as *mut RuntimeAccount;
217                let view = AccountView::new_unchecked(raw);
218                self.advance_non_dup_cursor(raw);
219                view
220            } else {
221                // Duplicate: references an earlier account.
222                let original_idx = dup_marker as usize;
223                self.cursor = self.cursor.add(8); // skip 8-byte padding
224                                                  // The loader guarantees duplicate markers refer to
225                                                  // **previously parsed** slots. A marker that points at
226                                                  // ourselves or forward is malformed loader input -
227                                                  // pre-audit we returned `self.resolved[0]` which is a
228                                                  // zeroed `AccountView` until a real account has been
229                                                  // parsed, silently handing out a null-pointer view. The
230                                                  // Hopper Safety Audit flagged this; we now trap.
231                if original_idx >= self.parsed_count {
232                    crate::raw_input::malformed_duplicate_marker(dup_marker, self.parsed_count);
233                }
234                self.resolved[original_idx].clone()
235            }
236        }
237    }
238
239    /// Advance the cursor past one account slot without constructing a view.
240    ///
241    /// # Safety
242    ///
243    /// Caller must ensure `parsed_count < total_accounts` and cursor is valid.
244    #[inline(always)]
245    unsafe fn advance_cursor(&mut self) {
246        unsafe {
247            let dup_marker = *self.cursor;
248            if dup_marker == u8::MAX {
249                let raw = self.cursor as *mut RuntimeAccount;
250                self.advance_non_dup_cursor(raw);
251            } else {
252                self.cursor = self.cursor.add(8);
253            }
254        }
255    }
256
257    /// Advance cursor past a non-duplicate account (shared by parse + skip).
258    #[inline(always)]
259    unsafe fn advance_non_dup_cursor(&mut self, raw: *mut RuntimeAccount) {
260        unsafe {
261            let data_len = (*raw).data_len as usize;
262            let mut offset = RuntimeAccount::SIZE + data_len + MAX_PERMITTED_DATA_INCREASE;
263            offset += self.cursor.add(offset).align_offset(BPF_ALIGN_OF_U128);
264            offset += 8;
265            self.cursor = self.cursor.add(offset);
266        }
267    }
268}
269
270/// Deserialize a BPF input buffer into a `LazyContext`.
271///
272/// Reads the account count, then scans forward to find instruction data
273/// and program ID WITHOUT parsing any individual accounts. The actual
274/// account parsing is deferred to `LazyContext::next_account()`.
275///
276/// # Safety
277///
278/// `input` must point to a valid Solana BPF input buffer.
279#[inline(always)]
280pub unsafe fn lazy_deserialize(input: *mut u8) -> LazyContext {
281    let frame = unsafe { crate::raw_input::scan_instruction_frame(input) };
282    // SAFETY: AccountView is a single raw pointer, zeroed is a valid
283    // sentinel (null). These slots are only read after `next_account()`
284    // initializes them via `parse_one_account()`.
285    let resolved: [AccountView; 254] = unsafe { core::mem::zeroed() };
286
287    LazyContext {
288        cursor: frame.accounts_start,
289        total_accounts: frame.account_count,
290        parsed_count: 0,
291        instruction_data: frame.instruction_data.as_ptr(),
292        instruction_data_len: frame.instruction_data.len(),
293        program_id: frame.program_id,
294        resolved,
295    }
296}