How the Transaction Executor in Fuel Core Works: Native and WASM Execution Modes Explained
The transaction executor in Fuel Core is an upgradable, dual-mode engine that executes blocks using either native Rust code or sandboxed WebAssembly (WASM) based on the block's state_transition_bytecode_version, enabling seamless protocol upgrades without requiring node restarts.
The transaction executor in Fuel Core serves as the central component for processing state transitions in the FuelLabs/fuel-core repository. Located within the upgradable executor service, this generic system evaluates whether to run the pure Rust state-transition function (STF) or a compiled WASM module, ensuring consensus across heterogeneous client versions while supporting block production, validation, and dry-run queries.
Architecture of the Upgradable Executor
The Generic Executor Struct
At the heart of the system lies the Executor<S, R> struct defined in crates/services/upgradable-executor/src/executor.rs around line 41. This generic implementation accepts two view providers and runtime configuration:
pub struct Executor<S, R> {
pub storage_view_provider: S, // Historical view of the DB
pub relayer_view_provider: R, // Relayer (DA-layer) interface
pub config: Arc<Config>, // Runtime configuration
#[cfg(feature = "wasm-executor")]
engine: wasmtime::Engine,
#[cfg(feature = "wasm-executor")]
execution_strategy: ExecutionStrategy, // Native or Wasm
#[cfg(feature = "wasm-executor")]
cached_modules: Mutex<HashMap<StateTransitionBytecodeVersion, wasmtime::Module>>,
}
The executor maintains an in-memory cache of compiled WASM modules (cached_modules) to avoid recompilation overhead during version switches.
Execution Strategy Selection
During instantiation via Executor::new (lines 68-85), the executor determines its default execution strategy. If the environment variable FUEL_ALWAYS_USE_WASM is present, the system forces WASM execution regardless of version matching; otherwise, it defaults to Native mode.
Dual-Mode Execution Strategy
The transaction executor in Fuel Core supports two distinct STF implementations selected dynamically per block:
| Execution Mode | Implementation | Selection Criteria |
|---|---|---|
| Native | fuel_core_executor::executor::ExecutionInstance (pure Rust) |
Block's state_transition_bytecode_version matches Executor::VERSION |
| WASM | wasmtime::Module compiled from uploaded bytecode |
Block version differs from native version or FUEL_ALWAYS_USE_WASM is set |
Native Execution Path
When the block version aligns with the native executor version, the system invokes native_produce_inner (starting at line 1021). This method constructs an ExecutionInstance with the supplied storage view, relayer interface, and memory pools, then calls produce_without_commit to run the STF in-process. This path offers optimal performance by avoiding WASM sandboxing overhead.
WebAssembly Execution Path
For version mismatches or forced WASM mode, the executor calls wasm_produce_inner (line 1490). This builds a crate::instance::Instance—a thin wrapper around the WASM VM that injects storage, relayer, and block data into the sandbox. The executor executes module.run and converts the returned ReturnType into a standard ExecutionResult using the conversion utilities located in crates/services/upgradable-executor/wasm-executor/src/utils.rs (line 87).
Block Production and Validation Workflow
Producing Blocks Without Commitment
The primary entry point for block production is produce_without_commit (lines 382-397), which accepts a Components struct containing the header, transaction source, coinbase recipient, and gas price. The synchronous variant produce_inner_sync (lines 784-803) simply blocks on the asynchronous produce_inner implementation.
Version-Based Path Selection
Inside produce_inner (lines 816-838), the executor compares versions to determine the execution path:
let block_version = block.header_to_produce.state_transition_bytecode_version;
let native_executor_version = self.native_executor_version();
if block_version == native_executor_version {
// use strategy (Native or Wasm) stored in self.execution_strategy
} else {
// load the appropriate WASM module for that version
}
Block Validation
For verifying imported blocks, the validate method (lines 845-869) invokes validate_inner, which mirrors the production logic but operates in validation mode only. Both Native and WASM branches return a ValidationResult rather than committing state changes. The executor can also replay storage reads performed during validation for debugging purposes (lines 934-1002), creating either an ExecutionInstance (Native) or WASM instance on the previous block's state.
Dry-Run Execution
The dry_run method (lines 774-818) executes transactions without committing changes to the database. When ProduceBlockMode::DryRun is specified with record_storage_reads enabled, the method returns a DryRunResult containing transaction outcomes and the complete list of storage accesses—critical for RPC endpoints that simulate transactions against current state.
WASM Module Lifecycle and Caching
Bytecode Retrieval and Compilation
When WASM execution requires a specific version not present in memory, the get_module method (lines 1610-1647) handles compilation:
- Checks the
cached_modulesHashMap for an existingwasmtime::Module - If missing, queries the
StateTransitionBytecodeVersionstable for the bytecode root - Retrieves the actual bytecode from
UploadedBytecodesstorage - Validates and compiles the bytecode using the
wasmtime::Engine
This caching mechanism ensures that frequently used protocol versions remain hot in memory while still supporting historical block validation.
Practical Code Examples
Instantiating the Executor
use fuel_core::services::upgradable_executor::Executor;
use fuel_core::config::Config;
use fuel_core_storage::{HistoricalView, AtomicView};
use std::sync::Arc;
// Assume `storage` implements HistoricalView<BlockHeight> + Modifiable
// and `relayer` implements AtomicView.
let config = Config::default();
let executor = Executor::new(storage, relayer, config);
Source: Executor::new – lines 68-85 in crates/services/upgradable-executor/src/executor.rs
Producing a Block Synchronously
use fuel_core_types::services::executor::Components;
use fuel_core_types::fuel_tx::Transaction;
// Build the block header & list of transactions …
let components = Components {
header_to_produce: header,
transactions_source: vec![tx1, tx2],
coinbase_recipient: default_coinbase,
gas_price: 0,
};
let result = executor.produce_without_commit(components)?;
println!("Block executed, {} txs", result.result.transactions.len());
Source: produce_without_commit – lines 382-397
Validating an Imported Block
let validation = executor.validate(&block)?;
if validation.result.is_valid() {
println!("Block is valid!");
}
Source: validate – lines 845-869
Dry-Running Transactions
let dry = executor.dry_run(
components,
Some(false), // forbid_fake_coins
None, // at latest height
true, // record storage reads
)?;
println!("Dry-run storage reads: {}", dry.storage_reads.len());
Source: dry_run – lines 774-818
Forcing WASM Execution
std::env::set_var("FUEL_ALWAYS_USE_WASM", "1");
let wasm_executor = Executor::new(storage, relayer, config);
// The executor will now always use the WASM module, even if the versions match.
Source: Executor::new – lines 74-80
Summary
- The transaction executor in Fuel Core operates as a generic, upgradable system capable of switching between native Rust and WASM execution environments at runtime.
- Version matching determines the execution path: matching versions trigger native execution via
ExecutionInstance, while mismatches trigger sandboxed WASM execution viawasmtime::Module. - Block production occurs through
produce_without_commit, which assembles components and routes to eithernative_produce_innerorwasm_produce_innerbased on the block'sstate_transition_bytecode_version. - Validation and dry-runs reuse the same execution paths but return
ValidationResultorDryRunResultwithout committing state changes, enabling safe transaction simulation and block verification. - Module caching in
cached_modulesoptimizes WASM performance by storing compiledwasmtime::Moduleinstances indexed byStateTransitionBytecodeVersion.
Frequently Asked Questions
What determines whether the transaction executor uses Native or WASM mode?
The executor compares the block's state_transition_bytecode_version header field against the node's native version (Executor::VERSION). If they match, the system uses the strategy stored in execution_strategy (defaulting to Native). If they differ, or if the FUEL_ALWAYS_USE_WASM environment variable is set, the executor loads and executes the appropriate WASM module compiled from the uploaded bytecode stored in the StateTransitionBytecodeVersions table.
How does the transaction executor handle protocol upgrades without stopping the node?
Fuel Core supports stateful upgrades by storing new STF bytecode on-chain in the UploadedBytecodes table and registering version mappings in StateTransitionBytecodeVersions. When a block arrives with a newer bytecode version, the executor automatically fetches, validates, and compiles the WASM module via get_module (lines 1610-1647), caching it for subsequent executions. This allows the network to upgrade consensus logic without requiring operators to restart or update their binaries immediately.
What is the purpose of dry-run execution in the Fuel Core transaction executor?
Dry-run mode, invoked via dry_run (lines 774-818), simulates block execution against the current state without persisting changes. When configured to record storage reads, it returns a DryRunResult containing the exact storage keys accessed during execution. This functionality powers RPC endpoints that estimate gas costs, validate transaction sequences, or debug state access patterns before submitting transactions to the mempool.
Where does the transaction executor store WASM bytecode, and how is it compiled?
The executor retrieves WASM bytecode from the UploadedBytecodes storage table, indexed by the root hash stored in StateTransitionBytecodeVersions. The get_module method checks an in-memory HashMap<StateTransitionBytecodeVersion, wasmtime::Module> before compiling. If the module is absent, the executor validates the bytecode and compiles it using the wasmtime::Engine instance stored in the executor struct, then caches the result for future use.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →