nautechsystems/nautilus_trader · error

Replacement scan cursor {} is not durably finalized

Error message

Replacement scan cursor {} is not durably finalized

What it means

When a replacement scan carries a `finalized_cursor`, the function checks that a header with that (chain_id, wallet_address, number) exists in `execution_verified_finalized_header`. If the lookup returns no row, the cursor was never durably recorded as a verified finalized header, so the scan cannot anchor to it and the transaction aborts.

Source

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

        if let Some(cursor) = scan.finalized_cursor {
            let number = i64::try_from(cursor.number)
                .context("Replacement scan cursor exceeds PostgreSQL BIGINT")?;
            let durable_hash = 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(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)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Run the verified-finalized-header pipeline up to the cursor's block number before recording the replacement scan.
  2. Use the newest cursor already present in `execution_verified_finalized_header` for this wallet instead of a newer one.
  3. Confirm the scan's wallet_address matches the address used when the headers were persisted.

Example fix

// before: cursor taken straight from the latest RPC head
let cursor = ExecutionVerifiedHeader { number: latest_head_number, .. };
// after: clamp to the last durably verified header
let cursor = db.latest_verified_finalized_header(chain_id, wallet).await?;
Defensive patterns

Strategy: validation

Validate before calling

let durable = 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 durable.is_none() { return Err(anyhow!("cursor {} not durably finalized yet", cursor.number)); }

Type guard

async fn cursor_is_durable(pool: &PgPool, chain_id: i32, wallet: &str, number: u64) -> anyhow::Result<bool> {
    Ok(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(number as i64).fetch_optional(pool).await?.is_some())
}

Prevention

When it happens

Trigger: Passing `scan.finalized_cursor` pointing at a block number that has no row in `execution_verified_finalized_header` — e.g. the header verification pipeline has not yet persisted that height, or the cursor was fabricated from a raw RPC response without prior verification.

Common situations: The finalized-header writer lagged behind the scanner (crash or backlog); the cursor came from an unverified RPC source; the wallet_address differs from the one used when headers were recorded.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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