hopper_native/cpi.rs
1//! Cross-program invocation via `sol_invoke_signed_c`.
2//!
3//! Provides both checked (borrow-validating) and unchecked invoke paths.
4
5use crate::account_view::AccountView;
6use crate::address::address_eq;
7use crate::error::ProgramError;
8use crate::instruction::{CpiAccount, InstructionView, Signer};
9use crate::ProgramResult;
10use core::mem::MaybeUninit;
11
12/// Maximum number of accounts in a static CPI call.
13pub const MAX_STATIC_CPI_ACCOUNTS: usize = 64;
14
15/// Maximum number of accounts in any CPI call.
16pub const MAX_CPI_ACCOUNTS: usize = 128;
17
18/// Maximum return data size (1 KiB).
19pub const MAX_RETURN_DATA: usize = 1024;
20
21// ── Unchecked invoke ─────────────────────────────────────────────────
22
23/// Invoke a CPI without borrow validation (lowest CU cost).
24///
25/// This is Tier C of the CPI surface. The checked variant
26/// ([`invoke`](crate::cpi::invoke)) enforces the full contract below
27/// before calling this function; prefer that unless you have measured
28/// a reason to bypass the validation pass.
29///
30/// # Safety
31///
32/// The caller must uphold every one of the following invariants. A
33/// violation of any of them is undefined behaviour, because the Solana
34/// runtime's `sol_invoke_signed_c` syscall assumes they already hold.
35///
36/// 1. **No aliasing borrows.** No `&` or `&mut` references into any
37/// account data region referenced by `accounts` may be live for
38/// the duration of the call. The CPI can (and will) mutate those
39/// regions via the callee, and Rust's aliasing rules do not permit
40/// the caller to hold outstanding references to memory that is
41/// about to change under it.
42/// 2. **Account list consistency.** Every `CpiAccount` in `accounts`
43/// must correspond to a real account previously passed to the
44/// program's entrypoint (same address, same `is_signer` /
45/// `is_writable` flags the runtime already knows about). The
46/// runtime will not re-derive account permissions; invalid flags
47/// propagate into the callee.
48/// 3. **Writability coverage.** Every account that the `instruction`
49/// marks writable must have `is_writable = true` in `accounts`,
50/// and every account the instruction marks as signer must have
51/// `is_signer = true`. Mismatches are rejected by the runtime but
52/// the rejection path is not cheap and the caller is expected to
53/// get this right.
54/// 4. **No shared mutable slices across CPIs.** If the same account
55/// appears more than once in `accounts` (duplicate accounts), the
56/// caller is responsible for ensuring that any subsequent borrow
57/// of that account's data respects the CPI's writes.
58/// 5. **Valid instruction encoding.** `instruction.program_id`,
59/// `instruction.accounts`, and `instruction.data` must all point
60/// to valid memory for the duration of the call. An
61/// `InstructionView` built from a local `InstructionAccount` slice
62/// is fine; one built from a dropped stack slot is not.
63///
64/// The runtime does not enforce any of these from the caller side -
65/// it assumes a well-formed CPI. That is the cost of the Tier C path.
66#[inline]
67pub unsafe fn invoke_unchecked(
68 instruction: &InstructionView,
69 accounts: &[CpiAccount],
70) -> ProgramResult {
71 #[cfg(target_os = "solana")]
72 {
73 // Build the C-ABI instruction struct on the stack.
74 // The Solana runtime expects:
75 // struct { program_id: *const u8, accounts: *const SolAccountMeta, acct_len: u64, data: *const u8, data_len: u64 }
76 // But sol_invoke_signed_c takes the instruction as raw bytes.
77 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
78 let result = unsafe {
79 crate::syscalls::sol_invoke_signed_c(
80 instruction as *const _ as *const u8,
81 accounts.as_ptr() as *const u8,
82 accounts.len() as u64,
83 core::ptr::null(),
84 0,
85 )
86 };
87 if result == 0 {
88 Ok(())
89 } else {
90 Err(ProgramError::from(result))
91 }
92 }
93 #[cfg(not(target_os = "solana"))]
94 {
95 let _ = (instruction, accounts);
96 Ok(())
97 }
98}
99
100/// Invoke a signed CPI without borrow validation.
101///
102/// Same as [`invoke_unchecked`] but also passes PDA signer seeds so
103/// the callee can accept writes that would otherwise require a
104/// signature.
105///
106/// # Safety
107///
108/// All of [`invoke_unchecked`]'s invariants apply, plus two more for
109/// the signer-seeds path:
110///
111/// 6. **Signer seeds must derive the claimed PDA.** For every
112/// `Signer` in `signers_seeds`, the derived address
113/// (sha256 of `seeds || program_id || PDA_MARKER`) must equal an
114/// address in `accounts` that is marked as signer. A mismatch will
115/// cause the runtime to reject the CPI, but the caller is expected
116/// to have verified this before reaching the Tier C path.
117/// 7. **Seed lifetime.** `signers_seeds` (and every `&[u8]` it points
118/// at) must outlive the call. Temporary seed slices built inside a
119/// function frame are fine; seeds referencing dropped storage are
120/// not.
121///
122/// For the happy path the caller should hold a `CpiValidator` or
123/// equivalent proof-object constructed by the checked path and let
124/// that drive both this function's inputs and the aliasing discipline
125/// required above.
126#[inline]
127pub unsafe fn invoke_signed_unchecked(
128 instruction: &InstructionView,
129 accounts: &[CpiAccount],
130 signers_seeds: &[Signer],
131) -> ProgramResult {
132 #[cfg(target_os = "solana")]
133 {
134 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
135 let result = unsafe {
136 crate::syscalls::sol_invoke_signed_c(
137 instruction as *const _ as *const u8,
138 accounts.as_ptr() as *const u8,
139 accounts.len() as u64,
140 signers_seeds.as_ptr() as *const u8,
141 signers_seeds.len() as u64,
142 )
143 };
144 if result == 0 {
145 Ok(())
146 } else {
147 Err(ProgramError::from(result))
148 }
149 }
150 #[cfg(not(target_os = "solana"))]
151 {
152 let _ = (instruction, accounts, signers_seeds);
153 Ok(())
154 }
155}
156
157// ── CPI validation ───────────────────────────────────────────────────
158
159/// Validate that CPI account views match the instruction's expectations.
160///
161/// Checks:
162/// - Sufficient number of accounts.
163/// - Address identity (order-dependent matching).
164/// - Signer requirements.
165/// - Writable requirements.
166/// - Borrow compatibility (writable accounts must not be already borrowed,
167/// read-only accounts must not be exclusively borrowed).
168#[inline]
169fn validate_cpi_accounts(
170 instruction: &InstructionView,
171 account_views: &[&AccountView],
172) -> ProgramResult {
173 if account_views.len() < instruction.accounts.len() {
174 return Err(ProgramError::NotEnoughAccountKeys);
175 }
176
177 let mut i = 0;
178 while i < instruction.accounts.len() {
179 let expected = &instruction.accounts[i];
180 let actual = account_views[i];
181
182 if !address_eq(actual.address(), expected.address) {
183 return Err(ProgramError::InvalidAccountData);
184 }
185
186 if expected.is_signer && !actual.is_signer() {
187 return Err(ProgramError::MissingRequiredSignature);
188 }
189
190 if expected.is_writable && !actual.is_writable() {
191 return Err(ProgramError::Immutable);
192 }
193
194 // Borrow compatibility: writable needs exclusive access,
195 // read-only needs at least shared access.
196 if expected.is_writable {
197 actual.check_borrow_mut()?;
198 } else {
199 actual.check_borrow()?;
200 }
201
202 i += 1;
203 }
204
205 Ok(())
206}
207
208// ── Checked invoke ───────────────────────────────────────────────────
209
210/// Invoke a CPI with full validation.
211///
212/// Validates account count, address identity, signer/writable requirements,
213/// and borrow compatibility before calling the runtime.
214#[inline]
215pub fn invoke<const ACCOUNTS: usize>(
216 instruction: &InstructionView,
217 account_views: &[&AccountView; ACCOUNTS],
218) -> ProgramResult {
219 invoke_signed::<ACCOUNTS>(instruction, account_views, &[])
220}
221
222/// Invoke a signed CPI with full validation.
223///
224/// Validates account count, address identity, signer/writable requirements,
225/// and borrow compatibility before calling the runtime.
226#[inline]
227pub fn invoke_signed<const ACCOUNTS: usize>(
228 instruction: &InstructionView,
229 account_views: &[&AccountView; ACCOUNTS],
230 signers_seeds: &[Signer],
231) -> ProgramResult {
232 validate_cpi_accounts(instruction, &account_views[..])?;
233
234 // Build CpiAccount array on the stack.
235 let mut cpi_accounts: [MaybeUninit<CpiAccount>; ACCOUNTS] =
236 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
237 unsafe { MaybeUninit::uninit().assume_init() };
238
239 let mut i = 0;
240 while i < ACCOUNTS {
241 cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(account_views[i]));
242 i += 1;
243 }
244
245 // SAFETY: All ACCOUNTS slots are now initialized.
246 let accounts: &[CpiAccount; ACCOUNTS] =
247 unsafe { &*(cpi_accounts.as_ptr() as *const [CpiAccount; ACCOUNTS]) };
248
249 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
250 unsafe {
251 if signers_seeds.is_empty() {
252 invoke_unchecked(instruction, accounts.as_slice())
253 } else {
254 invoke_signed_unchecked(instruction, accounts.as_slice(), signers_seeds)
255 }
256 }
257}
258
259/// Invoke with a dynamic number of accounts (bounded by const generic).
260#[inline]
261pub fn invoke_with_bounds<const MAX_ACCOUNTS: usize>(
262 instruction: &InstructionView,
263 account_views: &[&AccountView],
264) -> ProgramResult {
265 invoke_signed_with_bounds::<MAX_ACCOUNTS>(instruction, account_views, &[])
266}
267
268/// Signed invoke with a dynamic number of accounts (bounded by const generic).
269///
270/// Returns `Err(InvalidArgument)` if `account_views.len() > MAX_ACCOUNTS`.
271/// Validates accounts before invoking.
272#[inline]
273pub fn invoke_signed_with_bounds<const MAX_ACCOUNTS: usize>(
274 instruction: &InstructionView,
275 account_views: &[&AccountView],
276 signers_seeds: &[Signer],
277) -> ProgramResult {
278 if account_views.len() > MAX_ACCOUNTS {
279 return Err(ProgramError::InvalidArgument);
280 }
281
282 validate_cpi_accounts(instruction, account_views)?;
283
284 let mut cpi_accounts: [MaybeUninit<CpiAccount>; MAX_ACCOUNTS] =
285 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
286 unsafe { MaybeUninit::uninit().assume_init() };
287
288 let count = account_views.len();
289 let mut i = 0;
290 while i < count {
291 cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(account_views[i]));
292 i += 1;
293 }
294
295 // SAFETY: first `count` slots are initialized.
296 let accounts =
297 unsafe { core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount, count) };
298
299 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
300 unsafe {
301 if signers_seeds.is_empty() {
302 invoke_unchecked(instruction, accounts)
303 } else {
304 invoke_signed_unchecked(instruction, accounts, signers_seeds)
305 }
306 }
307}
308
309// ── Return data ──────────────────────────────────────────────────────
310
311/// Set return data for the current instruction.
312#[inline(always)]
313pub fn set_return_data(data: &[u8]) {
314 #[cfg(target_os = "solana")]
315 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
316 unsafe {
317 crate::syscalls::sol_set_return_data(data.as_ptr(), data.len() as u64);
318 }
319 #[cfg(not(target_os = "solana"))]
320 {
321 let _ = data;
322 }
323}