Skip to main content

sbpf_runtime/
runtime.rs

1use {
2    crate::{
3        config::{ExecutionCost, RuntimeConfig, SysvarContext},
4        cpi,
5        elf::load_elf,
6        errors::{RuntimeError, RuntimeResult},
7        serialize,
8        syscalls::RuntimeSyscallHandler,
9    },
10    base64::{Engine, engine::general_purpose::STANDARD as BASE64},
11    sbpf_common::{execute::Vm, instruction::Instruction},
12    sbpf_vm::{
13        compute::ComputeMeter,
14        memory::Memory,
15        vm::{CallFrame, SbpfVm, SbpfVmConfig},
16    },
17    solana_account::Account,
18    solana_address::Address,
19    solana_instruction::{AccountMeta, Instruction as SolanaInstruction},
20    std::{cell::RefCell, collections::HashMap, rc::Rc},
21};
22
23pub type LogCollector = Rc<RefCell<Vec<String>>>;
24
25pub enum ElfSource {
26    Path(String),
27    Bytes(Vec<u8>),
28}
29
30impl From<&str> for ElfSource {
31    fn from(path: &str) -> Self {
32        ElfSource::Path(path.to_string())
33    }
34}
35
36impl From<&[u8]> for ElfSource {
37    fn from(bytes: &[u8]) -> Self {
38        ElfSource::Bytes(bytes.to_vec())
39    }
40}
41
42impl From<Vec<u8>> for ElfSource {
43    fn from(bytes: Vec<u8>) -> Self {
44        ElfSource::Bytes(bytes)
45    }
46}
47
48pub struct ExecutionResult {
49    pub exit_code: Option<u64>,
50    pub compute_units_consumed: u64,
51    pub logs: Vec<String>,
52}
53
54pub struct Runtime {
55    program_id: Address,
56    instructions: Vec<Instruction>,
57    rodata: Vec<u8>,
58    entrypoint: usize,
59    programs: HashMap<Address, Vec<u8>>,
60    config: RuntimeConfig,
61    sysvars: SysvarContext,
62    vm: Option<SbpfVm<RuntimeSyscallHandler>>,
63    accounts: HashMap<Address, Account>,
64    account_metas: Vec<AccountMeta>,
65    pre_lens: Vec<usize>, // original account data lengths at serialization
66    log_collector: LogCollector,
67}
68
69impl Runtime {
70    pub fn new(
71        program_id: Address,
72        elf: impl Into<ElfSource>,
73        config: RuntimeConfig,
74    ) -> RuntimeResult<Self> {
75        let elf_bytes = match elf.into() {
76            ElfSource::Path(path) => std::fs::read(&path)?,
77            ElfSource::Bytes(bytes) => bytes,
78        };
79
80        let (instructions, rodata, entrypoint) = load_elf(&elf_bytes)?;
81
82        Ok(Self {
83            program_id,
84            instructions,
85            rodata,
86            entrypoint,
87            programs: HashMap::new(),
88            config,
89            sysvars: SysvarContext::default(),
90            vm: None,
91            accounts: HashMap::new(),
92            account_metas: Vec::new(),
93            pre_lens: Vec::new(),
94            log_collector: Rc::new(RefCell::new(Vec::new())),
95        })
96    }
97
98    pub fn add_program(&mut self, program_id: &Address, elf: impl Into<ElfSource>) {
99        let elf_bytes = match elf.into() {
100            ElfSource::Path(path) => std::fs::read(&path).expect("Failed to read ELF"),
101            ElfSource::Bytes(bytes) => bytes,
102        };
103        self.programs.insert(*program_id, elf_bytes);
104    }
105
106    fn setup_vm(
107        &mut self,
108        instruction: &SolanaInstruction,
109        accounts: &[(Address, Account)],
110    ) -> RuntimeResult<()> {
111        // Setup accounts (merge with existing account state).
112        for (address, account) in accounts.iter() {
113            self.accounts
114                .entry(*address)
115                .or_insert_with(|| account.clone());
116        }
117        self.account_metas = instruction.accounts.clone();
118
119        let (input, pre_lens, instruction_data_offset) = serialize::serialize_parameters(
120            &self.accounts,
121            &self.account_metas,
122            &instruction.data,
123            &self.program_id,
124        )?;
125
126        let vm_config = SbpfVmConfig {
127            compute_unit_limit: self.config.compute_budget,
128            max_call_depth: self.config.max_call_depth,
129            heap_size: self.config.heap_size,
130        };
131
132        let handler = RuntimeSyscallHandler::new(
133            ExecutionCost::default(),
134            self.program_id,
135            self.sysvars.clone(),
136            self.log_collector.clone(),
137        );
138
139        let mut vm = SbpfVm::new_with_config(
140            self.instructions.clone(),
141            input,
142            self.rodata.clone(),
143            handler,
144            vm_config,
145        );
146        vm.compute_meter = ComputeMeter::new(self.config.compute_budget);
147        vm.set_entrypoint(self.entrypoint);
148        vm.registers[2] = Memory::INPUT_START + instruction_data_offset as u64;
149
150        self.pre_lens = pre_lens;
151        self.vm = Some(vm);
152        Ok(())
153    }
154
155    fn sync_accounts(&mut self) -> RuntimeResult<()> {
156        if let Some(ref vm) = self.vm {
157            serialize::deserialize_parameters(
158                &mut self.accounts,
159                &self.account_metas,
160                &vm.memory.input,
161                &self.pre_lens,
162                &self.program_id,
163            )?;
164        }
165        Ok(())
166    }
167
168    pub fn run(
169        &mut self,
170        instruction: &SolanaInstruction,
171        accounts: &[(Address, Account)],
172    ) -> RuntimeResult<ExecutionResult> {
173        self.log_collector.borrow_mut().clear();
174        self.setup_vm(instruction, accounts)?;
175
176        // Get pre-execution lamports from the account state.
177        let pre_lamports: HashMap<Address, u64> = self
178            .account_metas
179            .iter()
180            .filter_map(|meta| {
181                self.accounts
182                    .get(&meta.pubkey)
183                    .map(|a| (meta.pubkey, a.lamports))
184            })
185            .collect();
186
187        self.log_collector
188            .borrow_mut()
189            .push(format!("Program {} invoke [1]", self.program_id));
190
191        loop {
192            let vm = self.vm.as_mut().unwrap();
193            if let Err(e) = vm.step() {
194                self.log_collector
195                    .borrow_mut()
196                    .push(format!("Program failed: {}", e));
197                return Err(e.into());
198            }
199
200            if let Some(request) = vm.syscall_handler.pending_cpi.take() {
201                if let Err(e) = self.handle_cpi(request) {
202                    self.log_collector
203                        .borrow_mut()
204                        .push(format!("Program failed: {}", e));
205                    return Err(e);
206                }
207                continue;
208            }
209
210            if vm.halted {
211                break;
212            }
213        }
214
215        self.sync_accounts()?;
216
217        // Verify total lamport balance is conserved across all instruction accounts.
218        let pre_total: u64 = pre_lamports.values().sum();
219        let post_total: u64 = pre_lamports
220            .keys()
221            .filter_map(|pk| self.accounts.get(pk))
222            .map(|a| a.lamports)
223            .sum();
224        if pre_total != post_total {
225            return Err(RuntimeError::UnbalancedInstruction(pre_total, post_total));
226        }
227
228        let vm = self.vm.as_ref().unwrap();
229        let consumed = vm.compute_meter.get_consumed();
230        let exit_code = vm.exit_code;
231
232        if let Some(ref return_data) = vm.syscall_handler.return_data
233            && !return_data.1.is_empty()
234        {
235            self.log_collector.borrow_mut().push(format!(
236                "Program return: {} {}",
237                return_data.0,
238                BASE64.encode(&return_data.1)
239            ));
240        }
241
242        self.log_collector.borrow_mut().push(format!(
243            "Program {} consumed {} of {} compute units",
244            self.program_id, consumed, self.config.compute_budget
245        ));
246
247        if exit_code.unwrap_or(0) == 0 {
248            self.log_collector
249                .borrow_mut()
250                .push(format!("Program {} success", self.program_id));
251        } else {
252            self.log_collector.borrow_mut().push(format!(
253                "Program {} failed: exit code {}",
254                self.program_id,
255                exit_code.unwrap_or(0)
256            ));
257        }
258
259        let logs = self.log_collector.borrow().clone();
260
261        Ok(ExecutionResult {
262            exit_code,
263            compute_units_consumed: consumed,
264            logs,
265        })
266    }
267
268    pub fn prepare(
269        &mut self,
270        instruction: &SolanaInstruction,
271        accounts: &[(Address, Account)],
272    ) -> RuntimeResult<()> {
273        self.log_collector.borrow_mut().clear();
274        self.setup_vm(instruction, accounts)?;
275        self.log_collector
276            .borrow_mut()
277            .push(format!("Program {} invoke [1]", self.program_id));
278        Ok(())
279    }
280
281    pub fn step(&mut self) -> RuntimeResult<()> {
282        let vm = self.vm.as_mut().ok_or(RuntimeError::VmNotPrepared)?;
283        if vm.halted {
284            return Ok(());
285        }
286        if let Err(e) = vm.step() {
287            self.log_collector
288                .borrow_mut()
289                .push(format!("Program failed: {}", e));
290            return Err(e.into());
291        }
292
293        if let Some(request) = vm.syscall_handler.pending_cpi.take()
294            && let Err(e) = self.handle_cpi(request)
295        {
296            self.log_collector
297                .borrow_mut()
298                .push(format!("Program failed: {}", e));
299            return Err(e);
300        }
301
302        let vm_ref = self.vm.as_ref().unwrap();
303        if vm_ref.halted {
304            self.sync_accounts()?;
305
306            let vm = self.vm.as_ref().unwrap();
307            let consumed = vm.compute_meter.get_consumed();
308            let exit_code = vm.exit_code;
309
310            if let Some(ref return_data) = vm.syscall_handler.return_data
311                && !return_data.1.is_empty()
312            {
313                self.log_collector.borrow_mut().push(format!(
314                    "Program return: {} {}",
315                    return_data.0,
316                    BASE64.encode(&return_data.1)
317                ));
318            }
319
320            self.log_collector.borrow_mut().push(format!(
321                "Program {} consumed {} of {} compute units",
322                self.program_id, consumed, self.config.compute_budget
323            ));
324
325            if exit_code.unwrap_or(0) == 0 {
326                self.log_collector
327                    .borrow_mut()
328                    .push(format!("Program {} success", self.program_id));
329            } else {
330                self.log_collector.borrow_mut().push(format!(
331                    "Program {} failed: exit code {}",
332                    self.program_id,
333                    exit_code.unwrap_or(0)
334                ));
335            }
336        }
337
338        Ok(())
339    }
340
341    fn handle_cpi(&mut self, request: cpi::request::CpiRequest) -> RuntimeResult<()> {
342        let vm = self.vm.as_ref().unwrap();
343        let compute_remaining = self.config.compute_budget - vm.compute_meter.get_consumed();
344
345        // Sync latest account state from caller VM memory into account store.
346        cpi::sync::sync_from_caller(&vm.memory, &request.caller_accounts, &mut self.accounts)?;
347
348        let caller_accounts = request.caller_accounts;
349        let cpi_request = cpi::request::CpiRequest {
350            program_id: request.program_id,
351            accounts: request.accounts,
352            data: request.data,
353            caller_accounts: Vec::new(),
354            signers: request.signers,
355        };
356
357        let mut ctx = cpi::CpiContext {
358            request: cpi_request,
359            programs: &self.programs,
360            accounts: &mut self.accounts,
361            config: &self.config,
362            sysvars: &self.sysvars,
363            compute_remaining,
364            cpi_depth: 1,
365            caller_account_metas: &self.account_metas,
366            log_collector: &self.log_collector,
367        };
368
369        let output = cpi::execute_cpi(&mut ctx)?;
370
371        let vm = self.vm.as_mut().unwrap();
372        vm.compute_meter.consume(output.compute_consumed)?;
373        vm.syscall_handler.return_data = output.return_data;
374
375        if output.exit_code != 0 {
376            return Err(RuntimeError::VmError(
377                sbpf_vm::errors::SbpfVmError::SyscallError(format!(
378                    "CPI callee returned error: {}",
379                    output.exit_code
380                )),
381            ));
382        }
383
384        // Sync updated accounts back to caller VM memory.
385        let vm = self.vm.as_mut().unwrap();
386        cpi::sync::sync_to_caller(&mut vm.memory, &caller_accounts, &self.accounts)?;
387
388        Ok(())
389    }
390
391    pub fn get_pc(&self) -> usize {
392        self.vm.as_ref().map(|vm| vm.pc).unwrap_or(0)
393    }
394
395    pub fn get_registers(&self) -> Option<&[u64; 11]> {
396        self.vm.as_ref().map(|vm| &vm.registers)
397    }
398
399    pub fn current_program_id(&self) -> &Address {
400        &self.program_id
401    }
402
403    pub fn is_halted(&self) -> bool {
404        self.vm.as_ref().map(|vm| vm.halted).unwrap_or(false)
405    }
406
407    pub fn exit_code(&self) -> Option<u64> {
408        self.vm.as_ref().and_then(|vm| vm.exit_code)
409    }
410
411    pub fn compute_units_consumed(&self) -> u64 {
412        self.vm
413            .as_ref()
414            .map(|vm| vm.compute_meter.get_consumed())
415            .unwrap_or(0)
416    }
417
418    pub fn get_account(&self, pubkey: &Address) -> Option<Account> {
419        self.accounts.get(pubkey).cloned()
420    }
421
422    pub fn get_accounts(&self) -> &HashMap<Address, Account> {
423        &self.accounts
424    }
425
426    pub fn get_register(&self, idx: usize) -> Option<u64> {
427        self.vm
428            .as_ref()
429            .and_then(|vm| vm.registers.get(idx).copied())
430    }
431
432    pub fn set_register(&mut self, idx: usize, value: u64) -> RuntimeResult<()> {
433        let vm = self.vm.as_mut().ok_or(RuntimeError::VmNotPrepared)?;
434        if idx >= vm.registers.len() {
435            return Err(RuntimeError::RegisterOutOfRange(idx));
436        }
437        vm.set_register(idx, value);
438        Ok(())
439    }
440
441    pub fn read_memory(&self, addr: u64, size: usize) -> Option<Vec<u8>> {
442        self.vm
443            .as_ref()
444            .and_then(|vm| vm.memory.read_bytes(addr, size).ok().map(|s| s.to_vec()))
445    }
446
447    pub fn get_instruction(&self) -> Option<&Instruction> {
448        let vm = self.vm.as_ref()?;
449        vm.program.get(vm.pc)
450    }
451
452    pub fn get_program(&self) -> &[Instruction] {
453        &self.instructions
454    }
455
456    pub fn get_call_stack(&self) -> Option<&[CallFrame]> {
457        self.vm.as_ref().map(|vm| vm.call_stack.as_slice())
458    }
459
460    pub fn config(&self) -> &RuntimeConfig {
461        &self.config
462    }
463
464    pub fn sysvars(&self) -> &SysvarContext {
465        &self.sysvars
466    }
467
468    pub fn sysvars_mut(&mut self) -> &mut SysvarContext {
469        &mut self.sysvars
470    }
471
472    pub fn log_collector(&self) -> &LogCollector {
473        &self.log_collector
474    }
475
476    pub fn drain_logs(&self) -> Vec<String> {
477        self.log_collector.borrow_mut().drain(..).collect()
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use {super::*, std::path::PathBuf};
484
485    const PROGRAM_ID: Address =
486        Address::from_str_const("22222222222222222222222222222222222222222222");
487
488    fn escrow_elf_path() -> String {
489        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
490            .join("tests/fixtures/libupstream_pinocchio_escrow.so")
491            .to_str()
492            .unwrap()
493            .to_string()
494    }
495
496    fn new_runtime() -> Runtime {
497        Runtime::new(
498            PROGRAM_ID,
499            escrow_elf_path().as_str(),
500            RuntimeConfig::default(),
501        )
502        .unwrap()
503    }
504
505    fn empty_instruction() -> SolanaInstruction {
506        SolanaInstruction {
507            program_id: PROGRAM_ID,
508            accounts: Vec::new(),
509            data: Vec::new(),
510        }
511    }
512
513    #[test]
514    fn elf_source_from_str() {
515        let path = escrow_elf_path();
516        assert!(matches!(ElfSource::from(path.as_str()), ElfSource::Path(_)));
517    }
518
519    #[test]
520    fn elf_source_from_bytes_slice() {
521        let bytes: &[u8] = &[1, 2, 3];
522        assert!(matches!(ElfSource::from(bytes), ElfSource::Bytes(_)));
523    }
524
525    #[test]
526    fn elf_source_from_vec() {
527        assert!(matches!(
528            ElfSource::from(vec![1u8, 2, 3]),
529            ElfSource::Bytes(_)
530        ));
531    }
532
533    #[test]
534    fn new_from_path() {
535        let rt = new_runtime();
536        assert_eq!(*rt.current_program_id(), PROGRAM_ID);
537        assert!(!rt.get_program().is_empty());
538    }
539
540    #[test]
541    fn new_from_bytes() {
542        let bytes = std::fs::read(escrow_elf_path()).unwrap();
543        let rt = Runtime::new(PROGRAM_ID, bytes, RuntimeConfig::default()).unwrap();
544        assert!(!rt.get_program().is_empty());
545    }
546
547    #[test]
548    fn new_bad_path_errors() {
549        match Runtime::new(
550            PROGRAM_ID,
551            "/nonexistent/path/to/program.so",
552            RuntimeConfig::default(),
553        ) {
554            Err(RuntimeError::ElfReadError(_)) => {}
555            other => panic!("expected ElfReadError, got {:?}", other.map(|_| ())),
556        }
557    }
558
559    #[test]
560    fn new_invalid_elf_errors() {
561        match Runtime::new(PROGRAM_ID, vec![0u8; 8], RuntimeConfig::default()) {
562            Err(RuntimeError::ElfReadError(_)) => panic!("expected parse error, got io error"),
563            Err(_) => {}
564            Ok(_) => panic!("expected error for invalid ELF"),
565        }
566    }
567
568    #[test]
569    fn add_program_accepts_path_and_bytes() {
570        let mut rt = new_runtime();
571        rt.add_program(&Address::new_unique(), escrow_elf_path().as_str());
572        let bytes = std::fs::read(escrow_elf_path()).unwrap();
573        rt.add_program(&Address::new_unique(), bytes);
574    }
575
576    #[test]
577    fn getters_before_prepare_are_defaults() {
578        let rt = new_runtime();
579        assert_eq!(rt.get_pc(), 0);
580        assert!(rt.get_registers().is_none());
581        assert!(rt.get_register(2).is_none());
582        assert!(rt.read_memory(Memory::INPUT_START, 8).is_none());
583        assert!(rt.get_instruction().is_none());
584        assert!(rt.get_call_stack().is_none());
585        assert!(!rt.is_halted());
586        assert!(rt.exit_code().is_none());
587        assert_eq!(rt.compute_units_consumed(), 0);
588        assert!(rt.get_account(&PROGRAM_ID).is_none());
589        assert!(rt.get_accounts().is_empty());
590        assert!(rt.drain_logs().is_empty());
591    }
592
593    #[test]
594    fn set_register_before_prepare_errors() {
595        let mut rt = new_runtime();
596        let err = rt.set_register(0, 42).unwrap_err();
597        assert!(matches!(err, RuntimeError::VmNotPrepared));
598    }
599
600    #[test]
601    fn step_before_prepare_errors() {
602        let mut rt = new_runtime();
603        let err = rt.step().unwrap_err();
604        assert!(matches!(err, RuntimeError::VmNotPrepared));
605    }
606
607    #[test]
608    fn config_and_sysvar_accessors() {
609        let mut rt = new_runtime();
610        assert_eq!(
611            rt.config().compute_budget,
612            RuntimeConfig::default().compute_budget
613        );
614        let slot = rt.sysvars().clock.slot;
615        rt.sysvars_mut().clock.slot = slot + 5;
616        assert_eq!(rt.sysvars().clock.slot, slot + 5);
617        assert!(rt.log_collector().borrow().is_empty());
618    }
619
620    #[test]
621    fn prepare_initializes_vm_state() {
622        let mut rt = new_runtime();
623        rt.prepare(&empty_instruction(), &[]).unwrap();
624
625        // After prepare the VM exists: getters now return Some / live values.
626        assert!(rt.get_registers().is_some());
627        assert!(rt.get_call_stack().is_some());
628        assert!(rt.get_instruction().is_some());
629        let r2 = rt.get_register(2).unwrap();
630        assert!(r2 >= Memory::INPUT_START);
631        assert!(rt.read_memory(Memory::INPUT_START, 8).is_some());
632        let logs = rt.log_collector().borrow().clone();
633        assert!(logs.iter().any(|l| l.contains("invoke [1]")));
634    }
635
636    #[test]
637    fn set_register_after_prepare_and_out_of_range() {
638        let mut rt = new_runtime();
639        rt.prepare(&empty_instruction(), &[]).unwrap();
640
641        rt.set_register(3, 0xdead_beef).unwrap();
642        assert_eq!(rt.get_register(3), Some(0xdead_beef));
643
644        let err = rt.set_register(99, 1).unwrap_err();
645        assert!(matches!(err, RuntimeError::RegisterOutOfRange(99)));
646    }
647
648    #[test]
649    fn step_drives_program_to_clean_halt() {
650        let mut rt = new_runtime();
651        rt.prepare(&empty_instruction(), &[]).unwrap();
652
653        let mut steps = 0;
654        while !rt.is_halted() {
655            rt.step().expect("step should not trap on this program");
656            steps += 1;
657            assert!(steps <= 100_000, "program did not terminate");
658        }
659        assert!(steps > 0);
660        assert!(rt.is_halted());
661        assert!(rt.exit_code().is_some());
662    }
663
664    #[test]
665    fn run_returns_ok_with_nonzero_exit_and_logs() {
666        let mut rt = new_runtime();
667        let exec = rt.run(&empty_instruction(), &[]).unwrap();
668
669        assert!(matches!(exec.exit_code, Some(code) if code != 0));
670        assert!(exec.compute_units_consumed > 0);
671
672        assert!(exec.logs.iter().any(|l| l.contains("invoke [1]")));
673        assert!(exec.logs.iter().any(|l| l.contains("consumed")));
674        assert!(exec.logs.iter().any(|l| l.contains("failed: exit code")));
675    }
676}