nautechsystems/nautilus_trader · critical

Replacement scan cursor conflicts with the finalized header

Error message

Replacement scan cursor conflicts with the finalized header ledger

What it means

After finding the durable header for the cursor's block number, the code compares the stored hash with `cursor.hash` under `anyhow::ensure!`. A mismatch means the cursor claims a hash that contradicts the verified finalized header ledger — i.e. the local view of that block diverges from what was durably verified (or the cursor is corrupt/forged).

Source

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

                "
                SELECT hash
                FROM execution_verified_finalized_header
                WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
                ",
            )
            .bind(chain_id)
            .bind(scan.wallet_address)
            .bind(number)
            .fetch_optional(&mut *transaction)
            .await
            .context("failed to validate replacement cursor against finalized headers")?
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "Replacement scan cursor {} is not durably finalized",
                    cursor.number
                )
            })?;
            anyhow::ensure!(
                durable_hash == cursor.hash,
                "Replacement scan cursor conflicts with the finalized header ledger"
            );
            let existing = sqlx::query_as::<_, (i64, String)>(
                "
                SELECT finalized_cursor_number, finalized_cursor_hash
                FROM execution_replacement_scan
                WHERE intent_id = $1
                FOR UPDATE
                ",
            )
            .bind(scan.intent_id)
            .fetch_optional(&mut *transaction)
            .await
            .context("failed to lock replacement scan progress")?;

            if let Some((stored_number, stored_hash)) = existing {
                anyhow::ensure!(

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Re-verify the header at that height and rebuild the cursor from the hash stored in `execution_verified_finalized_header`.
  2. If a genuine reorg occurred, run the reorg-handling path to update the finalized header ledger before re-submitting the scan.
  3. Confirm chain_id is correct; a mismatched network produces entirely different hashes at the same height.

Example fix

// before: trusting a cached cursor hash
let cursor = cached_cursor.clone();
// after: adopt the durably verified hash
let verified = db.verified_finalized_header(chain_id, wallet, cursor.number).await?;
let cursor = ExecutionVerifiedHeader { hash: verified.hash, ..cursor };
Defensive patterns

Strategy: validation

Validate before calling

let stored = sqlx::query_scalar::<_, String>("SELECT hash FROM execution_verified_finalized_header WHERE chain_id=$1 AND wallet_address=$2 AND number=$3").bind(chain_id).bind(wallet).bind(cursor.number as i64).fetch_optional(&pool).await?;
if stored.as_deref() != Some(cursor.hash.as_str()) { return Err(anyhow!("cursor hash diverges from finalized header ledger")); }

Type guard

fn cursor_hash_matches(cursor: &ExecutionVerifiedHeader, durable_hash: &str) -> bool { cursor.hash == durable_hash }

Prevention

When it happens

Trigger: Calling `record_execution_replacement_scan` with a `finalized_cursor` whose `hash` differs from the hash stored in `execution_verified_finalized_header` for the same (chain_id, wallet_address, number).

Common situations: A chain reorg replaced the block at that height after it was durably verified; the cursor hash was computed from a different network (wrong chain_id); stale bytes were cached and hashed instead of the verified header.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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