{"record":{"id":"013543e803b20842","repo":"FuelLabs/fuel-core","slug":"unable-to-produce-blocks-without-a-consensus-key","errorCode":null,"errorMessage":"unable to produce blocks without a consensus key","messagePattern":"unable to produce blocks without a consensus key","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"critical","filePath":"crates/services/consensus_module/poa/src/service.rs","lineNumber":383,"sourceCode":"                    Instant::now(),\n                )\n                .await?;\n            }\n        }\n        Ok(())\n    }\n\n    async fn produce_block(\n        &mut self,\n        height: BlockHeight,\n        block_time: Tai64,\n        source: TransactionsSource,\n        deadline: Instant,\n    ) -> anyhow::Result<()> {\n        let last_block_created = Instant::now();\n        // verify signing key is set\n        if !self.signer.is_available() {\n            return Err(anyhow!(\"unable to produce blocks without a consensus key\"))\n        }\n\n        if self.last_timestamp > block_time {\n            return Err(anyhow!(\"The block timestamp should monotonically increase\"))\n        }\n        // Ask the block producer to create the block\n        let (\n            ExecutionResult {\n                block,\n                skipped_transactions,\n                tx_status,\n                events,\n            },\n            changes,\n        ) = self\n            .signal_produce_block(height, block_time, source, deadline)\n            .await?\n            .into();","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/FuelLabs/fuel-core/blob/b9d4d170da3a31c9ace5f963d633b326348e0d42/crates/services/consensus_module/poa/src/service.rs#L365-L401","documentation":"produce_block verifies self.signer.is_available() before doing anything: PoA blocks must be signed, and the signer (fuel_core_types::signer::SignMode) only reports available when it actually holds a secret key. If the node reaches a production path (trigger firing or manual production) with SignMode::Unavailable, block production fails immediately and the chain stalls at the current height.","triggerScenarios":"Any call to produce_block: default Trigger::Instant firing on new transactions, Trigger::Interval/Open ticks, or manually_produce_block — while Config.signer / node Config.consensus_signer is SignMode::Unavailable (no key loaded). Note Trigger defaults to Instant, so merely starting a node without a key is enough to hit this on the first tx.","commonSituations":"Custom node configs that drop the local/dev default consensus key (crates/fuel-core/src/service/config.rs:289 defaults to default_consensus_dev_key); operators forgetting to provision the consensus key secret for a producing validator; test setups constructing PoA Config with SignMode::Unavailable but leaving the trigger active.","solutions":["Configure the signing key: set consensus_signer to SignMode::Key(secret) in the node config (or run with the local/dev defaults which embed default_consensus_dev_key).","If this node is a passive follower/validator-only observer, set trigger: Trigger::Never so production paths are never entered.","Fail fast at startup: log or assert signer.is_available() before starting the service when the trigger is not Never."],"exampleFix":"// before\nlet config = Config {\n    signer: SignMode::Unavailable,\n    trigger: Trigger::default(), // Instant\n    ..Default::default()\n};\n\n// after (producing node)\nlet config = Config {\n    signer: SignMode::Key(Secret::new(secret_key_bytes.into())),\n    trigger: Trigger::default(),\n    ..Default::default()\n};\n\n// or (passive node)\nlet config = Config {\n    signer: SignMode::Unavailable,\n    trigger: Trigger::Never,\n    ..Default::default()\n};","handlingStrategy":"validation","validationCode":"// Fail fast at startup when the node is expected to produce\nuse fuel_core_types::signer::SignMode;\n\nfn assert_signer_for_production(signer: &SignMode, trigger: &fuel_core_poa::Trigger) -> anyhow::Result<()> {\n    if !matches!(trigger, fuel_core_poa::Trigger::Never) && !matches!(signer, SignMode::Key(_)) {\n        anyhow::bail!(\"trigger {trigger:?} requires a consensus key, but the signer is unavailable\");\n    }\n    Ok(())\n}","typeGuard":"fn can_sign(signer: &SignMode) -> bool {\n    matches!(signer, SignMode::Key(_))\n}","tryCatchPattern":"match node.manually_produce_block(None, mode).await {\n    Err(err) if err.to_string().contains(\"without a consensus key\") => {\n        // provisioning error: load the key or set Trigger::Never; do not retry\n        std::process::exit(1);\n    }\n    other => other,\n}","preventionTips":["Run producing nodes with an explicit consensus key in config; never rely on defaults carrying over across config refactors.","Add a startup check: signer.is_available() must be true unless trigger is Never.","For observer nodes, always set Trigger::Never so production paths are never entered."],"tags":["consensus","poa","signer","keys","configuration","rust"],"backgroundTag":null,"analyzedSha":"b9d4d170da3a31c9ace5f963d633b326348e0d42","analyzedAt":"2026-08-16T08:56:42.692Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}