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, Address};
7use crate::error::ProgramError;
8use crate::instruction::{
9    preflight_cpi_accounts, CpiAccount, InstructionAccount, InstructionView, Signer,
10};
11use crate::ProgramResult;
12use core::mem::MaybeUninit;
13
14#[cfg(all(test, not(target_os = "solana")))]
15static LAST_HOST_ACCOUNT_INFOS_LEN: core::sync::atomic::AtomicUsize =
16    core::sync::atomic::AtomicUsize::new(usize::MAX);
17
18/// Default stack-sized ceiling for a *static* CPI call.
19///
20/// This is deliberately the low pre-SIMD-0339 value: it sizes the
21/// `MaybeUninit` account/meta scratch arrays that some fixed-shape CPI
22/// helpers stack-allocate, and every SBF call frame is only 4 KiB. Raising
23/// it would grow those arrays for *every* program, including the vast
24/// majority that never approach 64 accounts. Callers that genuinely need a
25/// wider CPI opt in per-call through the const-generic `MAX_ACCOUNTS`
26/// parameter (up to [`MAX_CPI_ACCOUNTS`]), which costs nothing when unused.
27pub const MAX_STATIC_CPI_ACCOUNTS: usize = 64;
28
29/// Hard ceiling on the number of account-infos in any single CPI.
30///
31/// Raised to 255 for **SIMD-0339** (`increase_cpi_account_info_limit`; active
32/// on mainnet at slot 403,056,000), which lifts the runtime CPI account-info limit
33/// from 64 to 255. This is a *ceiling* only; it does not size any array, so
34/// widening it does not cost stack for programs that stay small. A per-call
35/// const-generic `MAX_ACCOUNTS` still governs the actual scratch allocation.
36pub const MAX_CPI_ACCOUNTS: usize = 255;
37
38/// Maximum return data size (1 KiB).
39pub const MAX_RETURN_DATA: usize = 1024;
40
41/// Exact C instruction descriptor consumed by `sol_invoke_signed_c`.
42///
43/// `InstructionView` contains Rust fat slices and has a different field order;
44/// it must never be passed to the syscall directly.
45#[cfg(any(target_os = "solana", test))]
46#[repr(C)]
47struct CInstruction {
48    program_id: *const Address,
49    accounts: *const u8,
50    accounts_len: u64,
51    data: *const u8,
52    data_len: u64,
53}
54
55#[cfg(any(target_os = "solana", test))]
56impl CInstruction {
57    #[inline(always)]
58    fn from_view(instruction: &InstructionView<'_, '_, '_, '_>) -> Self {
59        Self {
60            program_id: instruction.program_id as *const Address,
61            accounts: instruction.accounts.as_ptr() as *const u8,
62            accounts_len: instruction.accounts.len() as u64,
63            data: instruction.data.as_ptr(),
64            data_len: instruction.data.len() as u64,
65        }
66    }
67}
68
69#[cfg(any(target_os = "solana", test))]
70const _: () = {
71    assert!(core::mem::size_of::<CInstruction>() == 40);
72    assert!(core::mem::align_of::<CInstruction>() == 8);
73    assert!(core::mem::offset_of!(CInstruction, program_id) == 0);
74    assert!(core::mem::offset_of!(CInstruction, accounts) == 8);
75    assert!(core::mem::offset_of!(CInstruction, accounts_len) == 16);
76    assert!(core::mem::offset_of!(CInstruction, data) == 24);
77    assert!(core::mem::offset_of!(CInstruction, data_len) == 32);
78};
79
80#[inline(always)]
81fn specialized_instruction_accounts<'a, const ACCOUNTS: usize>(
82    accounts: &[CpiAccount<'a>; ACCOUNTS],
83    writable_mask: usize,
84    signer_mask: usize,
85) -> [InstructionAccount<'a>; ACCOUNTS] {
86    core::array::from_fn(|index| {
87        accounts[index].instruction_account(
88            writable_mask & (1usize << index) != 0,
89            signer_mask & (1usize << index) != 0,
90        )
91    })
92}
93
94/// Invoke a fixed-shape specialized builder through the same checked C-ABI
95/// boundary as the generic CPI surface.
96#[inline]
97pub(crate) fn invoke_specialized_signed<'a, const ACCOUNTS: usize>(
98    program_id: &Address,
99    data: &[u8],
100    accounts: &[CpiAccount<'a>; ACCOUNTS],
101    writable_mask: usize,
102    signer_mask: usize,
103    signers_seeds: &[Signer<'_, '_>],
104) -> ProgramResult {
105    preflight_cpi_accounts(accounts, writable_mask)?;
106    let instruction_accounts =
107        specialized_instruction_accounts(accounts, writable_mask, signer_mask);
108    let instruction = InstructionView {
109        program_id,
110        data,
111        accounts: &instruction_accounts,
112    };
113
114    // SAFETY: the protocol masks above produced exact metas; preflight checked
115    // outer writable privileges and every account's borrow compatibility.
116    // The signed form with an empty seed list is the unsigned invoke (the
117    // syscall reads the seed pointer only when the count is nonzero), so one
118    // syscall site serves both instead of the two copies a branch compiled to.
119    unsafe { invoke_signed_unchecked(&instruction, accounts, signers_seeds) }
120}
121
122// ---------------------------------------------------------------------
123
124/// Invoke a CPI without borrow validation (lowest CU cost).
125///
126/// This is Tier C of the CPI surface. The checked variant
127/// ([`invoke`]) enforces the full contract below
128/// before calling this function; prefer that unless you have measured
129/// a reason to bypass the validation pass.
130///
131/// # Safety
132///
133/// The caller must uphold every one of the following invariants. A
134/// violation of any of them is undefined behaviour, because the Solana
135/// runtime's `sol_invoke_signed_c` syscall assumes they already hold.
136///
137/// 1. **No aliasing borrows.** No `&` or `&mut` references into any
138///    account data region referenced by `accounts` may be live for
139///    the duration of the call. The CPI can (and will) mutate those
140///    regions via the callee, and Rust's aliasing rules do not permit
141///    the caller to hold outstanding references to memory that is
142///    about to change under it.
143/// 2. **Account list consistency.** Every `CpiAccount<'_>` in `accounts`
144///    must correspond to a real account previously passed to the
145///    program's entrypoint (same address, same `is_Signer<'_, '_>` /
146///    `is_writable` flags the runtime already knows about). The
147///    runtime will not re-derive account permissions; invalid flags
148///    propagate into the callee.
149/// 3. **Writability coverage.** Every account that the `instruction`
150///    marks writable must have `is_writable = true` in `accounts`,
151///    and every account the instruction marks as Signer<'_, '_> must have
152///    `is_Signer<'_, '_> = true`. Mismatches are rejected by the runtime but
153///    the rejection path is not cheap and the caller is expected to
154///    get this right.
155/// 4. **No shared mutable slices across CPIs.** If the same account
156///    appears more than once in `accounts` (duplicate accounts), the
157///    caller is responsible for ensuring that any subsequent borrow
158///    of that account's data respects the CPI's writes.
159/// 5. **Valid instruction encoding.** `instruction.program_id`,
160///    `instruction.accounts`, and `instruction.data` must all point
161///    to valid memory for the duration of the call. An
162///    `InstructionView<'_, '_, '_, '_>` built from a local `InstructionAccount` slice
163///    is fine; one built from a dropped stack slot is not.
164///
165/// The runtime does not enforce any of these from the caller side -
166/// it assumes a well-formed CPI. That is the cost of the Tier C path.
167#[inline]
168pub unsafe fn invoke_unchecked(
169    instruction: &InstructionView<'_, '_, '_, '_>,
170    accounts: &[CpiAccount<'_>],
171) -> ProgramResult {
172    #[cfg(target_os = "solana")]
173    {
174        let c_instruction = CInstruction::from_view(instruction);
175        // Prevent LLVM from moving stack descriptor/meta initialization below
176        // the opaque runtime call.
177        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
178        // SAFETY: the caller upholds the unchecked CPI contract and
179        // `c_instruction` is the exact repr(C) syscall descriptor.
180        let result = unsafe {
181            crate::syscalls::sol_invoke_signed_c(
182                &c_instruction as *const _ as *const u8,
183                accounts.as_ptr() as *const u8,
184                accounts.len() as u64,
185                core::ptr::null(),
186                0,
187            )
188        };
189        if result == 0 {
190            Ok(())
191        } else {
192            Err(ProgramError::from(result))
193        }
194    }
195    #[cfg(not(target_os = "solana"))]
196    {
197        #[cfg(test)]
198        LAST_HOST_ACCOUNT_INFOS_LEN.store(accounts.len(), core::sync::atomic::Ordering::SeqCst);
199        let _ = (instruction, accounts);
200        Ok(())
201    }
202}
203
204/// Invoke a signed CPI without borrow validation.
205///
206/// Same as [`invoke_unchecked`] but also passes PDA Signer<'_, '_> seeds so
207/// the callee can accept writes that would otherwise require a
208/// signature.
209///
210/// # Safety
211///
212/// All of [`invoke_unchecked`]'s invariants apply, plus two more for
213/// the Signer<'_, '_>-seeds path:
214///
215/// 6. **Signer<'_, '_> seeds must derive the claimed PDA.** For every
216///    `Signer<'_, '_>` in `signers_seeds`, the derived address
217///    (sha256 of `seeds || program_id || PDA_MARKER`) must equal an
218///    address in `accounts` that is marked as Signer<'_, '_>. A mismatch will
219///    cause the runtime to reject the CPI, but the caller is expected
220///    to have verified this before reaching the Tier C path.
221/// 7. **Seed lifetime.** `signers_seeds` (and every `&[u8]` it points
222///    at) must outlive the call. Temporary seed slices built inside a
223///    function frame are fine; seeds referencing dropped storage are
224///    not.
225///
226/// For the happy path the caller should hold a `CpiValidator` or
227/// equivalent proof-object constructed by the checked path and let
228/// that drive both this function's inputs and the aliasing discipline
229/// required above.
230#[inline]
231pub unsafe fn invoke_signed_unchecked(
232    instruction: &InstructionView<'_, '_, '_, '_>,
233    accounts: &[CpiAccount<'_>],
234    signers_seeds: &[Signer<'_, '_>],
235) -> ProgramResult {
236    #[cfg(target_os = "solana")]
237    {
238        let c_instruction = CInstruction::from_view(instruction);
239        // Keep every stack-backed descriptor, meta, account and signer seed
240        // fully materialized before the opaque syscall boundary.
241        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
242        // SAFETY: the caller upholds the unchecked CPI contract and
243        // `c_instruction` is the exact repr(C) syscall descriptor.
244        let result = unsafe {
245            crate::syscalls::sol_invoke_signed_c(
246                &c_instruction as *const _ as *const u8,
247                accounts.as_ptr() as *const u8,
248                accounts.len() as u64,
249                signers_seeds.as_ptr() as *const u8,
250                signers_seeds.len() as u64,
251            )
252        };
253        if result == 0 {
254            Ok(())
255        } else {
256            Err(ProgramError::from(result))
257        }
258    }
259    #[cfg(not(target_os = "solana"))]
260    {
261        #[cfg(test)]
262        LAST_HOST_ACCOUNT_INFOS_LEN.store(accounts.len(), core::sync::atomic::Ordering::SeqCst);
263        let _ = (instruction, accounts, signers_seeds);
264        Ok(())
265    }
266}
267
268// ---------------------------------------------------------------------
269
270/// Invoke a CPI with full validation.
271///
272/// Validates account count, address identity, Signer<'_, '_>/writable requirements,
273/// and borrow compatibility before calling the runtime.
274#[inline]
275pub fn invoke<const ACCOUNTS: usize>(
276    instruction: &InstructionView<'_, '_, '_, '_>,
277    account_views: &[&AccountView<'_>; ACCOUNTS],
278) -> ProgramResult {
279    invoke_signed::<ACCOUNTS>(instruction, account_views, &[])
280}
281
282/// Invoke a signed CPI with full validation.
283///
284/// Validates account count, address identity, Signer<'_, '_>/writable requirements,
285/// and borrow compatibility before calling the runtime.
286#[inline]
287pub fn invoke_signed<const ACCOUNTS: usize>(
288    instruction: &InstructionView<'_, '_, '_, '_>,
289    account_views: &[&AccountView<'_>; ACCOUNTS],
290    signers_seeds: &[Signer<'_, '_>],
291) -> ProgramResult {
292    let metas_len = instruction.accounts.len();
293    if ACCOUNTS < metas_len {
294        return Err(ProgramError::NotEnoughAccountKeys);
295    }
296
297    // Fused validate+build: one pass over the instruction metas performs the
298    // address/signer/writable/borrow checks and materializes exactly the
299    // matching CpiAccount prefix. Extra caller views are neither validated nor
300    // forwarded because the callee instruction cannot access them.
301    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; ACCOUNTS] =
302        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
303        // state; the initialized prefix is sliced to `metas_len` below, and on
304        // an early return the array is discarded unread (`CpiAccount` is
305        // `Copy`, so no drop runs).
306        unsafe { MaybeUninit::uninit().assume_init() };
307
308    let mut i = 0;
309    while i < metas_len {
310        let actual = account_views[i];
311        let expected = &instruction.accounts[i];
312
313        if !address_eq(actual.address(), expected.address) {
314            return Err(ProgramError::InvalidAccountData);
315        }
316
317        // Non-empty signer seeds may grant this instruction-only signer
318        // privilege to a PDA. The SVM remains the derivation authority.
319        if expected.is_signer && !actual.is_signer() && signers_seeds.is_empty() {
320            return Err(ProgramError::MissingRequiredSignature);
321        }
322
323        if expected.is_writable && !actual.is_writable() {
324            return Err(ProgramError::Immutable);
325        }
326
327        // Borrow compatibility: writable needs exclusive access,
328        // read-only needs at least shared access.
329        if expected.is_writable {
330            actual.check_borrow_mut()?;
331        } else {
332            actual.check_borrow()?;
333        }
334        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(actual));
335        i += 1;
336    }
337
338    // SAFETY: exactly the first `metas_len` slots are initialized. Extra
339    // caller views are absent from the instruction and are not forwarded.
340    let accounts = unsafe {
341        core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount<'_>, metas_len)
342    };
343
344    // 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.
345    unsafe {
346        if signers_seeds.is_empty() {
347            invoke_unchecked(instruction, accounts)
348        } else {
349            invoke_signed_unchecked(instruction, accounts, signers_seeds)
350        }
351    }
352}
353
354/// Invoke with a dynamic number of accounts (bounded by const generic).
355#[inline]
356pub fn invoke_with_bounds<const MAX_ACCOUNTS: usize>(
357    instruction: &InstructionView<'_, '_, '_, '_>,
358    account_views: &[&AccountView<'_>],
359) -> ProgramResult {
360    invoke_signed_with_bounds::<MAX_ACCOUNTS>(instruction, account_views, &[])
361}
362
363/// Signed invoke with a dynamic number of accounts (bounded by const generic).
364///
365/// Returns `Err(InvalidArgument)` if `account_views.len() > MAX_ACCOUNTS`.
366/// Validates accounts before invoking.
367#[inline]
368pub fn invoke_signed_with_bounds<const MAX_ACCOUNTS: usize>(
369    instruction: &InstructionView<'_, '_, '_, '_>,
370    account_views: &[&AccountView<'_>],
371    signers_seeds: &[Signer<'_, '_>],
372) -> ProgramResult {
373    if account_views.len() > MAX_ACCOUNTS {
374        return Err(ProgramError::InvalidArgument);
375    }
376
377    let metas_len = instruction.accounts.len();
378    let count = account_views.len();
379    if count < metas_len {
380        return Err(ProgramError::NotEnoughAccountKeys);
381    }
382
383    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; MAX_ACCOUNTS] =
384        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
385        // state; the first `metas_len` slots are written before being read
386        // below, and on an early return the array is discarded unread
387        // (`CpiAccount` is `Copy`, so no drop runs).
388        unsafe { MaybeUninit::uninit().assume_init() };
389
390    // Fused validate+build (see `invoke_signed`): one pass validates each
391    // instruction meta and writes only its matching scratch slot.
392    let mut i = 0;
393    while i < metas_len {
394        let actual = account_views[i];
395        let expected = &instruction.accounts[i];
396
397        if !address_eq(actual.address(), expected.address) {
398            return Err(ProgramError::InvalidAccountData);
399        }
400
401        if expected.is_signer && !actual.is_signer() && signers_seeds.is_empty() {
402            return Err(ProgramError::MissingRequiredSignature);
403        }
404
405        if expected.is_writable && !actual.is_writable() {
406            return Err(ProgramError::Immutable);
407        }
408
409        if expected.is_writable {
410            actual.check_borrow_mut()?;
411        } else {
412            actual.check_borrow()?;
413        }
414        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(actual));
415        i += 1;
416    }
417
418    // SAFETY: first `metas_len` slots are initialized; extra caller views are
419    // not part of the instruction and are not forwarded to the syscall.
420    let accounts = unsafe {
421        core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount<'_>, metas_len)
422    };
423
424    // 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.
425    unsafe {
426        if signers_seeds.is_empty() {
427            invoke_unchecked(instruction, accounts)
428        } else {
429            invoke_signed_unchecked(instruction, accounts, signers_seeds)
430        }
431    }
432}
433
434// ---------------------------------------------------------------------
435
436/// Set return data for the current instruction.
437#[inline(always)]
438pub fn set_return_data(data: &[u8]) {
439    #[cfg(target_os = "solana")]
440    // 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.
441    unsafe {
442        crate::syscalls::sol_set_return_data(data.as_ptr(), data.len() as u64);
443    }
444    #[cfg(not(target_os = "solana"))]
445    {
446        let _ = data;
447    }
448}
449
450#[cfg(test)]
451mod abi_tests {
452    use super::*;
453    use crate::instruction::Seed;
454    use crate::{RuntimeAccount, NOT_BORROWED};
455
456    #[repr(C)]
457    struct Backing {
458        header: RuntimeAccount,
459        data: [u8; 8],
460    }
461
462    fn backing(tag: u8) -> Backing {
463        Backing {
464            header: RuntimeAccount {
465                borrow_state: NOT_BORROWED,
466                is_signer: 0,
467                is_writable: 1,
468                executable: 0,
469                resize_delta: 8,
470                address: Address::new_from_array([tag; 32]),
471                owner: Address::new_from_array([0xA5; 32]),
472                lamports: 5,
473                data_len: 8,
474            },
475            data: [tag; 8],
476        }
477    }
478
479    #[test]
480    fn c_instruction_and_specialized_meta_encoding_match_the_c_abi() {
481        let program_id = Address::new_from_array([9; 32]);
482        let key = Address::new_from_array([3; 32]);
483        let data = [1, 2, 3];
484        let metas = [InstructionAccount::new(&key, true, false)];
485        let view = InstructionView {
486            program_id: &program_id,
487            data: &data,
488            accounts: &metas,
489        };
490        let c = CInstruction::from_view(&view);
491        assert_eq!(c.program_id, &program_id as *const Address);
492        assert_eq!(c.accounts, metas.as_ptr() as *const u8);
493        assert_eq!(c.accounts_len, 1);
494        assert_eq!(c.data, data.as_ptr());
495        assert_eq!(c.data_len, 3);
496
497        let mut first_backing = backing(1);
498        let mut second_backing = backing(2);
499        let mut third_backing = backing(3);
500        // SAFETY: each backing is an owned, aligned RuntimeAccount header
501        // followed by its data, and outlives the view built over it; the three
502        // backings are distinct allocations, so the views never alias.
503        let first = unsafe { AccountView::new_unchecked(&mut first_backing.header) };
504        // SAFETY: same contract as `first`, over its own backing.
505        let second = unsafe { AccountView::new_unchecked(&mut second_backing.header) };
506        // SAFETY: same contract as `first`, over its own backing.
507        let third = unsafe { AccountView::new_unchecked(&mut third_backing.header) };
508        let infos = [
509            CpiAccount::from(&first),
510            CpiAccount::from(&second),
511            CpiAccount::from(&third),
512        ];
513        let encoded = specialized_instruction_accounts(&infos, 0b011, 0b100);
514        assert!(encoded[0].is_writable);
515        assert!(encoded[1].is_writable);
516        assert!(!encoded[2].is_writable);
517        assert!(!encoded[0].is_signer);
518        assert!(!encoded[1].is_signer);
519        assert!(encoded[2].is_signer);
520        assert_eq!(encoded[0].address, first.address());
521        assert_eq!(encoded[1].address, second.address());
522        assert_eq!(encoded[2].address, third.address());
523    }
524
525    #[test]
526    fn checked_paths_accept_pda_seeds_and_forward_only_instruction_metas() {
527        let mut signer_backing = backing(4);
528        let mut extra_backing = backing(5);
529        // SAFETY: both backings are owned, aligned RuntimeAccount headers with
530        // their data, distinct from each other, and outlive the views.
531        let signer_view = unsafe { AccountView::new_unchecked(&mut signer_backing.header) };
532        let extra_view = unsafe { AccountView::new_unchecked(&mut extra_backing.header) };
533        let metas = [InstructionAccount::readonly_signer(signer_view.address())];
534        let program_id = Address::new_from_array([6; 32]);
535        let instruction = InstructionView {
536            program_id: &program_id,
537            data: &[],
538            accounts: &metas,
539        };
540
541        LAST_HOST_ACCOUNT_INFOS_LEN.store(usize::MAX, core::sync::atomic::Ordering::SeqCst);
542        assert_eq!(
543            invoke::<1>(&instruction, &[&signer_view]),
544            Err(ProgramError::MissingRequiredSignature)
545        );
546        assert_eq!(
547            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
548            usize::MAX,
549            "unsigned signer failure must happen before invoke"
550        );
551
552        let seed = Seed::from(&b"pda"[..]);
553        let signer_seeds = [seed];
554        let signers = [Signer::from(&signer_seeds)];
555        invoke_signed::<1>(&instruction, &[&signer_view], &signers).unwrap();
556        assert_eq!(
557            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
558            1
559        );
560
561        let caller_views = [&signer_view, &extra_view];
562        invoke_signed::<2>(&instruction, &caller_views, &signers).unwrap();
563        assert_eq!(
564            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
565            1,
566            "fixed path must not forward caller views absent from metas"
567        );
568        invoke_signed_with_bounds::<2>(&instruction, &caller_views, &signers).unwrap();
569        assert_eq!(
570            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
571            1,
572            "bounded path must not forward caller views absent from metas"
573        );
574    }
575}