nautechsystems/nautilus_trader · error · anyhow::Error

Verified finality headers must form a continuous chain throu

Error message

Verified finality headers must form a continuous chain through the inclusion height

What it means

This error fires when the `finalized_headers` slice attached to a verified finality transition is empty, is not strictly sequential (each header's number must be the previous plus one and each must be the previous header's parent-hash child), or its last header does not reach the transaction's inclusion block height. The database layer requires an unbroken, hash-linked header chain proving canonicality up to the inclusion block before persisting finality. It is a fail-fast precondition checked before any SQL executes.

Source

Thrown at crates/adapters/blockchain/src/cache/database.rs:6906

    ///
    /// Returns an error if the receipt transition, nonce ledger, manifest identity, or evidence
    /// is inconsistent, or if persistence fails.
    pub(crate) async fn record_execution_finality_verified(
        &self,
        finality: &ExecutionFinalityTransition<'_>,
    ) -> anyhow::Result<()> {
        anyhow::ensure!(
            matches!(
                finality.status,
                TransactionStatus::Finalized | TransactionStatus::Reverted
            ),
            "Verified finality requires a finalized or reverted status"
        );
        anyhow::ensure!(
            !finality.decisions.is_empty(),
            "Verified finality requires decision evidence"
        );
        anyhow::ensure!(
            !finality.finalized_headers.is_empty()
                && finality.finalized_headers.windows(2).all(|headers| {
                    headers[1].number == headers[0].number.saturating_add(1)
                        && headers[1].parent_hash == headers[0].hash
                })
                && finality
                    .finalized_headers
                    .last()
                    .is_some_and(|header| header.number >= finality.block_number),
            "Verified finality headers must form a continuous chain through the inclusion height"
        );
        let chain_id = i32::try_from(finality.chain_id)
            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
        let nonce =
            i64::try_from(finality.nonce).context("Execution nonce exceeds PostgreSQL BIGINT")?;
        let next_nonce = nonce
            .checked_add(1)
            .ok_or_else(|| anyhow::anyhow!("Canonical nonce overflow"))?;

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Re-fetch the finalized header range from the node and verify each header's number is contiguous and each parent_hash links to the previous header's hash before calling the API
  2. Ensure the header range extends at least to finality.block_number (the inclusion height)
  3. Re-order headers oldest-to-newest if they were collected newest-first
  4. If a reorg invalidated the chain, rebuild the finality transition from the current canonical chain instead of reusing stale headers

Example fix

// before
let headers = client.finalized_headers(from..to).await?; // may contain gaps after reorg
db.record_execution_finality_verified(&finality_with(headers)).await?;
// after
let headers = client.finalized_headers(from..=finality.block_number).await?;
assert!(windows(2).all(|h| h[1].number == h[0].number + 1 && h[1].parent_hash == h[0].hash));
db.record_execution_finality_verified(&finality_with(headers)).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Rust: validate the header chain before persisting finality
fn headers_form_chain(headers: &[ExecutionVerifiedHeader], inclusion: u64) -> bool {
    !headers.is_empty()
        && headers.windows(2).all(|h| {
            h[1].number == h[0].number.saturating_add(1) && h[1].parent_hash == h[0].hash
        })
        && headers.last().is_some_and(|h| h.number >= inclusion)
}
ensure!(headers_form_chain(finality.finalized_headers, finality.block_number));

Type guard

fn is_contiguous_header_chain(headers: &[ExecutionVerifiedHeader]) -> bool {
    headers.windows(2).all(|h| h[1].parent_hash == h[0].hash && h[1].number == h[0].number + 1)
}

Try / catch

match db.record_execution_finality_verified(&finality).await {
    Err(e) if e.to_string().contains("continuous chain") => {
        // re-fetch headers from the node and rebuild the transition before retrying
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling record_execution_finality_verified with an empty finalized_headers slice; headers fetched with a gap (e.g. reorg removed a block); headers in the wrong order; header.number < finality.block_number for the last header; parent_hash not matching the previous header's hash.

Common situations: RPC provider returned a partial or reorged header range; caching layer deduplicated a header that was actually required for continuity; a reorg happened between finality decision and header fetch; test fixture headers hand-built without linking parent hashes; off-by-one so the last header stops one block short of the inclusion height.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/fe28b07ee29d5d32. Report an issue: GitHub.