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
- Run the verified-finalized-header pipeline up to the cursor's block number before recording the replacement scan.
- Use the newest cursor already present in `execution_verified_finalized_header` for this wallet instead of a newer one.
- 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
- Clamp scan cursors to the highest durably verified header instead of the raw RPC head.
- Ensure the finalized-header writer runs before the replacement scanner consumes new heights.
- Use a single wallet_address consistently between header verification and scan submission.
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
- Verified finalized transaction count advanced without an act
- Verified finalized transaction count is outside the owned re
- Verified finalized header extension does not start at the du
- Finalized header ledger conflicts at height {}
- Verified finality headers must form a continuous chain throu
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/87f65eec508d3389.
Report an issue: GitHub.