Skip to main content

fluentbase_runtime/
executor.rs

1use crate::{
2    module_factory::ModuleFactory,
3    runtime::{ContractRuntime, ExecutionMode, SystemRuntime},
4    RuntimeContext,
5};
6use fluentbase_types::{
7    byteorder::{ByteOrder, LittleEndian},
8    import_linker_v1_preview, Address, BytecodeOrHash, ExitCode, HashMap, B256,
9};
10use rwasm::{ExecutionEngine, ImportLinker, RwasmModule, StrategyDefinition, TrapCode};
11use std::{cell::RefCell, mem::take, sync::Arc};
12
13/// Finalized outcome of a single runtime invocation.
14///
15/// Values are reported in fuel units; gas conversion (if any) is handled by the caller.
16#[derive(Default, Clone, Debug)]
17pub struct ExecutionResult {
18    /// Contract-defined exit status. Negative values map from TrapCode via ExitCode; zero indicates success.
19    pub exit_code: i32,
20    /// Total fuel consumed by the invocation (excludes refunded fuel).
21    pub fuel_consumed: u64,
22    /// Fuel refunded to the caller (negative values are not expected).
23    pub fuel_refunded: i64,
24    /// Raw output buffer produced by the callee; for nested calls it is moved into the parent's return_data.
25    pub output: Vec<u8>,
26    /// Return data propagated back to the parent on success paths of nested calls.
27    pub return_data: Vec<u8>,
28}
29
30impl ExecutionResult {
31    pub fn take_and_continue(&mut self, is_interrupted: bool) -> Self {
32        let mut result = take(self);
33        // We don't propagate output into an intermediary state
34        if is_interrupted {
35            self.output = take(&mut result.output);
36            self.return_data = take(&mut result.return_data);
37        }
38        result
39    }
40}
41
42/// Captures an intentional execution interruption that must be resumed by the root context.
43#[derive(Debug, Default, Clone)]
44pub struct ExecutionInterruption {
45    /// Fuel spent up to the interruption point.
46    pub fuel_consumed: u64,
47    /// Fuel to refund to the caller at the interruption point.
48    pub fuel_refunded: i64,
49    /// Encoded interruption payload (e.g., delegated call parameters).
50    pub return_data: Vec<u8>,
51}
52
53/// Result of running or resuming a runtime.
54#[derive(Clone, Debug)]
55pub enum RuntimeResult {
56    /// Execution finished; contains the finalized result.
57    Result(ExecutionResult),
58    /// Execution yielded; contains data necessary to resume later.
59    Interruption(ExecutionInterruption),
60}
61
62impl RuntimeResult {
63    /// Unwraps the successful execution result; panics if this is an interruption.
64    pub fn into_execution_result(self) -> ExecutionResult {
65        match self {
66            RuntimeResult::Result(result) => result,
67            _ => unreachable!(),
68        }
69    }
70}
71
72pub trait RuntimeExecutor {
73    /// Executes the entry function of the module determined by the current execution state.
74    ///
75    /// Returns either a finalized result.
76    fn execute(&mut self, bytecode_or_hash: BytecodeOrHash, ctx: RuntimeContext)
77        -> ExecutionResult;
78
79    /// Resumes a previously interrupted runtime.
80    ///
81    /// `fuel16_ptr` optionally points to a 16-byte buffer where fuel consumption and refund are written back.
82    fn resume(
83        &mut self,
84        call_id: u32,
85        return_data: &[u8],
86        fuel16_ptr: u32,
87        fuel_consumed: u64,
88        fuel_refunded: i64,
89        exit_code: i32,
90    ) -> ExecutionResult;
91
92    /// Drop a runtime we don't need to resume anymore
93    fn forget_runtime(&mut self, call_id: u32);
94
95    /// Warm up the bytecode
96    fn warmup(&mut self, bytecode: RwasmModule, hash: B256, address: Address);
97
98    /// Resets the per-transaction call identifier counter and clears recoverable runtimes.
99    ///
100    /// Intended to be invoked at the beginning of a new transaction.
101    fn reset_call_id_counter(&mut self);
102
103    fn memory_read(
104        &mut self,
105        call_id: u32,
106        offset: usize,
107        buffer: &mut [u8],
108    ) -> Result<(), TrapCode>;
109}
110
111pub struct ThreadLocalExecutor;
112
113thread_local! {
114    pub static LOCAL_RUNTIME_EXECUTOR: RefCell<RuntimeFactoryExecutor> = RefCell::new(RuntimeFactoryExecutor::new(import_linker_v1_preview()));
115}
116
117impl RuntimeExecutor for ThreadLocalExecutor {
118    fn execute(
119        &mut self,
120        bytecode_or_hash: BytecodeOrHash,
121        ctx: RuntimeContext,
122    ) -> ExecutionResult {
123        LOCAL_RUNTIME_EXECUTOR
124            .with_borrow_mut(|runtime_executor| runtime_executor.execute(bytecode_or_hash, ctx))
125    }
126
127    fn resume(
128        &mut self,
129        call_id: u32,
130        return_data: &[u8],
131        fuel16_ptr: u32,
132        fuel_consumed: u64,
133        fuel_refunded: i64,
134        exit_code: i32,
135    ) -> ExecutionResult {
136        LOCAL_RUNTIME_EXECUTOR.with_borrow_mut(|runtime_executor| {
137            runtime_executor.resume(
138                call_id,
139                return_data,
140                fuel16_ptr,
141                fuel_consumed,
142                fuel_refunded,
143                exit_code,
144            )
145        })
146    }
147
148    fn forget_runtime(&mut self, call_id: u32) {
149        LOCAL_RUNTIME_EXECUTOR
150            .with_borrow_mut(|runtime_executor| runtime_executor.forget_runtime(call_id))
151    }
152
153    fn warmup(&mut self, bytecode: RwasmModule, hash: B256, address: Address) {
154        LOCAL_RUNTIME_EXECUTOR
155            .with_borrow_mut(|runtime_executor| runtime_executor.warmup(bytecode, hash, address))
156    }
157
158    fn reset_call_id_counter(&mut self) {
159        LOCAL_RUNTIME_EXECUTOR
160            .with_borrow_mut(|runtime_executor| runtime_executor.reset_call_id_counter())
161    }
162
163    fn memory_read(
164        &mut self,
165        call_id: u32,
166        offset: usize,
167        buffer: &mut [u8],
168    ) -> Result<(), TrapCode> {
169        LOCAL_RUNTIME_EXECUTOR.with_borrow_mut(|runtime_executor| {
170            runtime_executor.memory_read(call_id, offset, buffer)
171        })
172    }
173}
174
175/// Returns a default runtime executor.
176pub fn default_runtime_executor() -> impl RuntimeExecutor {
177    ThreadLocalExecutor {}
178}
179
180pub struct RuntimeFactoryExecutor {
181    /// A module factory
182    pub module_factory: ModuleFactory,
183    /// Suspended runtimes keyed by per-transaction call identifier.
184    pub recoverable_runtimes: HashMap<u32, ExecutionMode>,
185    /// An import linker
186    pub import_linker: Arc<ImportLinker>,
187    /// Monotonically increasing counter for assigning call identifiers.
188    pub transaction_call_id_counter: u32,
189}
190
191impl RuntimeFactoryExecutor {
192    pub fn new(import_linker: Arc<ImportLinker>) -> Self {
193        Self {
194            module_factory: ModuleFactory::new(),
195            recoverable_runtimes: HashMap::new(),
196            import_linker,
197            transaction_call_id_counter: 1,
198        }
199    }
200
201    /// Saves the current runtime instance for later resumption and returns its call identifier.
202    pub fn try_remember_runtime(
203        &mut self,
204        runtime_result: RuntimeResult,
205        runtime: ExecutionMode,
206    ) -> ExecutionResult {
207        let interruption = match runtime_result {
208            RuntimeResult::Result(result) => {
209                // Return result (there is no need to do anything else)
210                return result;
211            }
212            RuntimeResult::Interruption(interruption) => interruption,
213        };
214
215        // Get current call_id before incrementing
216        let call_id = self.transaction_call_id_counter;
217
218        // Check if call_id would overflow i32 when cast (positive exit codes are reserved for call_id)
219        if call_id > i32::MAX as u32 {
220            return ExecutionResult {
221                exit_code: ExitCode::UnknownError.into_i32(),
222                fuel_consumed: interruption.fuel_consumed,
223                fuel_refunded: interruption.fuel_refunded,
224                output: vec![],
225                return_data: vec![],
226            };
227        }
228
229        // Increment counter for next call (safe since call_id <= i32::MAX < u32::MAX)
230        self.transaction_call_id_counter += 1;
231        let prev = self.recoverable_runtimes.insert(call_id, runtime);
232        debug_assert!(prev.is_none());
233
234        ExecutionResult {
235            // We return `call_id` as exit code (it's safe because exit code can't be positive)
236            exit_code: call_id as i32,
237            // Forward info about consumed and refunded fuel (during the call)
238            fuel_consumed: interruption.fuel_consumed,
239            fuel_refunded: interruption.fuel_refunded,
240            // The output we map into return data
241            output: interruption.return_data,
242            return_data: vec![],
243        }
244    }
245
246    /// Consolidates the trap/result of an invocation into a RuntimeResult and updates accounting.
247    ///
248    /// When fuel_consumed_before_the_call is provided, computes precise fuel usage by diffing the
249    /// store's remaining fuel. Returns either a finalized result or an interruption wrapper.
250    fn handle_execution_result(
251        &mut self,
252        next_result: Result<(), TrapCode>,
253        fuel_consumed: Option<u64>,
254        ctx: &mut RuntimeContext,
255    ) -> RuntimeResult {
256        let mut execution_result = ctx
257            .execution_result
258            .take_and_continue(ctx.resumable_context.is_some());
259        // There are two counters for fuel: opcode fuel counter; manually charged.
260        // It's applied for execution runtimes where we don't know the final fuel consumed
261        // till it's committed by Wasm runtime.
262        // That is why we rewrite fuel here to check how much we've really spent based on the context information.
263        if let Some(store_fuel_consumed) = fuel_consumed {
264            execution_result.fuel_consumed = store_fuel_consumed;
265        }
266
267        // Fill the exit code in the execution result based on the next result:
268        // - Ok - execution passed, exit code is 0 (Ok)
269        // - InterruptionCalled - we don't know exit code since it's just an interruption
270        // - Err - an execution trap code (halts execution)
271        match next_result {
272            Ok(_) => {
273                // Don't write exit code here, because it's managed by host functions
274            }
275            Err(TrapCode::InterruptionCalled) => {
276                // We don't set exit code here,
277                // because exit code is used to represent identifier of call id
278            }
279            Err(err) => {
280                execution_result.exit_code = ExitCode::from(err).into_i32();
281            }
282        }
283
284        // If the next result is interruption
285        if next_result == Err(TrapCode::InterruptionCalled) {
286            let ExecutionResult {
287                fuel_consumed,
288                fuel_refunded,
289                mut return_data,
290                ..
291            } = execution_result;
292            // A case for normal interruption (not system runtime interruption), where we should
293            // serialize the context we remembered inside the `exec.rs `
294            // handler to pass into parent runtime.
295            //
296            // Safety: For system runtimes we don't save this
297            if let Some(resumable_return_data) = ctx.take_resumable_context_serialized() {
298                return_data = resumable_return_data;
299            }
300            return RuntimeResult::Interruption(ExecutionInterruption {
301                fuel_consumed,
302                fuel_refunded,
303                return_data,
304            });
305        }
306
307        RuntimeResult::Result(execution_result)
308    }
309}
310impl RuntimeExecutor for RuntimeFactoryExecutor {
311    fn execute(
312        &mut self,
313        bytecode_or_hash: BytecodeOrHash,
314        ctx: RuntimeContext,
315    ) -> ExecutionResult {
316        let system_runtime_params = match &bytecode_or_hash {
317            BytecodeOrHash::Bytecode { address, hash, .. } => {
318                fluentbase_types::is_execute_using_system_runtime(address)
319                    .then_some((*address, *hash))
320            }
321            BytecodeOrHash::Hash(_) => None,
322        };
323
324        // If we have a cached module, then use it, otherwise create a new one and cache
325        let module = self.module_factory.get_module_or_init(bytecode_or_hash);
326
327        // If there is no cached store, then construct a new one (slow)
328        let fuel_limit_value = ctx.fuel_limit;
329        let fuel_limit = Some(fuel_limit_value);
330
331        let mut exec_mode = if let Some((address, code_hash)) = system_runtime_params {
332            let consume_fuel = fluentbase_types::is_engine_metered_precompile(&address);
333            let runtime = SystemRuntime::new(
334                module,
335                self.import_linker.clone(),
336                code_hash,
337                ctx,
338                consume_fuel,
339            );
340            ExecutionMode::System(runtime)
341        } else {
342            let engine = ExecutionEngine::acquire_shared();
343            // We always execute untrusted contracts with rWasm VM
344            let strategy = StrategyDefinition::Rwasm { engine, module };
345            let runtime =
346                ContractRuntime::new(strategy, self.import_linker.clone(), ctx, fuel_limit);
347            // This is an extraordinary case where we fail during resource init inside the entrypoint,
348            // but there is nothing we can do here rather than just return the execution error.
349            //
350            // By default, start sections are not allowed for users, so users can't deploy contracts that
351            // cause traps inside the entrypoint.
352            //
353            // Ideally, it should never happen.
354            if let Some(trap_code) = runtime.as_ref().err() {
355                return ExecutionResult {
356                    exit_code: ExitCode::from(trap_code).into_i32(),
357                    fuel_consumed: fuel_limit_value,
358                    fuel_refunded: 0,
359                    output: vec![],
360                    return_data: vec![],
361                };
362            }
363            ExecutionMode::Contract(runtime.unwrap())
364        };
365
366        // Execute program
367        let result = exec_mode.execute();
368        let fuel_consumed = exec_mode
369            .remaining_fuel()
370            .zip(fuel_limit)
371            .map(|(remaining_fuel, store_fuel)| store_fuel - remaining_fuel);
372
373        let runtime_result =
374            self.handle_execution_result(result, fuel_consumed, exec_mode.context_mut());
375        self.try_remember_runtime(runtime_result, exec_mode)
376    }
377
378    fn resume(
379        &mut self,
380        call_id: u32,
381        return_data: &[u8],
382        fuel16_ptr: u32,
383        fuel_consumed: u64,
384        fuel_refunded: i64,
385        exit_code: i32,
386    ) -> ExecutionResult {
387        let Some(mut runtime) = self.recoverable_runtimes.remove(&call_id) else {
388            unreachable!(
389                "runtime: missing recoverable runtime for resume, this should never happen: call_id={}, fuel_consumed={}, exit_code={}",
390                call_id, fuel_consumed, exit_code
391            )
392        };
393        let mut fuel_remaining = runtime.remaining_fuel();
394        let resume_inner = |runtime: &mut ExecutionMode| {
395            // Copy return data into return data
396            runtime.context_mut().execution_result.return_data = return_data.to_vec();
397            if fuel16_ptr > 0 {
398                let mut buffer = [0u8; 16];
399                LittleEndian::write_u64(&mut buffer[..8], fuel_consumed);
400                LittleEndian::write_i64(&mut buffer[8..], fuel_refunded);
401                runtime.memory_write(fuel16_ptr as usize, &buffer)?;
402            }
403            runtime.resume(exit_code, fuel_consumed)
404        };
405        let result = resume_inner(&mut runtime);
406        // We need to adjust the fuel limit because `fuel_consumed` should not be included into spent.
407        if result != Err(TrapCode::OutOfFuel) {
408            // Safety: We can safely unwrap here, because `OutOfFuel` check we did in `resume_inner` and the result is ok.
409            fuel_remaining = fuel_remaining.map(|v| v.checked_sub(fuel_consumed).unwrap());
410        }
411        let fuel_consumed = runtime
412            .remaining_fuel()
413            .and_then(|remaining_fuel| Some(fuel_remaining? - remaining_fuel));
414        let runtime_result =
415            self.handle_execution_result(result, fuel_consumed, runtime.context_mut());
416        self.try_remember_runtime(runtime_result, runtime)
417    }
418
419    fn forget_runtime(&mut self, call_id: u32) {
420        _ = self.recoverable_runtimes.remove(&call_id);
421    }
422
423    fn warmup(&mut self, bytecode: RwasmModule, hash: B256, address: Address) {
424        self.module_factory
425            .get_module_or_init(BytecodeOrHash::Bytecode {
426                bytecode,
427                hash,
428                address,
429            });
430    }
431
432    fn reset_call_id_counter(&mut self) {
433        // For each transaction we reset the `call_id` counter (used to track interruptions)
434        self.transaction_call_id_counter = 1;
435        // Clear recoverable runtimes, because they are no longer valid
436        self.recoverable_runtimes.clear();
437    }
438
439    fn memory_read(
440        &mut self,
441        call_id: u32,
442        offset: usize,
443        buffer: &mut [u8],
444    ) -> Result<(), TrapCode> {
445        let runtime_ref = self.recoverable_runtimes.get_mut(&call_id).expect(
446            "runtime: missing recoverable runtime for memory read, this should never happen",
447        );
448        runtime_ref.memory_read(offset, buffer)
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use crate::{
455        executor::{ExecutionInterruption, RuntimeFactoryExecutor, RuntimeResult},
456        runtime::{ContractRuntime, ExecutionMode},
457        RuntimeContext,
458    };
459    use fluentbase_types::{import_linker_v1_preview, ExitCode};
460    use rwasm::{ExecutionEngine, RwasmModule, StrategyDefinition};
461
462    #[test]
463    fn call_id_overflow() {
464        let mut executor = RuntimeFactoryExecutor::new(import_linker_v1_preview());
465
466        // Set counter to i32::MAX to trigger overflow on the next allocation
467        executor.transaction_call_id_counter = i32::MAX as u32 + 1;
468
469        let interruption = RuntimeResult::Interruption(ExecutionInterruption {
470            fuel_consumed: 100,
471            fuel_refunded: 0,
472            return_data: vec![1, 2, 3],
473        });
474
475        let engine = ExecutionEngine::acquire_shared();
476        let module = RwasmModule::default();
477        let ctx = RuntimeContext::default();
478
479        let strategy_runtime = ContractRuntime::new(
480            StrategyDefinition::Rwasm { module, engine },
481            executor.import_linker.clone(),
482            ctx,
483            None,
484        )
485        .unwrap();
486        let runtime = ExecutionMode::Contract(strategy_runtime);
487
488        // Try to allocate call_id - should fail with overflow
489        let result = executor.try_remember_runtime(interruption, runtime);
490
491        // Verify overflow error
492        assert_eq!(result.exit_code, ExitCode::UnknownError.into_i32());
493        assert_eq!(result.fuel_consumed, 100);
494        assert_eq!(result.fuel_refunded, 0);
495        assert!(result.output.is_empty());
496    }
497}