Skip to main content

fluentbase_runtime/runtime/
system_runtime.rs

1//! System runtime backed by Wasmtime.
2//!
3//! This module implements **system runtimes** (trusted, privileged rWasm programs) executed via
4//! Wasmtime. The key difference from `ContractRuntime` is that system runtimes:
5//! - may be reused across multiple calls (store/instance caching),
6//! - signal "soft exits" via *returned output* rather than trapping/unwinding.
7//!
8//! ## Fuel metering modes
9//!
10//! System runtimes support two fuel metering strategies:
11//!
12//! 1. **Self-metering** (`consume_fuel=false`): The contract manages fuel internally by calling
13//!    the `_charge_fuel` syscall. Wasmtime fuel metering is disabled. This is used by runtimes
14//!    like EVM_RUNTIME and SVM_RUNTIME that have their own gas accounting.
15//!
16//! 2. **Engine-metered** (`consume_fuel=true`): Wasmtime automatically meters fuel for both
17//!    wasm instructions and builtin syscalls. This is used by precompiles that don't self-meter:
18//!    NITRO_VERIFIER, OAUTH2_VERIFIER, WASM_RUNTIME, WEBAUTHN_VERIFIER.
19//!
20//! ## Why "return output" instead of trapping?
21//! Some system runtimes intentionally avoid `trap` / `halt` paths because they do not always unwind
22//! the stack in the desired way for this environment. Instead, they return an output buffer where
23//! the first 4 bytes are the little-endian `exit_code`, and the remaining bytes are the payload.
24//!
25//! ## Caching model
26//! - `COMPILED_MODULES` caches compiled `wasmtime::Module` by code hash globally.
27//! - `COMPILED_RUNTIMES` caches instantiated `CompiledRuntime` per thread (thread-local) by code hash.
28//!
29//! The store is reused, so the `RuntimeContext` is swapped in/out on every call.
30
31use crate::{syscall_handler::runtime_syscall_handler, RuntimeContext};
32use alloc::sync::Arc;
33use core::{cell::RefCell, mem::take};
34use fluentbase_types::{ExitCode, HashMap, SysFuncIdx, B256, STATE_DEPLOY, STATE_MAIN};
35use rwasm::{
36    CompilationConfig, ImportLinker, Opcode, RwasmModule, StateRouterConfig, StoreTr,
37    StrategyDefinition, StrategyExecutor, TrapCode, Value, N_MAX_ALLOWED_MEMORY_PAGES,
38};
39
40/// A system runtime instance.
41///
42/// System runtimes are **trusted** programs executed through Wasmtime and reused across calls.
43/// This wrapper owns:
44/// - a cached, compiled+instantiated Wasmtime runtime (`CompiledRuntime`),
45/// - the per-call `RuntimeContext` (`ctx`) which is swapped into the cached store on execution,
46/// - an optional resumable interruption state used when system runtimes request an interruption.
47/// - a flag indicating whether Wasmtime fuel metering is enabled (`consume_fuel`).
48///
49/// The runtime is keyed by `code_hash` so that we can cache compiled artifacts and instances.
50pub struct SystemRuntime {
51    /// Cached compiled runtime (Module + Store + Instance + Memory + entry functions).
52    ///
53    /// NOTE: This is currently not `no_std` friendly due to Wasmtime usage.
54    /// The intent is to replace/relax this once rWasm ships an optimized embedded backend.
55    compiled_runtime: Arc<RefCell<CompiledRuntime>>,
56
57    /// Per-call execution context.
58    ///
59    /// This context is swapped into the cached store before execution and swapped back after,
60    /// so that a single cached store/instance can serve multiple contract calls sequentially.
61    ctx: RuntimeContext,
62
63    /// Code hash of the system runtime program.
64    ///
65    /// Used as a cache key for both compiled modules and instantiated runtimes.
66    code_hash: B256,
67
68    /// Whether Wasmtime fuel metering is enabled for this runtime.
69    ///
70    /// When `true`, the engine automatically charges fuel for wasm instructions and syscalls.
71    /// When `false`, the contract is expected to self-meter via `_charge_fuel` syscall.
72    consume_fuel: bool,
73}
74
75/// Fully initialized compiled runtime artifacts.
76///
77/// This structure is cached and reused. It contains:
78/// - a compiled Wasmtime `Module`,
79/// - a `Store<RuntimeContext>` holding runtime state and a swap-in context,
80/// - an instantiated `Instance` and its exported memory,
81/// - cached exported entry functions.
82type CompiledRuntime = StrategyExecutor<RuntimeContext>;
83
84thread_local! {
85    /// Thread-local cache of fully instantiated runtimes keyed by code hash.
86    ///
87    /// We keep this thread-local because Wasmtime components are not generally inexpensive to share across
88    /// threads without careful synchronization, and because per-thread reuse is often enough.
89    pub static COMPILED_RUNTIMES: RefCell<HashMap<B256, Arc<RefCell<CompiledRuntime>>>> =
90        RefCell::new(HashMap::new());
91}
92
93impl SystemRuntime {
94    /// Clears the per-thread cache of instantiated runtimes.
95    ///
96    /// Useful in tests or when a process needs to drop cached instances (e.g. after an upgrade).
97    pub fn reset_cached_runtimes() {
98        COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
99            compiled_runtimes.clear();
100        });
101    }
102
103    /// Creates a new `SystemRuntime`.
104    ///
105    /// If a compiled runtime for `code_hash` is present in the thread-local cache, it will be reused.
106    /// Otherwise, this function compiles/loads the module and instantiates it with imports wired via
107    /// `import_linker`.
108    ///
109    /// ## Fuel metering
110    ///
111    /// The `consume_fuel` parameter determines whether Wasmtime fuel metering is enabled:
112    /// - `true`: Engine automatically meters fuel (for NITRO, OAUTH2, WASM_RUNTIME, WEBAUTHN)
113    /// - `false`: Contract self-meters via `_charge_fuel` syscall (for EVM_RUNTIME, etc.)
114    pub fn new(
115        rwasm_module: RwasmModule,
116        import_linker: Arc<ImportLinker>,
117        code_hash: B256,
118        ctx: RuntimeContext,
119        consume_fuel: bool,
120    ) -> Self {
121        let compiled_runtime = COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
122            if let Some(compiled_runtime) = compiled_runtimes.get(&code_hash).cloned() {
123                return compiled_runtime;
124            }
125
126            let config = CompilationConfig::default()
127                .with_state_router(StateRouterConfig {
128                    states: Box::new([
129                        ("deploy".into(), STATE_DEPLOY),
130                        ("main".into(), STATE_MAIN),
131                    ]),
132                    opcode: Some(Opcode::Call(SysFuncIdx::STATE as u32)),
133                })
134                .with_import_linker(import_linker.clone())
135                .with_allow_malformed_entrypoint_func_type(true)
136                .with_consume_fuel(consume_fuel)
137                .with_consume_fuel_for_params_and_locals(false)
138                .with_builtins_consume_fuel(false)
139                .with_max_allowed_memory_pages(N_MAX_ALLOWED_MEMORY_PAGES);
140            // `hint_section` contains Wasmtime-compatible wasm bytes for the system runtime.
141            // Any compilation failure here is fatal: genesis/runtime packaging is inconsistent.
142            let typed_module =
143                StrategyDefinition::new(config, &rwasm_module.hint_section, Some(code_hash.0))
144                    .expect("runtime: failed to compile system runtime module");
145            let Ok(executor) = typed_module.create_executor(
146                import_linker,
147                RuntimeContext::default(),
148                runtime_syscall_handler,
149                // We can't set a fuel limit here because it's not known until execution.
150                None,
151                Some(N_MAX_ALLOWED_MEMORY_PAGES),
152            ) else {
153                unreachable!("runtime: failed to create executor for system runtime module")
154            };
155
156            #[allow(clippy::arc_with_non_send_sync)]
157            let compiled_runtime = Arc::new(RefCell::new(executor));
158            compiled_runtimes.insert(code_hash, compiled_runtime.clone());
159            compiled_runtime
160        });
161
162        Self {
163            compiled_runtime,
164            ctx,
165            code_hash,
166            consume_fuel,
167        }
168    }
169
170    /// Executes the system runtime entrypoint and updates `self.ctx.execution_result`.
171    ///
172    /// Execution uses the cached store/instance. Before calling into Wasmtime, we swap
173    /// `self.ctx` into the store to ensure syscalls and state access refer to the correct context.
174    ///
175    /// ## Fuel metering
176    ///
177    /// If `consume_fuel=true`, the fuel limit is set in the store before execution. Wasmtime
178    /// will automatically decrement fuel as instructions execute.
179    ///
180    /// ## Error handling model
181    /// - If Wasmtime traps unexpectedly, we **do not propagate** the trap outward as fatal.
182    ///   Instead, we mark `UnexpectedFatalExecutionFailure` in `execution_result` and return `Ok(())`
183    ///   so the outer executor can treat it as a partially controlled failure.
184    /// - Normal completion is signaled by output where the first 4 bytes are LE `exit_code`.
185    /// - Interruption is requested by returning `ExitCode::InterruptionCalled` in that header.
186    pub fn execute(&mut self) -> Result<(), TrapCode> {
187        let mut compiled_runtime = self.compiled_runtime.borrow_mut();
188
189        // Rewrite runtime context before each call, since we reuse the same store/runtime instance
190        // across multiple calls. We must replace whatever context was left from the previous call.
191        //
192        // Safety: Calls into a cached runtime must be strictly sequential. No reentrancy or
193        // overlapping calls are allowed because we swap a single `RuntimeContext` in/out.
194        core::mem::swap(compiled_runtime.data_mut(), &mut self.ctx);
195
196        // If fuel metering is enabled, set the fuel limit before execution.
197        // The store is reused, so we must reset fuel for each new call.
198        let fuel_limit = compiled_runtime.data().fuel_limit;
199        compiled_runtime.reset_fuel(fuel_limit);
200
201        // Choose an entrypoint based on the current execution state.
202        let entrypoint = match compiled_runtime.data().state {
203            STATE_MAIN => "main",
204            STATE_DEPLOY => "deploy",
205            _ => unreachable!(),
206        };
207
208        let mut output = [Value::I32(0)];
209        // Rust generates a C-style `main(argc: i32, argv: i32) -> i32` signature for wasm targets when `main` returns `i32`.
210        // Even in `no_std`, the toolchain follows the traditional C/WASI ABI convention where `argc` and `argv` are passed
211        // as 32-bit values (wasm pointers).
212        //
213        // We do not use command-line arguments in this environment, so we pass `0, 0` as dummy values.
214        // The generated shim ignores them unless argument handling is explicitly implemented.
215        //
216        // https://reviews.llvm.org/D70700
217        let result = if entrypoint == "main" {
218            compiled_runtime.execute(entrypoint, &[Value::I32(0), Value::I32(0)], &mut output)
219        } else {
220            compiled_runtime.execute(entrypoint, &[], &mut output)
221        };
222        let exit_code = output[0].i32().unwrap();
223
224        // Always swap back immediately after the call, so we keep `self.ctx` authoritative.
225        core::mem::swap(compiled_runtime.data_mut(), &mut self.ctx);
226
227        // The application can return trap code though exit code, we should handle such cases as well
228        if self.ctx.execution_result.exit_code != ExitCode::Ok.into_i32() {
229            // If panic happens, then we can only forward into output
230            if self.ctx.execution_result.exit_code == ExitCode::Panic.into_i32() {
231                eprintln!(
232                    "WARN: system execution panicked: {} (investigate)",
233                    core::str::from_utf8(&self.ctx.execution_result.output)
234                        .unwrap_or("unable to decode UTF-8 panic message")
235                );
236            }
237            // We assume any not `Ok` error can happen, for example, due to OOM (because our EVM runtime is limited with 64mB only),
238            // but we should handle it gracefully if it happens. When it happens, we have a corrupted state: stack and memory.
239            // Ideally, we should terminate the latest frame and expect the caller to continue its execution because we can free
240            // resources we consumed. But here we just terminate the runtime entirely. It means that all previous calls
241            // will fail because they lose their state. It's the best here because we automatically terminate all nested
242            // execution to avoid potential memory or stack access violations.
243            COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
244                compiled_runtimes.remove(&self.code_hash);
245            });
246            // We return `Ok` here because the exit code is already set
247            return Ok(());
248        } else {
249            self.ctx.execution_result.exit_code = exit_code;
250        }
251
252        // If wasmtime trapped, treat it as an unexpected fatal failure and degrade into a safe
253        // error code. This avoids propagating a raw trap across the execution boundary.
254        if let Err(trap_code) = result.as_ref() {
255            // The trap `OutOfFuel` is expected for engine-metered precompiles when fuel is exhausted.
256            if *trap_code == TrapCode::OutOfFuel {
257                // This case is tricky, because if it happens, then we might have corrupted stack and
258                // uncleaned memory. Since we can't handle it gracefully, then we can only reset the existing
259                // runtime to make sure memory and stack are clean. There is no ddos attack here because,
260                // to achieve this, the user must pay a penalty.
261                COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
262                    compiled_runtimes.remove(&self.code_hash);
263                });
264                // Forward the `OutOfFuel` trap to the outer executor, so it can handle it gracefully.
265                return Err(*trap_code);
266            }
267            eprintln!(
268                "runtime: unexpected trap inside system runtime: {:?} ({}) (investigate)",
269                trap_code, trap_code,
270            );
271            self.ctx.execution_result.exit_code =
272                ExitCode::UnexpectedFatalExecutionFailure.into_i32();
273            return Ok(());
274        }
275
276        // If exit code indicates an interruption, convert it into a trap that the outer executor
277        // understands (`TrapCode::InterruptionCalled`).
278        //
279        // Safety: `InterruptionCalled` is expected only from trusted system runtimes.
280        // Untrusted contracts might use the same numeric code but will not be executed here.
281        if exit_code == ExitCode::InterruptionCalled.into_i32() {
282            // Move output into return_data. For system runtimes we don't expose a dedicated
283            // "interrupt params" ABI, so we treat the returned output payload as the interruption
284            // parameters.
285            self.ctx.execution_result.return_data = take(&mut self.ctx.execution_result.output);
286            assert!(
287                !self.ctx.execution_result.return_data.is_empty(),
288                "runtime: interruption payload must not be empty"
289            );
290            return Err(TrapCode::InterruptionCalled);
291        }
292
293        result
294    }
295
296    /// Resumes execution after an interruption.
297    ///
298    /// System runtimes do not support "native" resumable interruptions internally in the same way
299    /// as contract runtimes. Therefore, resume currently re-enters `execute()` after clearing any
300    /// stale output.
301    ///
302    /// Note: `exit_code` and `fuel_consumed` are intentionally ignored here because fuel metering
303    /// is handled by `RuntimeContext`, and exit codes are encoded in returned output.
304    pub fn resume(&mut self, _exit_code: i32, _fuel_consumed: u64) -> Result<(), TrapCode> {
305        // Ensure the output is clear before resuming; output is used to carry interruption params.
306        self.ctx.clear_output();
307
308        // Re-enter execution. Possible scenarios:
309        // 1) With return_data: current frame interruption outcome.
310        // 2) Without return_data: new frame call.
311        self.execute()
312    }
313
314    /// Writes bytes into the system runtime's linear memory.
315    ///
316    /// Bounds violations are mapped into `TrapCode::MemoryOutOfBounds`.
317    pub fn memory_write(&mut self, offset: usize, data: &[u8]) -> Result<(), TrapCode> {
318        let mut compiled_runtime = self.compiled_runtime.borrow_mut();
319        compiled_runtime.memory_write(offset, data)
320    }
321
322    /// Reads bytes from the system runtime's linear memory.
323    ///
324    /// Bounds violations are mapped into `TrapCode::MemoryOutOfBounds`.
325    pub fn memory_read(&mut self, offset: usize, buffer: &mut [u8]) -> Result<(), TrapCode> {
326        let mut compiled_runtime = self.compiled_runtime.borrow_mut();
327        compiled_runtime.memory_read(offset, buffer)
328    }
329
330    /// Returns remaining fuel if fuel metering is enabled.
331    ///
332    /// For engine-metered precompiles (`consume_fuel=true`), returns the actual remaining fuel
333    /// from the Wasmtime store.
334    ///
335    /// For self-metering runtimes (`consume_fuel=false`), returns `None` because fuel is
336    /// tracked in `RuntimeContext` via `_charge_fuel` syscall, not in the Wasmtime store.
337    pub fn remaining_fuel(&self) -> Option<u64> {
338        let compiled_runtime = self.compiled_runtime.borrow();
339        if self.consume_fuel {
340            compiled_runtime.remaining_fuel()
341        } else {
342            None
343        }
344    }
345
346    /// Provides mutable access to the per-call runtime context.
347    pub fn context_mut(&mut self) -> &mut RuntimeContext {
348        &mut self.ctx
349    }
350
351    /// Provides immutable access to the per-call runtime context.
352    pub fn context(&self) -> &RuntimeContext {
353        &self.ctx
354    }
355}