# How the Transaction Executor in Fuel Core Works: Native and WASM Execution Modes Explained

> Understand the Fuel Core transaction executor an upgradable dual-mode engine executing blocks via native Rust or sandboxed WASM unlocking seamless protocol upgrades. Learn how it works.

- Repository: [Fuel Labs/fuel-core](https://github.com/FuelLabs/fuel-core)
- Tags: deep-dive
- Published: 2026-03-06

---

**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`](https://github.com/FuelLabs/fuel-core/blob/main/crates/services/upgradable-executor/src/executor.rs) around line 41. This generic implementation accepts two view providers and runtime configuration:

```rust
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`](https://github.com/FuelLabs/fuel-core/blob/main/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:

```rust
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

```rust
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`](https://github.com/FuelLabs/fuel-core/blob/main/crates/services/upgradable-executor/src/executor.rs)

### Producing a Block Synchronously

```rust
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

```rust
let validation = executor.validate(&block)?;
if validation.result.is_valid() {
    println!("Block is valid!");
}

```

*Source:* `validate` – lines 845-869

### Dry-Running Transactions

```rust
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

```rust
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.