{"record":{"id":"0429995cf7b2c215","repo":"FuelLabs/fuel-core","slug":"the-block-timestamp-should-monotonically-increase","errorCode":null,"errorMessage":"The block timestamp should monotonically increase","messagePattern":"The block timestamp should monotonically increase","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/services/consensus_module/poa/src/service.rs","lineNumber":387,"sourceCode":"        }\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();\n\n        if !skipped_transactions.is_empty() {\n            for (tx_id, err) in skipped_transactions {\n                tracing::error!(","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/FuelLabs/fuel-core/blob/b9d4d170da3a31c9ace5f963d633b326348e0d42/crates/services/consensus_module/poa/src/service.rs#L369-L405","documentation":"produce_block enforces self.last_timestamp <= block_time: each new block's Tai64 timestamp must be at least the previous block's. last_timestamp is bootstrapped from the last block header at startup (extract_block_info) and updated after every produced or imported block. Requesting an earlier timestamp is rejected before execution starts.","triggerScenarios":"manually_produce_block(Some(t), ..) with t earlier than the last block's timestamp; or automatic production where clock.now() (used by next_time(RequestType::Trigger)) is behind the chain's last timestamp — i.e. system clock skew, or a chain whose last block was produced with a future timestamp (e.g. manual blocks with a large start_time).","commonSituations":"VMs/containers with drifted clocks and no NTP; test chains that jumped timestamps forward via manual start_time then switched back to automatic production; restarting a producer against a chain with future-dated blocks; tests passing non-monotonic start_time values across calls.","solutions":["Omit start_time in manually_produce_block so next_time(RequestType::Manual) derives a valid timestamp from last_timestamp plus the elapsed/interval duration.","Sync the node's system clock (NTP/chrony) so clock.now() >= chain head timestamp.","If the head timestamp is in the future, manually produce with start_time above the head timestamp until wall clock catches up."],"exampleFix":"// before: passing a timestamp that can go backwards\nlet last = last_block_header.time();\nnode.produce_blocks_with_time(Tai64::now(), 3).await?; // Tai64::now() may be < last after clock drift\n\n// after: let the service derive the next time (or clamp above the head)\nnode.manually_produce_block(None, Mode::Blocks { number_of_blocks: 3 }).await?;\n// or explicitly:\nlet start = std::cmp::max(Tai64::now(), last);\nnode.manually_produce_block(Some(start), Mode::Blocks { number_of_blocks: 3 }).await?;","handlingStrategy":"validation","validationCode":"// Validate the requested timestamp against the chain head before producing\nlet last_timestamp = db.latest_header()?.time();\nlet requested = start_time.unwrap_or_else(|| max Tai64::now(), last_timestamp+1s);\nif requested < last_timestamp {\n    anyhow::bail!(\"requested time {requested:?} precedes head timestamp {last_timestamp:?}\");\n}\nnode.manually_produce_block(Some(requested), mode).await?;","typeGuard":"fn timestamp_is_monotonic(last: Tai64, next: Tai64) -> bool {\n    next >= last\n}","tryCatchPattern":"match node.manually_produce_block(Some(t), mode).await {\n    Err(err) if err.to_string().contains(\"monotonically increase\") => {\n        // re-issue with start_time = None (let the service derive it),\n        // or with a timestamp strictly above the chain head\n    }\n    other => other,\n}","preventionTips":["Run NTP on all block-producing nodes so wall clock never trails the chain head.","Prefer manually_produce_block(None, ..) and let next_time derive a valid timestamp.","After manually jumping chain time forward, keep manual start_times monotonic until real time catches up."],"tags":["consensus","poa","timestamps","clock-skew","manual-block-production","rust"],"backgroundTag":null,"analyzedSha":"b9d4d170da3a31c9ace5f963d633b326348e0d42","analyzedAt":"2026-08-16T08:56:42.692Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}