nautechsystems/nautilus_trader · error · anyhow::Error

Verified finalized header tip does not match the ledger

Error message

Verified finalized header tip does not match the ledger

What it means

Thrown by `record_execution_finality_verified` when the first header of finality.finalized_headers does not exactly equal the durable tip stored in execution_verified_finalized_header (number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest are all compared as a tuple). This guard ensures each new finality extension continues exactly from the previously persisted chain tip — no forks, reorgs, or gaps are accepted. The adjacent message about the ledger tip ("Verified finality extension does not start at the durable tip") is the paired ensure at line 6985.

Source

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

            "Finalized nonce {} does not match canonical nonce {stored_nonce}",
            finality.nonce
        );
        let stored_tip = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
            "
            SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
            FROM execution_verified_finalized_header
            WHERE chain_id = $1 AND wallet_address = $2
            ORDER BY number DESC
            LIMIT 1
            ",
        )
        .bind(chain_id)
        .bind(finality.wallet_address)
        .fetch_one(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
        let first_header = &finality.finalized_headers[0];
        anyhow::ensure!(
            stored_tip
                == (
                    i64::try_from(first_header.number)
                        .context("Verified finalized height exceeds PostgreSQL BIGINT")?,
                    first_header.hash.clone(),
                    first_header.parent_hash.clone(),
                    i64::try_from(first_header.timestamp)
                        .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?,
                    first_header.base_fee_per_gas.map(|value| value.to_string()),
                    finality.manifest_digest.to_string(),
                ),
            "Verified finality extension does not start at the durable tip"
        );

        for header in finality.finalized_headers.iter().skip(1) {
            let number = i64::try_from(header.number)
                .context("Verified finalized height exceeds PostgreSQL BIGINT")?;
            let timestamp = i64::try_from(header.timestamp)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Align the finalized_headers batch to begin exactly at the stored tip: headers[0] must be the persisted tip header itself, with subsequent headers extending it contiguously
  2. Check for a reorg; if the stored tip is stale or invalid, rebuild the verified finalized header chain from the canonical chain before recording
  3. Ensure the manifest digest is unchanged (a digest mismatch also fails this tuple comparison); re-verify under the stored manifest
  4. Verify field encodings match (base_fee_per_gas string formatting, timestamp units) so the tuple equality holds for identical headers

Example fix

// before: batch skips past the durable tip
let headers = fetch_finalized_headers(start = tip.number + 2);
cache.record_execution_finality_verified(&finality_with(headers)).await?;

// after: include the tip header as the first element of the extension
let headers = fetch_finalized_headers(start = tip.number);
assert_eq!(headers[0].hash, tip.hash);
cache.record_execution_finality_verified(&finality_with(headers)).await?;
Defensive patterns

Strategy: validation

Validate before calling

let (tip_number, tip_hash): (i64, String) = sqlx::query_as(
    "SELECT number, hash FROM execution_verified_finalized_header WHERE chain_id = $1 AND wallet_address = $2 ORDER BY number DESC LIMIT 1"
)
.bind(chain_id).bind(wallet_address).fetch_one(&pool).await?;
let first = &finality.finalized_headers[0];
anyhow::ensure!(
    first.hash == tip_hash && first.number == tip_number as u64,
    "Finality batch must extend the durable tip exactly"
);

Try / catch

match cache.record_execution_finality_verified(&finality).await {
    Err(e) if e.to_string().contains("does not start at the durable tip") => {
        // detect reorg: compare parent hashes and rebuild the header chain if needed
    }
    result => result?,
}

Prevention

When it happens

Trigger: Calling record_execution_finality_verified when finality.finalized_headers[0] differs from the stored tip in any field: the batch starts at a later/earlier height than the tip, continues from a different parent (fork/reorg), or any header field (hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest) diverges from what was persisted.

Common situations: A chain reorg replaced the header at the tip height so hashes no longer match; recording finality batches with overlaps or gaps (tip is height N but the new batch starts at N+2 or N); an incompatible manifest digest after a manifest change; a clock/base-fee normalization difference making stored and incoming field encodings differ.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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