Skip to main content

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}