Skip to main content

sbpf_runtime/cpi/
mod.rs

1pub mod builtins;
2pub mod request;
3pub mod sync;
4pub mod validate;
5
6use {
7    crate::{
8        config::{ExecutionCost, RuntimeConfig, SysvarContext},
9        elf::load_elf,
10        errors::{RuntimeError, RuntimeResult},
11        runtime::LogCollector,
12        serialize,
13        syscalls::RuntimeSyscallHandler,
14    },
15    base64::{Engine, engine::general_purpose::STANDARD as BASE64},
16    request::CpiRequest,
17    sbpf_vm::{
18        compute::ComputeMeter,
19        memory::Memory,
20        vm::{SbpfVm, SbpfVmConfig},
21    },
22    solana_account::Account,
23    solana_address::Address,
24    solana_instruction::AccountMeta,
25    std::collections::HashMap,
26};
27
28pub type ReturnData = Option<(Address, Vec<u8>)>;
29
30pub struct CpiOutput {
31    pub exit_code: u64,
32    pub return_data: ReturnData,
33    pub compute_consumed: u64,
34}
35
36pub type CpiExecResult = RuntimeResult<CpiOutput>;
37
38pub struct CpiContext<'a> {
39    pub request: CpiRequest,
40    pub programs: &'a HashMap<Address, Vec<u8>>,
41    pub accounts: &'a mut HashMap<Address, Account>,
42    pub config: &'a RuntimeConfig,
43    pub sysvars: &'a SysvarContext,
44    pub compute_remaining: u64,
45    pub cpi_depth: usize,
46    pub caller_account_metas: &'a [AccountMeta],
47    pub log_collector: &'a LogCollector,
48}
49
50pub fn execute_cpi(ctx: &mut CpiContext) -> CpiExecResult {
51    if ctx.cpi_depth >= ctx.config.max_cpi_depth {
52        return Err(RuntimeError::CpiDepthExceeded(ctx.config.max_cpi_depth));
53    }
54
55    validate::check_privileges(&ctx.request, ctx.caller_account_metas)?;
56
57    ctx.log_collector.borrow_mut().push(format!(
58        "Program {} invoke [{}]",
59        ctx.request.program_id,
60        ctx.cpi_depth + 1
61    ));
62
63    if builtins::is_builtin(&ctx.request.program_id) {
64        let mut all_signers = ctx.request.signers.clone();
65        for meta in &ctx.request.accounts {
66            if meta.is_signer && !all_signers.contains(&meta.pubkey) {
67                all_signers.push(meta.pubkey);
68            }
69        }
70        let consumed = builtins::execute_builtin(
71            &ctx.request.program_id,
72            ctx.accounts,
73            &ctx.request,
74            &all_signers,
75            ctx.compute_remaining,
76        )?;
77        ctx.log_collector.borrow_mut().push(format!(
78            "Program {} consumed {} of {} compute units",
79            ctx.request.program_id, consumed, ctx.compute_remaining
80        ));
81        ctx.log_collector
82            .borrow_mut()
83            .push(format!("Program {} success", ctx.request.program_id));
84        return Ok(CpiOutput {
85            exit_code: 0,
86            return_data: None,
87            compute_consumed: consumed,
88        });
89    }
90
91    execute_elf_cpi(ctx)
92}
93
94fn execute_elf_cpi(ctx: &mut CpiContext) -> CpiExecResult {
95    let elf_bytes = ctx
96        .programs
97        .get(&ctx.request.program_id)
98        .ok_or_else(|| RuntimeError::ProgramNotFound(ctx.request.program_id.to_string()))?;
99
100    let (instructions, rodata, entrypoint) = load_elf(elf_bytes)?;
101
102    let account_metas: Vec<AccountMeta> = ctx
103        .request
104        .accounts
105        .iter()
106        .map(|a| AccountMeta {
107            pubkey: a.pubkey,
108            is_signer: a.is_signer,
109            is_writable: a.is_writable,
110        })
111        .collect();
112
113    let (input, pre_lens, instruction_data_offset) = serialize::serialize_parameters(
114        ctx.accounts,
115        &account_metas,
116        &ctx.request.data,
117        &ctx.request.program_id,
118    )?;
119
120    let vm_config = SbpfVmConfig {
121        compute_unit_limit: ctx.compute_remaining,
122        max_call_depth: ctx.config.max_call_depth,
123        heap_size: ctx.config.heap_size,
124    };
125
126    let handler = RuntimeSyscallHandler::new(
127        ExecutionCost::default(),
128        ctx.request.program_id,
129        ctx.sysvars.clone(),
130        ctx.log_collector.clone(),
131    );
132
133    let mut callee_vm = SbpfVm::new_with_config(instructions, input, rodata, handler, vm_config);
134    callee_vm.compute_meter = ComputeMeter::new(ctx.compute_remaining);
135    callee_vm.set_entrypoint(entrypoint);
136    callee_vm.registers[2] = Memory::INPUT_START + instruction_data_offset as u64;
137
138    loop {
139        if let Err(e) = callee_vm.step() {
140            return Err(e.into());
141        }
142
143        if let Some(nested_request) = callee_vm.syscall_handler.pending_cpi.take() {
144            sync::sync_from_caller(
145                &callee_vm.memory,
146                &nested_request.caller_accounts,
147                ctx.accounts,
148            )?;
149
150            let caller_accounts_for_sync = nested_request.caller_accounts;
151            let nested_consumed = callee_vm.compute_meter.get_consumed();
152            let nested_remaining = ctx.compute_remaining.saturating_sub(nested_consumed);
153
154            let nested_cpi_request = CpiRequest {
155                program_id: nested_request.program_id,
156                accounts: nested_request.accounts,
157                data: nested_request.data,
158                caller_accounts: Vec::new(),
159                signers: nested_request.signers,
160            };
161
162            let mut nested_ctx = CpiContext {
163                request: nested_cpi_request,
164                programs: ctx.programs,
165                accounts: ctx.accounts,
166                config: ctx.config,
167                sysvars: ctx.sysvars,
168                compute_remaining: nested_remaining,
169                cpi_depth: ctx.cpi_depth + 1,
170                caller_account_metas: &account_metas,
171                log_collector: ctx.log_collector,
172            };
173
174            let nested_output = execute_cpi(&mut nested_ctx)?;
175
176            callee_vm
177                .compute_meter
178                .consume(nested_output.compute_consumed)?;
179            callee_vm.syscall_handler.return_data = nested_output.return_data;
180
181            if nested_output.exit_code != 0 {
182                let consumed = callee_vm.compute_meter.get_consumed();
183                return Ok(CpiOutput {
184                    exit_code: nested_output.exit_code,
185                    return_data: callee_vm.syscall_handler.return_data.take(),
186                    compute_consumed: consumed,
187                });
188            }
189
190            sync::sync_to_caller(
191                &mut callee_vm.memory,
192                &caller_accounts_for_sync,
193                ctx.accounts,
194            )?;
195        }
196
197        if callee_vm.halted {
198            break;
199        }
200    }
201
202    let exit_code = callee_vm.exit_code.unwrap_or(0);
203    let callee_return_data = callee_vm.syscall_handler.return_data.take();
204    let consumed = callee_vm.compute_meter.get_consumed();
205
206    if exit_code != 0 {
207        ctx.log_collector.borrow_mut().push(format!(
208            "Program {} consumed {} of {} compute units",
209            ctx.request.program_id, consumed, ctx.compute_remaining
210        ));
211        ctx.log_collector.borrow_mut().push(format!(
212            "Program {} failed: exit code {}",
213            ctx.request.program_id, exit_code
214        ));
215        return Ok(CpiOutput {
216            exit_code,
217            return_data: callee_return_data,
218            compute_consumed: consumed,
219        });
220    }
221
222    serialize::deserialize_parameters(
223        ctx.accounts,
224        &account_metas,
225        &callee_vm.memory.input,
226        &pre_lens,
227        &ctx.request.program_id,
228    )?;
229
230    if let Some((ref pid, ref data)) = callee_return_data
231        && !data.is_empty()
232    {
233        ctx.log_collector.borrow_mut().push(format!(
234            "Program return: {} {}",
235            pid,
236            BASE64.encode(data)
237        ));
238    }
239
240    ctx.log_collector.borrow_mut().push(format!(
241        "Program {} consumed {} of {} compute units",
242        ctx.request.program_id, consumed, ctx.compute_remaining
243    ));
244    ctx.log_collector
245        .borrow_mut()
246        .push(format!("Program {} success", ctx.request.program_id));
247
248    Ok(CpiOutput {
249        exit_code: 0,
250        return_data: callee_return_data,
251        compute_consumed: consumed,
252    })
253}
254
255#[cfg(test)]
256mod tests {
257    use {
258        super::*,
259        crate::cpi::request::CpiAccountMeta,
260        solana_system_interface::{instruction::SystemInstruction, program as system_program},
261        std::{cell::RefCell, rc::Rc},
262    };
263
264    fn addr(seed: u8) -> Address {
265        Address::new_from_array([seed; 32])
266    }
267
268    fn log_collector() -> LogCollector {
269        Rc::new(RefCell::new(Vec::new()))
270    }
271
272    fn system_account(lamports: u64) -> Account {
273        Account {
274            lamports,
275            data: vec![],
276            owner: system_program::ID,
277            executable: false,
278            rent_epoch: 0,
279        }
280    }
281
282    #[test]
283    fn execute_cpi_program_not_found() {
284        let config = RuntimeConfig::default();
285        let sysvars = SysvarContext::default();
286        let programs: HashMap<Address, Vec<u8>> = HashMap::new();
287        let mut accounts: HashMap<Address, Account> = HashMap::new();
288        let logs = log_collector();
289
290        // Non-builtin program that was never registered via add_program.
291        let request = CpiRequest {
292            program_id: addr(9),
293            accounts: Vec::new(),
294            data: Vec::new(),
295            caller_accounts: Vec::new(),
296            signers: Vec::new(),
297        };
298        let mut ctx = CpiContext {
299            request,
300            programs: &programs,
301            accounts: &mut accounts,
302            config: &config,
303            sysvars: &sysvars,
304            compute_remaining: 200_000,
305            cpi_depth: 0,
306            caller_account_metas: &[],
307            log_collector: &logs,
308        };
309
310        match execute_cpi(&mut ctx) {
311            Err(RuntimeError::ProgramNotFound(_)) => {}
312            Err(other) => panic!("expected ProgramNotFound, got {other:?}"),
313            Ok(_) => panic!("expected ProgramNotFound error"),
314        }
315    }
316
317    #[test]
318    fn execute_cpi_depth_exceeded() {
319        let config = RuntimeConfig::default();
320        let sysvars = SysvarContext::default();
321        let programs: HashMap<Address, Vec<u8>> = HashMap::new();
322        let mut accounts: HashMap<Address, Account> = HashMap::new();
323        let logs = log_collector();
324
325        let request = CpiRequest {
326            program_id: addr(9),
327            accounts: Vec::new(),
328            data: Vec::new(),
329            caller_accounts: Vec::new(),
330            signers: Vec::new(),
331        };
332        let mut ctx = CpiContext {
333            request,
334            programs: &programs,
335            accounts: &mut accounts,
336            config: &config,
337            sysvars: &sysvars,
338            compute_remaining: 200_000,
339            cpi_depth: config.max_cpi_depth, // already at the limit
340            caller_account_metas: &[],
341            log_collector: &logs,
342        };
343
344        match execute_cpi(&mut ctx) {
345            Err(RuntimeError::CpiDepthExceeded(_)) => {}
346            Err(other) => panic!("expected CpiDepthExceeded, got {other:?}"),
347            Ok(_) => panic!("expected CpiDepthExceeded error"),
348        }
349    }
350
351    #[test]
352    fn execute_cpi_dispatches_system_transfer_builtin() {
353        let config = RuntimeConfig::default();
354        let sysvars = SysvarContext::default();
355        let programs: HashMap<Address, Vec<u8>> = HashMap::new();
356        let logs = log_collector();
357
358        let from = addr(1);
359        let to = addr(2);
360        let mut accounts =
361            HashMap::from([(from, system_account(1_000_000)), (to, system_account(0))]);
362
363        let request = CpiRequest {
364            program_id: system_program::ID,
365            accounts: vec![
366                CpiAccountMeta {
367                    pubkey: from,
368                    is_signer: true,
369                    is_writable: true,
370                },
371                CpiAccountMeta {
372                    pubkey: to,
373                    is_signer: false,
374                    is_writable: true,
375                },
376            ],
377            data: wincode::serialize(&SystemInstruction::Transfer { lamports: 400_000 }).unwrap(),
378            caller_accounts: Vec::new(),
379            signers: Vec::new(),
380        };
381
382        let caller_metas = vec![
383            AccountMeta {
384                pubkey: from,
385                is_signer: true,
386                is_writable: true,
387            },
388            AccountMeta {
389                pubkey: to,
390                is_signer: false,
391                is_writable: true,
392            },
393        ];
394
395        let mut ctx = CpiContext {
396            request,
397            programs: &programs,
398            accounts: &mut accounts,
399            config: &config,
400            sysvars: &sysvars,
401            compute_remaining: 200_000,
402            cpi_depth: 0,
403            caller_account_metas: &caller_metas,
404            log_collector: &logs,
405        };
406
407        let output = execute_cpi(&mut ctx).unwrap();
408        assert_eq!(output.exit_code, 0);
409        assert_eq!(accounts[&from].lamports, 600_000);
410        assert_eq!(accounts[&to].lamports, 400_000);
411        assert!(logs.borrow().iter().any(|l| l.contains("invoke [1]")));
412        assert!(logs.borrow().iter().any(|l| l.contains("success")));
413    }
414}