nautechsystems/nautilus_trader · error
Replacement scan cursor regressed or changed
Error message
Replacement scan cursor regressed or changed
What it means
When an existing `execution_replacement_scan` row exists for the intent, the new cursor must strictly advance (number > stored_number) or, at the same number, carry the identical hash. Otherwise the scan is regressing or flip-flopping between headers at the same height, which breaks the monotonic progress invariant, so the update is rejected.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:6333
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!(
number > stored_number
|| (number == stored_number && cursor.hash == stored_hash),
"Replacement scan cursor regressed or changed"
);
}
sqlx::query(
"
INSERT INTO execution_replacement_scan (
intent_id, chain_id, wallet_address, nonce,
finalized_cursor_number, finalized_cursor_hash, manifest_digest
) VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (intent_id) DO UPDATE SET
finalized_cursor_number = EXCLUDED.finalized_cursor_number,
finalized_cursor_hash = EXCLUDED.finalized_cursor_hash,
updated_at = NOW()
",
)
.bind(scan.intent_id)View on GitHub (pinned to 18893faf8b)
Solutions
- Only submit scans whose cursor is >= the last persisted cursor; drop or skip stale scan snapshots.
- Serialize scan recording per intent (single worker or advisory lock) so cursors advance monotonically.
- If a reorg genuinely moved the head backward, clear the intent's replacement scan progress through the proper reorg path rather than re-submitting a lower cursor.
Example fix
// before: blind resubmission of an old snapshot
for snapshot in pending_snapshots { db.record_execution_replacement_scan(&snapshot).await?; }
// after: only advance
if snapshot.finalized_cursor.as_ref().unwrap().number >= last_recorded_cursor_number {
db.record_execution_replacement_scan(&snapshot).await?;
} Defensive patterns
Strategy: validation
Validate before calling
if let Some((stored_number, stored_hash)) = sqlx::query_as::<_, (i64, String)>("SELECT finalized_cursor_number, finalized_cursor_hash FROM execution_replacement_scan WHERE intent_id=$1").bind(scan.intent_id).fetch_optional(&pool).await? {
let c = scan.finalized_cursor.as_ref().unwrap();
if !(c.number as i64 > stored_number || (c.number as i64 == stored_number && c.hash == stored_hash)) {
return Err(anyhow!("cursor would regress; skipping stale snapshot"));
}
} Type guard
fn cursor_advances(new: &ExecutionVerifiedHeader, stored_number: i64, stored_hash: &str) -> bool {
new.number as i64 > stored_number || (new.number as i64 == stored_number && new.hash == stored_hash)
} Prevention
- Queue scan snapshots per intent and drop any whose cursor is older than the last recorded one.
- Use one scanner worker per intent (or an advisory lock) to keep cursor progress monotonic.
- After a reorg, clear progress through the reorg path instead of re-submitting lower cursors.
When it happens
Trigger: Submitting a replacement scan whose `finalized_cursor.number` is lower than the stored `finalized_cursor_number`, or equal in number but with a different `hash` than the stored `finalized_cursor_hash`.
Common situations: Two scan workers processing headers out of order; a reorg caused the scanner to emit an older or alternative block; retrying an old scan snapshot after a newer one was already recorded.
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
- Replacement scan conflicts with the canonical nonce or manif
- Intent cannot attach a verified replacement from status {cur
- 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
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/6fc4385249573b4b.
Report an issue: GitHub.