rtvm_interpreter/
interpreter.rs

1pub mod analysis;
2mod contract;
3#[cfg(feature = "serde")]
4pub mod serde;
5mod shared_memory;
6mod stack;
7
8pub use contract::Contract;
9pub use shared_memory::{next_multiple_of_32, SharedMemory, EMPTY_SHARED_MEMORY};
10pub use stack::{Stack, STACK_LIMIT};
11
12use crate::EOFCreateOutcome;
13use crate::{
14    primitives::Bytes, push, push_b256, return_ok, return_revert, CallOutcome, CreateOutcome,
15    FunctionStack, Gas, Host, InstructionResult, InterpreterAction,
16};
17use core::cmp::min;
18use rtvm_primitives::{Bytecode, Eof, U256};
19use std::borrow::ToOwned;
20
21/// EVM bytecode interpreter.
22#[derive(Debug)]
23pub struct Interpreter {
24    /// The current instruction pointer.
25    pub instruction_pointer: *const u8,
26    /// The gas state.
27    pub gas: Gas,
28    /// Contract information and invoking data
29    pub contract: Contract,
30    /// The execution control flag. If this is not set to `Continue`, the interpreter will stop
31    /// execution.
32    pub instruction_result: InstructionResult,
33    /// Currently run Bytecode that instruction result will point to.
34    /// Bytecode is owned by the contract.
35    pub bytecode: Bytes,
36    /// Whether we are Interpreting the Ethereum Object Format (EOF) bytecode.
37    /// This is local field that is set from `contract.is_eof()`.
38    pub is_eof: bool,
39    /// Is init flag for eof create
40    pub is_eof_init: bool,
41    /// Shared memory.
42    ///
43    /// Note: This field is only set while running the interpreter loop.
44    /// Otherwise it is taken and replaced with empty shared memory.
45    pub shared_memory: SharedMemory,
46    /// Stack.
47    pub stack: Stack,
48    /// EOF function stack.
49    pub function_stack: FunctionStack,
50    /// The return data buffer for internal calls.
51    /// It has multi usage:
52    ///
53    /// * It contains the output bytes of call sub call.
54    /// * When this interpreter finishes execution it contains the output bytes of this contract.
55    pub return_data_buffer: Bytes,
56    /// Whether the interpreter is in "staticcall" mode, meaning no state changes can happen.
57    pub is_static: bool,
58    /// Actions that the EVM should do.
59    ///
60    /// Set inside CALL or CREATE instructions and RETURN or REVERT instructions. Additionally those instructions will set
61    /// InstructionResult to CallOrCreate/Return/Revert so we know the reason.
62    pub next_action: InterpreterAction,
63}
64
65impl Default for Interpreter {
66    fn default() -> Self {
67        Self::new(Contract::default(), 0, false)
68    }
69}
70
71/// The result of an interpreter operation.
72#[derive(Clone, Debug, PartialEq, Eq)]
73#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
74pub struct InterpreterResult {
75    /// The result of the instruction execution.
76    pub result: InstructionResult,
77    /// The output of the instruction execution.
78    pub output: Bytes,
79    /// The gas usage information.
80    pub gas: Gas,
81}
82
83impl Interpreter {
84    /// Create new interpreter
85    pub fn new(contract: Contract, gas_limit: u64, is_static: bool) -> Self {
86        if !contract.bytecode.is_execution_ready() {
87            panic!("Contract is not execution ready {:?}", contract.bytecode);
88        }
89        let is_eof = contract.bytecode.is_eof();
90        let bytecode = contract.bytecode.bytecode_bytes();
91        Self {
92            instruction_pointer: bytecode.as_ptr(),
93            bytecode,
94            contract,
95            gas: Gas::new(gas_limit),
96            instruction_result: InstructionResult::Continue,
97            function_stack: FunctionStack::default(),
98            is_static,
99            is_eof,
100            is_eof_init: false,
101            return_data_buffer: Bytes::new(),
102            shared_memory: EMPTY_SHARED_MEMORY,
103            stack: Stack::new(),
104            next_action: InterpreterAction::None,
105        }
106    }
107
108    /// Set set is_eof_init to true, this is used to enable `RETURNCONTRACT` opcode.
109    #[inline]
110    pub fn set_is_eof_init(&mut self) {
111        self.is_eof_init = true;
112    }
113
114    #[inline]
115    pub fn eof(&self) -> Option<&Eof> {
116        self.contract.bytecode.eof()
117    }
118
119    /// Test related helper
120    #[cfg(test)]
121    pub fn new_bytecode(bytecode: Bytecode) -> Self {
122        Self::new(
123            Contract::new(
124                Bytes::new(),
125                bytecode,
126                None,
127                crate::primitives::Address::default(),
128                crate::primitives::Address::default(),
129                U256::ZERO,
130            ),
131            0,
132            false,
133        )
134    }
135
136    /// Load EOF code into interpreter. PC is assumed to be correctly set
137    pub(crate) fn load_eof_code(&mut self, idx: usize, pc: usize) {
138        // SAFETY: eof flag is true only if bytecode is Eof.
139        let Bytecode::Eof(eof) = &self.contract.bytecode else {
140            panic!("Expected EOF bytecode")
141        };
142        let Some(code) = eof.body.code(idx) else {
143            panic!("Code not found")
144        };
145        self.bytecode = code.clone();
146        self.instruction_pointer = unsafe { self.bytecode.as_ptr().add(pc) };
147    }
148
149    /// Inserts the output of a `create` call into the interpreter.
150    ///
151    /// This function is used after a `create` call has been executed. It processes the outcome
152    /// of that call and updates the state of the interpreter accordingly.
153    ///
154    /// # Arguments
155    ///
156    /// * `create_outcome` - A `CreateOutcome` struct containing the results of the `create` call.
157    ///
158    /// # Behavior
159    ///
160    /// The function updates the `return_data_buffer` with the data from `create_outcome`.
161    /// Depending on the `InstructionResult` indicated by `create_outcome`, it performs one of the following:
162    ///
163    /// - `Ok`: Pushes the address from `create_outcome` to the stack, updates gas costs, and records any gas refunds.
164    /// - `Revert`: Pushes `U256::ZERO` to the stack and updates gas costs.
165    /// - `FatalExternalError`: Sets the `instruction_result` to `InstructionResult::FatalExternalError`.
166    /// - `Default`: Pushes `U256::ZERO` to the stack.
167    ///
168    /// # Side Effects
169    ///
170    /// - Updates `return_data_buffer` with the data from `create_outcome`.
171    /// - Modifies the stack by pushing values depending on the `InstructionResult`.
172    /// - Updates gas costs and records refunds in the interpreter's `gas` field.
173    /// - May alter `instruction_result` in case of external errors.
174    pub fn insert_create_outcome(&mut self, create_outcome: CreateOutcome) {
175        self.instruction_result = InstructionResult::Continue;
176
177        let instruction_result = create_outcome.instruction_result();
178        self.return_data_buffer = if instruction_result.is_revert() {
179            // Save data to return data buffer if the create reverted
180            create_outcome.output().to_owned()
181        } else {
182            // Otherwise clear it
183            Bytes::new()
184        };
185
186        match instruction_result {
187            return_ok!() => {
188                let address = create_outcome.address;
189                push_b256!(self, address.unwrap_or_default().into_word());
190                self.gas.erase_cost(create_outcome.gas().remaining());
191                self.gas.record_refund(create_outcome.gas().refunded());
192            }
193            return_revert!() => {
194                push!(self, U256::ZERO);
195                self.gas.erase_cost(create_outcome.gas().remaining());
196            }
197            InstructionResult::FatalExternalError => {
198                panic!("Fatal external error in insert_create_outcome");
199            }
200            _ => {
201                push!(self, U256::ZERO);
202            }
203        }
204    }
205
206    pub fn insert_eofcreate_outcome(&mut self, create_outcome: EOFCreateOutcome) {
207        let instruction_result = create_outcome.instruction_result();
208
209        self.return_data_buffer = if *instruction_result == InstructionResult::Revert {
210            // Save data to return data buffer if the create reverted
211            create_outcome.output().to_owned()
212        } else {
213            // Otherwise clear it. Note that RETURN opcode should abort.
214            Bytes::new()
215        };
216
217        match instruction_result {
218            InstructionResult::ReturnContract => {
219                push_b256!(self, create_outcome.address.into_word());
220                self.gas.erase_cost(create_outcome.gas().remaining());
221                self.gas.record_refund(create_outcome.gas().refunded());
222            }
223            return_revert!() => {
224                push!(self, U256::ZERO);
225                self.gas.erase_cost(create_outcome.gas().remaining());
226            }
227            InstructionResult::FatalExternalError => {
228                panic!("Fatal external error in insert_eofcreate_outcome");
229            }
230            _ => {
231                push!(self, U256::ZERO);
232            }
233        }
234    }
235
236    /// Inserts the outcome of a call into the virtual machine's state.
237    ///
238    /// This function takes the result of a call, represented by `CallOutcome`,
239    /// and updates the virtual machine's state accordingly. It involves updating
240    /// the return data buffer, handling gas accounting, and setting the memory
241    /// in shared storage based on the outcome of the call.
242    ///
243    /// # Arguments
244    ///
245    /// * `shared_memory` - A mutable reference to the shared memory used by the virtual machine.
246    /// * `call_outcome` - The outcome of the call to be processed, containing details such as
247    ///   instruction result, gas information, and output data.
248    ///
249    /// # Behavior
250    ///
251    /// The function first copies the output data from the call outcome to the virtual machine's
252    /// return data buffer. It then checks the instruction result from the call outcome:
253    ///
254    /// - `return_ok!()`: Processes successful execution, refunds gas, and updates shared memory.
255    /// - `return_revert!()`: Handles a revert by only updating the gas usage and shared memory.
256    /// - `InstructionResult::FatalExternalError`: Sets the instruction result to a fatal external error.
257    /// - Any other result: No specific action is taken.
258    pub fn insert_call_outcome(
259        &mut self,
260        shared_memory: &mut SharedMemory,
261        call_outcome: CallOutcome,
262    ) {
263        self.instruction_result = InstructionResult::Continue;
264        self.return_data_buffer.clone_from(call_outcome.output());
265
266        let out_offset = call_outcome.memory_start();
267        let out_len = call_outcome.memory_length();
268
269        let target_len = min(out_len, self.return_data_buffer.len());
270        match call_outcome.instruction_result() {
271            return_ok!() => {
272                // return unspend gas.
273                let remaining = call_outcome.gas().remaining();
274                let refunded = call_outcome.gas().refunded();
275                self.gas.erase_cost(remaining);
276                self.gas.record_refund(refunded);
277                shared_memory.set(out_offset, &self.return_data_buffer[..target_len]);
278                push!(self, U256::from(1));
279            }
280            return_revert!() => {
281                self.gas.erase_cost(call_outcome.gas().remaining());
282                shared_memory.set(out_offset, &self.return_data_buffer[..target_len]);
283                push!(self, U256::ZERO);
284            }
285            InstructionResult::FatalExternalError => {
286                panic!("Fatal external error in insert_call_outcome");
287            }
288            _ => {
289                push!(self, U256::ZERO);
290            }
291        }
292    }
293
294    /// Returns the opcode at the current instruction pointer.
295    #[inline]
296    pub fn current_opcode(&self) -> u8 {
297        unsafe { *self.instruction_pointer }
298    }
299
300    /// Returns a reference to the contract.
301    #[inline]
302    pub fn contract(&self) -> &Contract {
303        &self.contract
304    }
305
306    /// Returns a reference to the interpreter's gas state.
307    #[inline]
308    pub fn gas(&self) -> &Gas {
309        &self.gas
310    }
311
312    /// Returns a reference to the interpreter's stack.
313    #[inline]
314    pub fn stack(&self) -> &Stack {
315        &self.stack
316    }
317
318    /// Returns the current program counter.
319    #[inline]
320    pub fn program_counter(&self) -> usize {
321        // SAFETY: `instruction_pointer` should be at an offset from the start of the bytecode.
322        // In practice this is always true unless a caller modifies the `instruction_pointer` field manually.
323        unsafe { self.instruction_pointer.offset_from(self.bytecode.as_ptr()) as usize }
324    }
325
326    /// Executes the instruction at the current instruction pointer.
327    ///
328    /// Internally it will increment instruction pointer by one.
329    #[inline]
330    pub(crate) fn step<FN, H: Host + ?Sized>(&mut self, instruction_table: &[FN; 256], host: &mut H)
331    where
332        FN: Fn(&mut Interpreter, &mut H),
333    {
334        // Get current opcode.
335        let opcode = unsafe { *self.instruction_pointer };
336
337        // SAFETY: In analysis we are doing padding of bytecode so that we are sure that last
338        // byte instruction is STOP so we are safe to just increment program_counter bcs on last instruction
339        // it will do noop and just stop execution of this contract
340        self.instruction_pointer = unsafe { self.instruction_pointer.offset(1) };
341
342        // execute instruction.
343        (instruction_table[opcode as usize])(self, host)
344    }
345
346    /// Take memory and replace it with empty memory.
347    pub fn take_memory(&mut self) -> SharedMemory {
348        core::mem::replace(&mut self.shared_memory, EMPTY_SHARED_MEMORY)
349    }
350
351    /// Executes the interpreter until it returns or stops.
352    pub fn run<FN, H: Host + ?Sized>(
353        &mut self,
354        shared_memory: SharedMemory,
355        instruction_table: &[FN; 256],
356        host: &mut H,
357    ) -> InterpreterAction
358    where
359        FN: Fn(&mut Interpreter, &mut H),
360    {
361        self.next_action = InterpreterAction::None;
362        self.shared_memory = shared_memory;
363        // main loop
364        while self.instruction_result == InstructionResult::Continue {
365            self.step(instruction_table, host);
366        }
367
368        // Return next action if it is some.
369        if self.next_action.is_some() {
370            return core::mem::take(&mut self.next_action);
371        }
372        // If not, return action without output as it is a halt.
373        InterpreterAction::Return {
374            result: InterpreterResult {
375                result: self.instruction_result,
376                // return empty bytecode
377                output: Bytes::new(),
378                gas: self.gas,
379            },
380        }
381    }
382}
383
384impl InterpreterResult {
385    /// Returns whether the instruction result is a success.
386    #[inline]
387    pub const fn is_ok(&self) -> bool {
388        self.result.is_ok()
389    }
390
391    /// Returns whether the instruction result is a revert.
392    #[inline]
393    pub const fn is_revert(&self) -> bool {
394        self.result.is_revert()
395    }
396
397    /// Returns whether the instruction result is an error.
398    #[inline]
399    pub const fn is_error(&self) -> bool {
400        self.result.is_error()
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::{opcode::InstructionTable, DummyHost};
408    use rtvm_primitives::CancunSpec;
409
410    #[test]
411    fn object_safety() {
412        let mut interp = Interpreter::new(Contract::default(), u64::MAX, false);
413
414        let mut host = crate::DummyHost::default();
415        let table: InstructionTable<DummyHost> =
416            crate::opcode::make_instruction_table::<DummyHost, CancunSpec>();
417        let _ = interp.run(EMPTY_SHARED_MEMORY, &table, &mut host);
418
419        let host: &mut dyn Host = &mut host as &mut dyn Host;
420        let table: InstructionTable<dyn Host> =
421            crate::opcode::make_instruction_table::<dyn Host, CancunSpec>();
422        let _ = interp.run(EMPTY_SHARED_MEMORY, &table, host);
423    }
424}