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}