Understanding Fuel Core Block Producer Modes of Operation: Executor, Manual, and Automatic Triggers

Fuel Core supports three distinct block producer operation modes—ProduceBlockMode for executor-level state control, Mode for manual production requests, and Trigger for automatic scheduling—that together govern how blocks are constructed, validated, and committed in the Fuel blockchain.

The Fuel Core block producer operates through a layered architecture that decouples scheduling logic from execution strategy. According to the FuelLabs/fuel-core source code, these distinct modes allow the Proof-of-Authority (PoA) consensus module to handle everything from production testing to live network validation. Understanding these modes is essential for node operators developers building high-performance applications on the Fuel network.

Executor-Level Production: ProduceBlockMode

At the lowest level, the ProduceBlockMode enum in crates/services/upgradable-executor/src/executor.rs (lines 109–115) determines whether the executor commits state changes or merely simulates execution. This mode controls how the block is processed at the database level.

Normal Production vs. Dry-Run

The executor recognizes two primary variants:

  • Produce – The standard mode that executes transactions and commits the resulting state to the database permanently.
  • DryRun { height, record_storage_reads } – A simulation mode that executes the block without committing state changes. This variant accepts a target block height (allowing historical replays) and a boolean flag to optionally record storage reads for debugging purposes.

The DryRun variant is particularly valuable for estimating gas costs, testing transaction validity against historical states, and debugging contract interactions without consuming network resources. When invoked, the executor returns an ExecutionResult containing transaction receipts while leaving the underlying Merkle trees unchanged.

Manual Production Requests: Mode

The PoA consensus module exposes a Mode enum in crates/services/consensus_module/poa/src/service.rs (lines 96–101) that enables external callers to request specific block production behaviors. This layer controls what content is included in the block.

Bulk Block Production

The Blocks { number_of_blocks: u32 } variant instructs the producer to generate a fixed number of consecutive blocks using transactions from the mempool. This mode is primarily used for stress testing, network bootstrapping, or bulk-generating empty blocks during initialization. The manually_produce_block method accepts this variant to trigger immediate production outside normal scheduling.

Custom Transaction Sets

Alternatively, BlockWithTransactions(Vec<Transaction>) allows the caller to specify an exact list of transactions to include in a single block. This deterministic approach bypasses the transaction pool entirely, making it ideal for testing specific execution paths, replaying historical blocks, or implementing custom sequencing logic in private networks.

Automatic Production Triggers: Trigger

The Trigger enum defined in crates/services/consensus_module/poa/src/config.rs (lines 33–47) configures when the block producer activates. This automatic scheduling layer operates independently of manual requests.

Instant, Interval, and Open Modes

  • Instant – Produces a block immediately upon detecting any transaction in the mempool. This minimizes latency for high-throughput test environments.
  • Interval { block_time } – Produces blocks at fixed time intervals (e.g., every 3 seconds), simulating traditional blockchain cadences suitable for production networks.
  • Open { period } – Opens a temporary production window for a specified duration, allowing continuous block production during bootstrap phases or maintenance windows.

Never Mode for Observer Nodes

The Never variant disables automatic production entirely, configuring the node as a pure observer or API endpoint. In this mode, the node validates and forwards transactions but never proposes new blocks, which is useful for indexers, explorers, or sentry nodes in a validator cluster.

Architectural Flow: How Modes Interact

The three modes operate in a cascading hierarchy that separates concerns across the stack:

  1. Trigger → Scheduler – The PoA service reads the Trigger configuration from config.rs and initializes an asynchronous scheduler.
  2. Scheduler → Mode – When the trigger fires or an admin API call arrives, the service constructs either a Mode::Blocks request for bulk production or Mode::BlockWithTransactions for custom sets.
  3. Mode → Executor – The service forwards the request to the block producer (crates/services/producer/src/block_producer.rs), which invokes the upgradable executor with ProduceBlockMode::Produce for normal operations or ProduceBlockMode::DryRun for simulations.

This layered design allows operators to mix and match configurations: for example, running with Trigger::Never for manual control while using DryRun to validate transactions before submission.

Practical Implementation Examples

Simulating Blocks with Dry-Run

To execute a block without committing state changes, invoke the executor's dry-run method as implemented in crates/services/upgradable-executor/src/executor.rs:

use fuel_core_services::upgradable_executor::Executor;
use fuel_core_types::services::executor::Result as ExecutorResult;

// executor is an initialized Executor instance
let block = /* construct PartialFuelBlock */;

let dry_result: ExecutorResult<_> = executor.dry_run_without_commit(block);
match dry_result {
    Ok(uncommitted) => {
        println!("Executed {} transactions", uncommitted.result().transactions.len());
        // State remains uncommitted; inspect receipts without side effects
    }
    Err(e) => eprintln!("Dry-run failed: {e:?}"),
}

Manually Producing Block Batches

For testing or bootstrap scenarios, manually trigger batch production using the PoA service:

use fuel_core_consensus_module_poa::service::{Mode, Trigger};
use std::time::Duration;

let cfg = Config {
    trigger: Trigger::Never,  // Disable automatic triggers
    ..Default::default()
};

let ctx = TestContextBuilder::new()
    .with_config(cfg)
    .build()
    .await;

// Produce exactly 5 blocks from the mempool
ctx.service
    .shared
    .manually_produce_block(None, Mode::Blocks { number_of_blocks: 5 })
    .await
    .expect("Manual production failed");

Configuring Automatic Interval Production

Configure Trigger::Interval via the node's TOML configuration file to enable regular block production:

[consensus_module.poa]
trigger = { Interval = { block_time = "3s" } }
signer = { Key = "0xdeadbeef..." }

This configuration directs the scheduler in crates/services/consensus_module/poa/src/service.rs to call produce_and_execute every 3 seconds.

Summary

  • ProduceBlockMode controls executor behavior, offering Produce for permanent state commits and DryRun for simulation without side effects.
  • Mode handles manual production requests, supporting both bulk block generation (Blocks) and custom transaction injection (BlockWithTransactions).
  • Trigger manages automatic scheduling with variants for immediate (Instant), periodic (Interval), temporary (Open), or disabled (Never) production.
  • The architecture decouples scheduling (Trigger), content selection (Mode), and execution semantics (ProduceBlockMode) across crates/services/consensus_module/poa/ and crates/services/upgradable-executor/.

Frequently Asked Questions

What is the difference between DryRun and Produce modes in Fuel Core?

DryRun simulates block execution without writing to the database, returning transaction receipts and gas estimates while leaving state unchanged. Produce commits all state transitions permanently to the underlying storage. According to the implementation in crates/services/upgradable-executor/src/executor.rs, DryRun accepts optional parameters for historical height targeting and storage read recording, making it ideal for debugging and gas estimation.

How do I configure a Fuel Core node to produce blocks at regular intervals?

Set the Trigger variant to Interval in your configuration TOML: [consensus_module.poa] trigger = { Interval = { block_time = "3s" } }. This corresponds to Trigger::Interval { block_time: Duration } in crates/services/consensus_module/poa/src/config.rs. The PoA service scheduler will then invoke the block producer at the specified cadence.

Can Fuel Core produce blocks without a transaction pool?

Yes, by using Mode::BlockWithTransactions. This mode, defined in crates/services/consensus_module/poa/src/service.rs, allows the caller to pass a Vec<Transaction> directly to manually_produce_block, bypassing the mempool entirely. This is commonly used in deterministic testing environments where specific transaction sequences must be executed.

What is the purpose of the Open trigger mode?

Trigger::Open { period } creates a time-bounded window where the block producer remains active and can generate multiple blocks as transactions arrive. Unlike Instant (which produces one block per transaction batch) or Interval (which produces at fixed times), Open is useful for bootstrap phases or maintenance windows where continuous production is needed for a specific duration before returning to normal scheduling rules.

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 →