nautechsystems/nautilus_trader · error

Replacement scan manifest identity changed

Error message

Replacement scan manifest identity changed

What it means

load_execution_replacement_cursor joins the stored replacement scan row with its verified finalized header and compares the stored manifest_digest against the caller-supplied manifest_digest. If they differ, the persisted scan cursor belongs to a different verification manifest than the current one, so the code refuses to return it.

Source

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

             AND h.wallet_address = s.wallet_address
             AND h.number = s.finalized_cursor_number
             AND h.hash = s.finalized_cursor_hash
            WHERE s.intent_id = $1
              AND s.chain_id = $2
              AND s.wallet_address = $3
              AND s.nonce = $4
            ",
        )
        .bind(intent_id)
        .bind(chain_id)
        .bind(wallet_address)
        .bind(nonce)
        .fetch_optional(&self.pool)
        .await
        .context("failed to load replacement scan cursor")?;
        row.map(
            |(number, hash, parent_hash, timestamp, base_fee, stored_digest)| {
                anyhow::ensure!(
                    stored_digest == manifest_digest,
                    "Replacement scan manifest identity changed"
                );
                Ok(ExecutionVerifiedHeader {
                    number: u64::try_from(number)
                        .context("Replacement scan cursor number is negative")?,
                    hash,
                    parent_hash,
                    timestamp: u64::try_from(timestamp)
                        .context("Replacement scan cursor timestamp is negative")?,
                    base_fee_per_gas: base_fee
                        .map(|value| {
                            value.parse::<u128>().map_err(|_| {
                                anyhow::anyhow!("Replacement scan cursor base fee is invalid")
                            })
                        })
                        .transpose()?,
                })

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Regenerate the replacement scan under the current manifest so the stored digest matches
  2. Pass the manifest_digest that was used when the scan was recorded (check execution_replacement_scan.manifest_digest)
  3. Confirm the (intent_id, chain_id, wallet_address, nonce) tuple identifies the intended scan row
  4. Clear stale scan rows after a manifest change and rescan

Example fix

// before
let cursor = db.load_execution_replacement_cursor(intent_id, chain, wallet, nonce, &new_manifest_digest).await?;
// after
let stored: Option<String> = sqlx::query_scalar(
    "SELECT manifest_digest FROM execution_replacement_scan WHERE intent_id=$1 AND nonce=$2",
).bind(intent_id).bind(nonce as i64).fetch_optional(&db.pool).await?;
let cursor = db.load_execution_replacement_cursor(intent_id, chain, wallet, nonce,
    stored.as_deref().expect("scan must exist before loading cursor")).await?;
Defensive patterns

Strategy: validation

Validate before calling

let stored: Option<String> = sqlx::query_scalar(
    "SELECT manifest_digest FROM execution_replacement_scan WHERE intent_id=$1 AND chain_id=$2 AND wallet_address=$3 AND nonce=$4",
).bind(intent_id).bind(chain_id).bind(wallet).bind(nonce as i64)
.fetch_optional(&pool).await?;
if stored.as_deref() != Some(current_manifest_digest) {
    // rescan under the current manifest instead of loading the stale cursor
}

Type guard

fn digest_matches(stored: &Option<String>, expected: &str) -> bool {
    stored.as_deref() == Some(expected)
}

Try / catch

match db.load_execution_replacement_cursor(intent_id, chain, wallet, nonce, digest).await {
    Err(e) if e.to_string().contains("manifest identity changed") => {
        // invalidate stored scan and re-run record_execution_replacement_scan
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling load_execution_replacement_cursor with a manifest_digest that differs from the digest stored on the execution_replacement_scan row for that (intent_id, chain_id, wallet_address, nonce).

Common situations: The verification manifest was regenerated (code/schema change) after the scan was recorded; pointing at the wrong wallet/nonce so the digest lookup matches a different scan; upgrading the binary between recording and loading the cursor.

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/49717ac11437c216. Report an issue: GitHub.