FuelLabs/fuel-core · error · anyhow::Error

Manual block production is not allowed with trigger `Open`

Error message

Manual block production is not allowed with trigger `Open`

What it means

produce_manual_blocks rejects every manual request while the node runs Trigger::Open. With Open, block production stays open for `period` seconds and the block is only finalized when the period elapses, so the manual request cannot be resolved immediately as SharedState::manually_produce_block's oneshot caller expects. The guard is an explicit config-combination check, not a runtime failure.

Solutions

  1. Switch the node's trigger to Trigger::Interval { block_time } or Trigger::Instant in the PoA config when tests/tooling need manual production.
  2. Do not call manually_produce_block against Open-configured nodes; wait for the open period to close the block instead.
  3. Gate the call up front: check the configured Trigger before sending a Request::ManualBlocks.

Example fix

// before
let config = Config {
    trigger: Trigger::Open { period: Duration::from_secs(5) },
    ..Default::default()
};
// ...
shared_state.manually_produce_block(None, Mode::Blocks { number_of_blocks: 1 }).await?; // Err

// after
let config = Config {
    trigger: Trigger::Interval { block_time: Duration::from_secs(5) },
    ..Default::default()
};
// ...
shared_state.manually_produce_block(None, Mode::Blocks { number_of_blocks: 1 }).await?; // Ok
Defensive patterns

Strategy: validation

Validate before calling

// Check before sending the manual request
fn manual_production_allowed(trigger: &fuel_core_poa::Trigger) -> bool {
    !matches!(trigger, fuel_core_poa::Trigger::Open { .. })
}

if !manual_production_allowed(&config.trigger) {
    anyhow::bail!("node uses Trigger::Open; manual block production is rejected");
}
shared_state.manually_produce_block(start_time, mode).await?;

Try / catch

match shared_state.manually_produce_block(start_time, mode).await {
    Err(err) if err.to_string().contains("Manual block production is not allowed") => {
        // config bug: switch trigger to Interval/Instant or wait for the open period
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling SharedState::manually_produce_block(start_time, Mode::Blocks{..} | Mode::BlockWithTransactions(..)) on a node whose Config.trigger is Trigger::Open { period } (crates/services/consensus_module/poa/src/service.rs:336). Typical in integration tests that reuse an Open-configured Service and then force blocks.

Common situations: Test harnesses configured for Trigger::Open (e.g. testing pre-confirmation flows) that also call manual production helpers; config migrated from Interval to Open while old test code still forces blocks; newer fuel-core versions where Trigger::Open was introduced and tests were not updated.

Related errors


AI-assisted analysis of FuelLabs/fuel-core@add100d30d (2026-09-05). Data as JSON: /api/errors/4d4b48696b81403b. Report an issue: GitHub.

Appendix: source

Thrown at crates/services/consensus_module/poa/src/service.rs:356

        deadline: Instant,
    ) -> anyhow::Result<()> {
        self.produce_block(
            self.next_height(),
            self.next_time(RequestType::Trigger)?,
            TransactionsSource::TxPool,
            deadline,
        )
        .await
    }

    async fn produce_manual_blocks(
        &mut self,
        block_production: ManualProduction,
    ) -> anyhow::Result<()> {
        // The caller of manual block production expects that it is resolved immediately
        // while in the case of `Trigger::Open` we need to wait for the period to pass.
        if matches!(self.trigger, Trigger::Open { .. }) {
            return Err(anyhow!(
                "Manual block production is not allowed with trigger `Open`"
            ));
        }

        // P2P imports can advance the tip independently of this task.
        // Manual production must re-read the DB or it produces at a stale
        // next_height (IncorrectBlockHeight).
        self.sync_last_height_from_db();

        let mut block_time = if let Some(time) = block_production.start_time {
            time
        } else {
            self.next_time(RequestType::Manual)?
        };
        match block_production.mode {
            Mode::Blocks { number_of_blocks } => {
                for _ in 0..number_of_blocks {
                    self.produce_block(

View on GitHub (pinned to add100d30d)