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:

  1. Checks the cached_modules HashMap for an existing wasmtime::Module
  2. If missing, queries the StateTransitionBytecodeVersions table for the bytecode root
  3. Retrieves the actual bytecode from UploadedBytecodes storage
  4. 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 via wasmtime::Module.
  • Block production occurs through produce_without_commit, which assembles components and routes to either native_produce_inner or wasm_produce_inner based on the block's state_transition_bytecode_version.
  • Validation and dry-runs reuse the same execution paths but return ValidationResult or DryRunResult without committing state changes, enabling safe transaction simulation and block verification.
  • Module caching in cached_modules optimizes WASM performance by storing compiled wasmtime::Module instances indexed by StateTransitionBytecodeVersion.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →