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(
106        accounts,
107        writable_mask,
108        signer_mask,
109        !signers_seeds.is_empty(),
110    )?;
111    let instruction_accounts =
112        specialized_instruction_accounts(accounts, writable_mask, signer_mask);
113    let instruction = InstructionView {
114        program_id,
115        data,
116        accounts: &instruction_accounts,
117    };
118
119    // SAFETY: the protocol masks above produced exact metas; preflight checked
120    // outer writable privileges and every account's borrow compatibility.
121    // The signed form with an empty seed list is the unsigned invoke (the
122    // syscall reads the seed pointer only when the count is nonzero), so one
123    // syscall site serves both instead of the two copies a branch compiled to.
124    unsafe { invoke_signed_unchecked(&instruction, accounts, signers_seeds) }
125}
126
127// ---------------------------------------------------------------------
128
129/// Invoke a CPI without the local borrow/privilege preflight.
130/// Prefer [`invoke`] unless the caller can uphold the unchecked contract.
131///
132/// # Safety
133///
134/// Every descriptor and referenced byte range must remain valid for the call.
135/// Accounts must come from the current invocation and describe their current
136/// data lengths. No exclusive borrow may overlap a referenced account, and no
137/// shared borrow may overlap an account writable by the callee. Duplicate metas
138/// must obey the same aliasing rules for their shared underlying account.
139///
140/// Solana still checks privileges, program identity, and account permissions.
141/// Those runtime checks do not establish Rust borrow safety. Malformed raw
142/// pointers or conflicting live borrows can violate Rust's memory invariants;
143/// a missing signature is a runtime authorization error, not itself Rust UB.
144#[inline]
145pub unsafe fn invoke_unchecked(
146    instruction: &InstructionView<'_, '_, '_, '_>,
147    accounts: &[CpiAccount<'_>],
148) -> ProgramResult {
149    #[cfg(target_os = "solana")]
150    {
151        let c_instruction = CInstruction::from_view(instruction);
152        // Prevent LLVM from moving stack descriptor/meta initialization below
153        // the opaque runtime call.
154        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
155        // SAFETY: the caller upholds the unchecked CPI contract and
156        // `c_instruction` is the exact repr(C) syscall descriptor.
157        let result = unsafe {
158            crate::syscalls::sol_invoke_signed_c(
159                &c_instruction as *const _ as *const u8,
160                accounts.as_ptr() as *const u8,
161                accounts.len() as u64,
162                core::ptr::null(),
163                0,
164            )
165        };
166        if result == 0 {
167            Ok(())
168        } else {
169            Err(ProgramError::from(result))
170        }
171    }
172    #[cfg(not(target_os = "solana"))]
173    {
174        #[cfg(test)]
175        LAST_HOST_ACCOUNT_INFOS_LEN.store(accounts.len(), core::sync::atomic::Ordering::SeqCst);
176        let _ = (instruction, accounts);
177        Ok(())
178    }
179}
180
181/// Invoke a signed CPI without the local borrow/privilege preflight.
182///
183/// # Safety
184///
185/// The memory and aliasing contract of [`invoke_unchecked`] applies. Every
186/// signer descriptor and seed byte slice must also remain valid for the call.
187/// The SVM derives PDA signer privileges using the calling program ID. Supplying
188/// seeds does not by itself authenticate a requested signer or prove canonicality.
189#[inline]
190pub unsafe fn invoke_signed_unchecked(
191    instruction: &InstructionView<'_, '_, '_, '_>,
192    accounts: &[CpiAccount<'_>],
193    signers_seeds: &[Signer<'_, '_>],
194) -> ProgramResult {
195    #[cfg(target_os = "solana")]
196    {
197        let c_instruction = CInstruction::from_view(instruction);
198        // Keep every stack-backed descriptor, meta, account and signer seed
199        // fully materialized before the opaque syscall boundary.
200        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
201        // SAFETY: the caller upholds the unchecked CPI contract and
202        // `c_instruction` is the exact repr(C) syscall descriptor.
203        let result = unsafe {
204            crate::syscalls::sol_invoke_signed_c(
205                &c_instruction as *const _ as *const u8,
206                accounts.as_ptr() as *const u8,
207                accounts.len() as u64,
208                signers_seeds.as_ptr() as *const u8,
209                signers_seeds.len() as u64,
210            )
211        };
212        if result == 0 {
213            Ok(())
214        } else {
215            Err(ProgramError::from(result))
216        }
217    }
218    #[cfg(not(target_os = "solana"))]
219    {
220        #[cfg(test)]
221        LAST_HOST_ACCOUNT_INFOS_LEN.store(accounts.len(), core::sync::atomic::Ordering::SeqCst);
222        let _ = (instruction, accounts, signers_seeds);
223        Ok(())
224    }
225}
226
227// ---------------------------------------------------------------------
228
229/// Invoke a CPI with full validation.
230///
231/// Validates account count, address identity, Signer<'_, '_>/writable requirements,
232/// and borrow compatibility before calling the runtime.
233#[inline]
234pub fn invoke<const ACCOUNTS: usize>(
235    instruction: &InstructionView<'_, '_, '_, '_>,
236    account_views: &[&AccountView<'_>; ACCOUNTS],
237) -> ProgramResult {
238    invoke_signed::<ACCOUNTS>(instruction, account_views, &[])
239}
240
241/// Invoke a signed CPI with full validation.
242///
243/// Validates account count, address identity, Signer<'_, '_>/writable requirements,
244/// and borrow compatibility before calling the runtime.
245#[inline]
246pub fn invoke_signed<const ACCOUNTS: usize>(
247    instruction: &InstructionView<'_, '_, '_, '_>,
248    account_views: &[&AccountView<'_>; ACCOUNTS],
249    signers_seeds: &[Signer<'_, '_>],
250) -> ProgramResult {
251    let metas_len = instruction.accounts.len();
252    if ACCOUNTS < metas_len {
253        return Err(ProgramError::NotEnoughAccountKeys);
254    }
255
256    // Fused validate+build: one pass over the instruction metas performs the
257    // address/signer/writable/borrow checks and materializes exactly the
258    // matching CpiAccount prefix. Extra caller views are neither validated nor
259    // forwarded because the callee instruction cannot access them.
260    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; ACCOUNTS] =
261        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
262        // state; the initialized prefix is sliced to `metas_len` below, and on
263        // an early return the array is discarded unread (`CpiAccount` is
264        // `Copy`, so no drop runs).
265        unsafe { MaybeUninit::uninit().assume_init() };
266
267    let mut i = 0;
268    while i < metas_len {
269        let actual = account_views[i];
270        let expected = &instruction.accounts[i];
271
272        if !address_eq(actual.address(), expected.address) {
273            return Err(ProgramError::InvalidAccountData);
274        }
275
276        // Non-empty signer seeds may grant this instruction-only signer
277        // privilege to a PDA. The SVM remains the derivation authority.
278        if expected.is_signer && !actual.is_signer() && signers_seeds.is_empty() {
279            return Err(ProgramError::MissingRequiredSignature);
280        }
281
282        if expected.is_writable && !actual.is_writable() {
283            return Err(ProgramError::Immutable);
284        }
285
286        // Borrow compatibility: writable needs exclusive access,
287        // read-only needs at least shared access.
288        if expected.is_writable {
289            actual.check_borrow_mut()?;
290        } else {
291            actual.check_borrow()?;
292        }
293        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(actual));
294        i += 1;
295    }
296
297    // SAFETY: exactly the first `metas_len` slots are initialized. Extra
298    // caller views are absent from the instruction and are not forwarded.
299    let accounts = unsafe {
300        core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount<'_>, metas_len)
301    };
302
303    // 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.
304    unsafe {
305        if signers_seeds.is_empty() {
306            invoke_unchecked(instruction, accounts)
307        } else {
308            invoke_signed_unchecked(instruction, accounts, signers_seeds)
309        }
310    }
311}
312
313/// Invoke with a dynamic number of accounts (bounded by const generic).
314#[inline]
315pub fn invoke_with_bounds<const MAX_ACCOUNTS: usize>(
316    instruction: &InstructionView<'_, '_, '_, '_>,
317    account_views: &[&AccountView<'_>],
318) -> ProgramResult {
319    invoke_signed_with_bounds::<MAX_ACCOUNTS>(instruction, account_views, &[])
320}
321
322/// Signed invoke with a dynamic number of accounts (bounded by const generic).
323///
324/// Returns `Err(InvalidArgument)` if `account_views.len() > MAX_ACCOUNTS`.
325/// Validates accounts before invoking.
326#[inline]
327pub fn invoke_signed_with_bounds<const MAX_ACCOUNTS: usize>(
328    instruction: &InstructionView<'_, '_, '_, '_>,
329    account_views: &[&AccountView<'_>],
330    signers_seeds: &[Signer<'_, '_>],
331) -> ProgramResult {
332    if account_views.len() > MAX_ACCOUNTS {
333        return Err(ProgramError::InvalidArgument);
334    }
335
336    let metas_len = instruction.accounts.len();
337    let count = account_views.len();
338    if count < metas_len {
339        return Err(ProgramError::NotEnoughAccountKeys);
340    }
341
342    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; MAX_ACCOUNTS] =
343        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
344        // state; the first `metas_len` slots are written before being read
345        // below, and on an early return the array is discarded unread
346        // (`CpiAccount` is `Copy`, so no drop runs).
347        unsafe { MaybeUninit::uninit().assume_init() };
348
349    // Fused validate+build (see `invoke_signed`): one pass validates each
350    // instruction meta and writes only its matching scratch slot.
351    let mut i = 0;
352    while i < metas_len {
353        let actual = account_views[i];
354        let expected = &instruction.accounts[i];
355
356        if !address_eq(actual.address(), expected.address) {
357            return Err(ProgramError::InvalidAccountData);
358        }
359
360        if expected.is_signer && !actual.is_signer() && signers_seeds.is_empty() {
361            return Err(ProgramError::MissingRequiredSignature);
362        }
363
364        if expected.is_writable && !actual.is_writable() {
365            return Err(ProgramError::Immutable);
366        }
367
368        if expected.is_writable {
369            actual.check_borrow_mut()?;
370        } else {
371            actual.check_borrow()?;
372        }
373        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(actual));
374        i += 1;
375    }
376
377    // SAFETY: first `metas_len` slots are initialized; extra caller views are
378    // not part of the instruction and are not forwarded to the syscall.
379    let accounts = unsafe {
380        core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount<'_>, metas_len)
381    };
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        if signers_seeds.is_empty() {
386            invoke_unchecked(instruction, accounts)
387        } else {
388            invoke_signed_unchecked(instruction, accounts, signers_seeds)
389        }
390    }
391}
392
393// ---------------------------------------------------------------------
394
395/// Set return data for the current instruction.
396#[inline(always)]
397pub fn set_return_data(data: &[u8]) {
398    #[cfg(target_os = "solana")]
399    // 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.
400    unsafe {
401        crate::syscalls::sol_set_return_data(data.as_ptr(), data.len() as u64);
402    }
403    #[cfg(not(target_os = "solana"))]
404    {
405        let _ = data;
406    }
407}
408
409#[cfg(test)]
410mod abi_tests {
411    use super::*;
412    use crate::instruction::Seed;
413    use crate::{RuntimeAccount, NOT_BORROWED};
414
415    #[repr(C)]
416    struct Backing {
417        header: RuntimeAccount,
418        data: [u8; 8],
419    }
420
421    fn backing(tag: u8) -> Backing {
422        Backing {
423            header: RuntimeAccount {
424                borrow_state: NOT_BORROWED,
425                is_signer: 0,
426                is_writable: 1,
427                executable: 0,
428                resize_delta: 8,
429                address: Address::new_from_array([tag; 32]),
430                owner: Address::new_from_array([0xA5; 32]),
431                lamports: 5,
432                data_len: 8,
433            },
434            data: [tag; 8],
435        }
436    }
437
438    #[test]
439    fn c_instruction_and_specialized_meta_encoding_match_the_c_abi() {
440        let program_id = Address::new_from_array([9; 32]);
441        let key = Address::new_from_array([3; 32]);
442        let data = [1, 2, 3];
443        let metas = [InstructionAccount::new(&key, true, false)];
444        let view = InstructionView {
445            program_id: &program_id,
446            data: &data,
447            accounts: &metas,
448        };
449        let c = CInstruction::from_view(&view);
450        assert_eq!(c.program_id, &program_id as *const Address);
451        assert_eq!(c.accounts, metas.as_ptr() as *const u8);
452        assert_eq!(c.accounts_len, 1);
453        assert_eq!(c.data, data.as_ptr());
454        assert_eq!(c.data_len, 3);
455
456        let mut first_backing = backing(1);
457        let mut second_backing = backing(2);
458        let mut third_backing = backing(3);
459        // SAFETY: each backing is an owned, aligned RuntimeAccount header
460        // followed by its data, and outlives the view built over it; the three
461        // backings are distinct allocations, so the views never alias.
462        let first = unsafe { AccountView::new_unchecked(&mut first_backing.header) };
463        // SAFETY: same contract as `first`, over its own backing.
464        let second = unsafe { AccountView::new_unchecked(&mut second_backing.header) };
465        // SAFETY: same contract as `first`, over its own backing.
466        let third = unsafe { AccountView::new_unchecked(&mut third_backing.header) };
467        let infos = [
468            CpiAccount::from(&first),
469            CpiAccount::from(&second),
470            CpiAccount::from(&third),
471        ];
472        let encoded = specialized_instruction_accounts(&infos, 0b011, 0b100);
473        assert!(encoded[0].is_writable);
474        assert!(encoded[1].is_writable);
475        assert!(!encoded[2].is_writable);
476        assert!(!encoded[0].is_signer);
477        assert!(!encoded[1].is_signer);
478        assert!(encoded[2].is_signer);
479        assert_eq!(encoded[0].address, first.address());
480        assert_eq!(encoded[1].address, second.address());
481        assert_eq!(encoded[2].address, third.address());
482    }
483
484    #[test]
485    fn checked_paths_accept_pda_seeds_and_forward_only_instruction_metas() {
486        let mut signer_backing = backing(4);
487        let mut extra_backing = backing(5);
488        // SAFETY: both backings are owned, aligned RuntimeAccount headers with
489        // their data, distinct from each other, and outlive the views.
490        let signer_view = unsafe { AccountView::new_unchecked(&mut signer_backing.header) };
491        let extra_view = unsafe { AccountView::new_unchecked(&mut extra_backing.header) };
492        let metas = [InstructionAccount::readonly_signer(signer_view.address())];
493        let program_id = Address::new_from_array([6; 32]);
494        let instruction = InstructionView {
495            program_id: &program_id,
496            data: &[],
497            accounts: &metas,
498        };
499
500        LAST_HOST_ACCOUNT_INFOS_LEN.store(usize::MAX, core::sync::atomic::Ordering::SeqCst);
501        assert_eq!(
502            invoke::<1>(&instruction, &[&signer_view]),
503            Err(ProgramError::MissingRequiredSignature)
504        );
505        assert_eq!(
506            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
507            usize::MAX,
508            "unsigned signer failure must happen before invoke"
509        );
510
511        let seed = Seed::from(&b"pda"[..]);
512        let signer_seeds = [seed];
513        let signers = [Signer::from(&signer_seeds)];
514        invoke_signed::<1>(&instruction, &[&signer_view], &signers).unwrap();
515        assert_eq!(
516            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
517            1
518        );
519
520        let caller_views = [&signer_view, &extra_view];
521        invoke_signed::<2>(&instruction, &caller_views, &signers).unwrap();
522        assert_eq!(
523            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
524            1,
525            "fixed path must not forward caller views absent from metas"
526        );
527        invoke_signed_with_bounds::<2>(&instruction, &caller_views, &signers).unwrap();
528        assert_eq!(
529            LAST_HOST_ACCOUNT_INFOS_LEN.load(core::sync::atomic::Ordering::SeqCst),
530            1,
531            "bounded path must not forward caller views absent from metas"
532        );
533    }
534}