Skip to main content

hopper_runtime/
cpi.rs

1//! Cross-program invocation for Hopper programs.
2//!
3//! Provides both checked (borrow-validating) and unchecked invoke paths.
4//! hopper-native-backend uses direct runtime syscalls; compatibility
5//! backends delegate through `compat` after Hopper-level validation.
6
7use crate::address::{address_eq, Address};
8use crate::error::ProgramError;
9use crate::ProgramResult;
10use crate::instruction::InstructionView;
11use crate::account::AccountView;
12
13#[cfg(all(feature = "hopper-native-backend", target_os = "solana"))]
14use crate::instruction::InstructionAccount;
15
16// Re-export Signer and Seed so callers can use `cpi::Signer` / `cpi::Seed`.
17pub use crate::instruction::{Signer, Seed};
18
19/// Maximum number of accounts in a static CPI call.
20pub const MAX_STATIC_CPI_ACCOUNTS: usize = 64;
21
22/// Maximum number of accounts in any CPI call.
23pub const MAX_CPI_ACCOUNTS: usize = 128;
24
25/// Maximum return data size (1 KiB).
26pub const MAX_RETURN_DATA: usize = 1024;
27
28// ══════════════════════════════════════════════════════════════════════
29//  hopper-native-backend CPI
30// ══════════════════════════════════════════════════════════════════════
31
32#[cfg(feature = "hopper-native-backend")]
33use crate::instruction::CpiAccount;
34#[cfg(feature = "hopper-native-backend")]
35use core::mem::MaybeUninit;
36
37#[cfg(all(feature = "hopper-native-backend", target_os = "solana"))]
38#[repr(C)]
39struct CInstruction<'a> {
40    program_id: *const Address,
41    accounts: *const InstructionAccount<'a>,
42    accounts_len: u64,
43    data: *const u8,
44    data_len: u64,
45}
46
47// ── Unchecked invoke ─────────────────────────────────────────────────
48
49/// Invoke a CPI without borrow validation (lowest CU cost).
50///
51/// # Safety
52///
53/// The caller must ensure no account data borrows conflict with the CPI.
54#[cfg(feature = "hopper-native-backend")]
55#[inline]
56pub unsafe fn invoke_unchecked(
57    instruction: &InstructionView,
58    accounts: &[CpiAccount],
59) -> ProgramResult {
60    #[cfg(target_os = "solana")]
61    {
62        let c_instruction = CInstruction {
63            program_id: instruction.program_id as *const Address,
64            accounts: instruction.accounts.as_ptr(),
65            accounts_len: instruction.accounts.len() as u64,
66            data: instruction.data.as_ptr(),
67            data_len: instruction.data.len() as u64,
68        };
69
70        let result = unsafe {
71            hopper_native::syscalls::sol_invoke_signed_c(
72                &c_instruction as *const _ as *const u8,
73                accounts.as_ptr() as *const u8,
74                accounts.len() as u64,
75                core::ptr::null(),
76                0,
77            )
78        };
79        if result == 0 { Ok(()) } else { Err(ProgramError::from(result)) }
80    }
81    #[cfg(not(target_os = "solana"))]
82    {
83        let _ = (instruction, accounts);
84        Ok(())
85    }
86}
87
88/// Invoke a signed CPI without borrow validation.
89///
90/// # Safety
91///
92/// The caller must ensure no account data borrows conflict with the CPI.
93#[cfg(feature = "hopper-native-backend")]
94#[inline]
95pub unsafe fn invoke_signed_unchecked(
96    instruction: &InstructionView,
97    accounts: &[CpiAccount],
98    signers_seeds: &[Signer],
99) -> ProgramResult {
100    #[cfg(target_os = "solana")]
101    {
102        let c_instruction = CInstruction {
103            program_id: instruction.program_id as *const Address,
104            accounts: instruction.accounts.as_ptr(),
105            accounts_len: instruction.accounts.len() as u64,
106            data: instruction.data.as_ptr(),
107            data_len: instruction.data.len() as u64,
108        };
109
110        let result = unsafe {
111            hopper_native::syscalls::sol_invoke_signed_c(
112                &c_instruction as *const _ as *const u8,
113                accounts.as_ptr() as *const u8,
114                accounts.len() as u64,
115                signers_seeds.as_ptr() as *const u8,
116                signers_seeds.len() as u64,
117            )
118        };
119        if result == 0 { Ok(()) } else { Err(ProgramError::from(result)) }
120    }
121    #[cfg(not(target_os = "solana"))]
122    {
123        let _ = (instruction, accounts, signers_seeds);
124        Ok(())
125    }
126}
127
128// ── CPI validation ───────────────────────────────────────────────────
129
130/// Reject duplicate writable accounts before invoking CPI.
131#[inline]
132fn validate_no_duplicate_writable(
133    instruction: &InstructionView,
134    account_views: &[&AccountView],
135) -> ProgramResult {
136    let mut i = 0;
137    while i < instruction.accounts.len() {
138        if instruction.accounts[i].is_writable {
139            let mut j = i + 1;
140            while j < instruction.accounts.len() {
141                if instruction.accounts[j].is_writable
142                    && address_eq(account_views[i].address(), account_views[j].address())
143                {
144                    return Err(ProgramError::AccountBorrowFailed);
145                }
146                j += 1;
147            }
148        }
149        i += 1;
150    }
151    Ok(())
152}
153
154#[inline]
155fn signer_matches_pda(program_id: &Address, account: &Address, signers_seeds: &[Signer]) -> bool {
156    let mut i = 0;
157    while i < signers_seeds.len() {
158        let signer = &signers_seeds[i];
159        let seeds = unsafe {
160            core::slice::from_raw_parts(signer.seeds, signer.len as usize)
161        };
162
163        if seeds.len() <= crate::address::MAX_SEEDS {
164            let mut seed_refs: [&[u8]; crate::address::MAX_SEEDS] = [&[]; crate::address::MAX_SEEDS];
165            let mut j = 0;
166            while j < seeds.len() {
167                seed_refs[j] = unsafe {
168                    core::slice::from_raw_parts(seeds[j].seed, seeds[j].len as usize)
169                };
170                j += 1;
171            }
172
173            if let Ok(derived) = crate::compat::create_program_address(&seed_refs[..seeds.len()], program_id) {
174                if address_eq(&derived, account) {
175                    return true;
176                }
177            }
178        }
179
180        i += 1;
181    }
182
183    false
184}
185
186/// Validate CPI account views match the instruction's expectations.
187#[inline]
188fn validate_cpi_accounts(
189    instruction: &InstructionView,
190    account_views: &[&AccountView],
191    signers_seeds: &[Signer],
192) -> ProgramResult {
193    if account_views.len() < instruction.accounts.len() {
194        return Err(ProgramError::NotEnoughAccountKeys);
195    }
196
197    let mut i = 0;
198    while i < instruction.accounts.len() {
199        let expected = &instruction.accounts[i];
200        let actual = account_views[i];
201
202        if !address_eq(actual.address(), expected.address) {
203            return Err(ProgramError::InvalidAccountData);
204        }
205
206        if expected.is_signer
207            && !actual.is_signer()
208            && !signer_matches_pda(instruction.program_id, actual.address(), signers_seeds)
209        {
210            return Err(ProgramError::MissingRequiredSignature);
211        }
212
213        if expected.is_writable && !actual.is_writable() {
214            return Err(ProgramError::Immutable);
215        }
216
217        if expected.is_writable {
218            actual.check_borrow_mut()?;
219        } else {
220            actual.check_borrow()?;
221        }
222
223        i += 1;
224    }
225
226    validate_no_duplicate_writable(instruction, account_views)?;
227
228    Ok(())
229}
230
231// ── Checked invoke ───────────────────────────────────────────────────
232
233/// Invoke a CPI with full validation.
234#[cfg(feature = "hopper-native-backend")]
235#[inline]
236pub fn invoke<const ACCOUNTS: usize>(
237    instruction: &InstructionView,
238    account_views: &[&AccountView; ACCOUNTS],
239) -> ProgramResult {
240    invoke_signed::<ACCOUNTS>(instruction, account_views, &[])
241}
242
243/// Invoke a signed CPI with full validation.
244#[cfg(feature = "hopper-native-backend")]
245#[inline]
246pub fn invoke_signed<const ACCOUNTS: usize>(
247    instruction: &InstructionView,
248    account_views: &[&AccountView; ACCOUNTS],
249    signers_seeds: &[Signer],
250) -> ProgramResult {
251    validate_cpi_accounts(instruction, &account_views[..], signers_seeds)?;
252
253    let mut cpi_accounts: [MaybeUninit<CpiAccount>; ACCOUNTS] =
254        unsafe { MaybeUninit::uninit().assume_init() };
255
256    let mut i = 0;
257    while i < ACCOUNTS {
258        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(account_views[i]));
259        i += 1;
260    }
261
262    let accounts: &[CpiAccount; ACCOUNTS] = unsafe {
263        &*(cpi_accounts.as_ptr() as *const [CpiAccount; ACCOUNTS])
264    };
265
266    unsafe {
267        if signers_seeds.is_empty() {
268            invoke_unchecked(instruction, accounts.as_slice())
269        } else {
270            invoke_signed_unchecked(instruction, accounts.as_slice(), signers_seeds)
271        }
272    }
273}
274
275/// Invoke with a dynamic number of accounts (bounded by const generic).
276#[cfg(feature = "hopper-native-backend")]
277#[inline]
278pub fn invoke_with_bounds<const MAX_ACCOUNTS: usize>(
279    instruction: &InstructionView,
280    account_views: &[&AccountView],
281) -> ProgramResult {
282    invoke_signed_with_bounds::<MAX_ACCOUNTS>(instruction, account_views, &[])
283}
284
285/// Signed invoke with a dynamic number of accounts (bounded by const generic).
286#[cfg(feature = "hopper-native-backend")]
287#[inline]
288pub fn invoke_signed_with_bounds<const MAX_ACCOUNTS: usize>(
289    instruction: &InstructionView,
290    account_views: &[&AccountView],
291    signers_seeds: &[Signer],
292) -> ProgramResult {
293    if account_views.len() > MAX_ACCOUNTS {
294        return Err(ProgramError::InvalidArgument);
295    }
296
297    validate_cpi_accounts(instruction, account_views, signers_seeds)?;
298
299    let mut cpi_accounts: [MaybeUninit<CpiAccount>; MAX_ACCOUNTS] =
300        unsafe { MaybeUninit::uninit().assume_init() };
301
302    let count = account_views.len();
303    let mut i = 0;
304    while i < count {
305        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(account_views[i]));
306        i += 1;
307    }
308
309    let accounts = unsafe {
310        core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount, count)
311    };
312
313    unsafe {
314        if signers_seeds.is_empty() {
315            invoke_unchecked(instruction, accounts)
316        } else {
317            invoke_signed_unchecked(instruction, accounts, signers_seeds)
318        }
319    }
320}
321
322// ══════════════════════════════════════════════════════════════════════
323//  Compatibility backends CPI
324// ══════════════════════════════════════════════════════════════════════
325
326/// Invoke a CPI through the active compatibility backend.
327#[cfg(any(feature = "legacy-pinocchio-compat", feature = "solana-program-backend"))]
328#[inline]
329pub fn invoke<const ACCOUNTS: usize>(
330    instruction: &InstructionView,
331    account_views: &[&AccountView; ACCOUNTS],
332) -> ProgramResult {
333    invoke_signed::<ACCOUNTS>(instruction, account_views, &[])
334}
335
336/// Invoke a signed CPI through the active compatibility backend.
337#[cfg(any(feature = "legacy-pinocchio-compat", feature = "solana-program-backend"))]
338#[inline]
339pub fn invoke_signed<const ACCOUNTS: usize>(
340    instruction: &InstructionView,
341    account_views: &[&AccountView; ACCOUNTS],
342    signers_seeds: &[Signer],
343) -> ProgramResult {
344    validate_cpi_accounts(instruction, &account_views[..], signers_seeds)?;
345    crate::compat::invoke_signed(instruction, account_views, signers_seeds)
346}
347
348// ── Return data ──────────────────────────────────────────────────────
349
350/// Set return data for the current instruction.
351#[inline(always)]
352pub fn set_return_data(data: &[u8]) {
353    crate::compat::set_return_data(data)
354}
355
356#[cfg(all(test, feature = "hopper-native-backend"))]
357mod tests {
358    use super::*;
359
360    use crate::InstructionAccount;
361    use hopper_native::{AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED};
362
363    fn make_account(address: [u8; 32]) -> (std::vec::Vec<u8>, AccountView) {
364        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + 16];
365        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
366        unsafe {
367            raw.write(RuntimeAccount {
368                borrow_state: NOT_BORROWED,
369                is_signer: 0,
370                is_writable: 1,
371                executable: 0,
372                resize_delta: 0,
373                address: NativeAddress::new_from_array(address),
374                owner: NativeAddress::new_from_array([9; 32]),
375                lamports: 1,
376                data_len: 16,
377            });
378        }
379        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
380        (backing, AccountView::from_backend(backend))
381    }
382
383    #[test]
384    fn duplicate_writable_accounts_are_rejected_before_cpi() {
385        let (_first_backing, first) = make_account([3; 32]);
386        let (_second_backing, second) = make_account([3; 32]);
387
388        let instruction_accounts = [
389            InstructionAccount::writable(first.address()),
390            InstructionAccount::writable(second.address()),
391        ];
392        let program_id = Address::new_from_array([7; 32]);
393        let instruction = InstructionView {
394            program_id: &program_id,
395            data: &[0u8],
396            accounts: &instruction_accounts,
397        };
398
399        let err = validate_no_duplicate_writable(&instruction, &[&first, &second]).unwrap_err();
400        assert_eq!(err, ProgramError::AccountBorrowFailed);
401    }
402}