nautechsystems/nautilus_trader · error

Replacement scan conflicts with the canonical nonce or manif

Error message

Replacement scan conflicts with the canonical nonce or manifest ledger

What it means

Raised in `record_execution_replacement_scan` when the caller-supplied replacement scan does not match the canonical verification nonce ledger row (`execution_verification_nonce`). The stored `manifest_version`, `manifest_digest`, and `next_canonical_nonce` are compared against the scan under a `FOR SHARE` lock; any mismatch aborts the transaction. This guards against submitting scan evidence from a stale or different manifest or an out-of-order nonce.

Source

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

            .begin()
            .await
            .context("failed to start verified replacement scan transition")?;
        let (stored_version, stored_digest, stored_nonce, revision) =
            sqlx::query_as::<_, (String, String, i64, i64)>(
                "
                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
                FROM execution_verification_nonce
                WHERE chain_id = $1 AND wallet_address = $2
                FOR SHARE
                ",
            )
            .bind(chain_id)
            .bind(scan.wallet_address)
            .fetch_optional(&mut *transaction)
            .await
            .context("failed to read replacement scan nonce ledger")?
            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
        anyhow::ensure!(
            stored_version == scan.manifest_version
                && stored_digest == scan.manifest_digest
                && stored_nonce == nonce,
            "Replacement scan conflicts with the canonical nonce or manifest ledger"
        );
        let current_status = sqlx::query_scalar::<_, String>(
            "
            SELECT status
            FROM execution_intent
            WHERE id = $1 AND chain_id = $2 AND wallet_address = $3
              AND nonce = $4 AND active
            FOR UPDATE
            ",
        )
        .bind(scan.intent_id)
        .bind(chain_id)
        .bind(scan.wallet_address)
        .bind(nonce)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Regenerate the scan from the current manifest so `manifest_version` and `manifest_digest` match the row in `execution_verification_nonce`.
  2. Re-read `next_canonical_nonce` for the (chain_id, wallet_address) pair and rebuild the scan with that exact nonce.
  3. Ensure only one scan pipeline advances the canonical nonce at a time; serialize scans per wallet to avoid nonce races.
  4. Verify the chain_id and wallet_address passed to the API are the ones the ledger was initialized with.

Example fix

// before: scan built from stale manifest
let scan = ExecutionReplacementScan { manifest_version: "v1", manifest_digest: old_digest, nonce: guessed_nonce, .. };
db.record_execution_replacement_scan(&scan).await?;
// after: read canonical ledger first and build from it
let ledger = db.load_verification_nonce_ledger(chain_id, wallet).await?;
let scan = ExecutionReplacementScan { manifest_version: ledger.version, manifest_digest: ledger.digest, nonce: ledger.next_canonical_nonce, .. };
db.record_execution_replacement_scan(&scan).await?;
Defensive patterns

Strategy: validation

Validate before calling

let ledger = sqlx::query_as::<_, (String, String, i64)>("SELECT manifest_version, manifest_digest, next_canonical_nonce FROM execution_verification_nonce WHERE chain_id = $1 AND wallet_address = $2").bind(chain_id).bind(wallet).fetch_one(&pool).await?;
if ledger.0 != scan.manifest_version || ledger.1 != scan.manifest_digest || ledger.2 != scan.nonce as i64 {
    return Err(anyhow!("scan does not match canonical nonce/manifest ledger"));
}

Type guard

fn scan_matches_ledger(scan: &ExecutionReplacementScan, version: &str, digest: &str, next_nonce: i64) -> bool {
    scan.manifest_version == version && scan.manifest_digest == digest && scan.nonce as i64 == next_nonce
}

Prevention

When it happens

Trigger: Calling `record_execution_replacement_scan` with a scan whose `manifest_version` or `manifest_digest` differs from the row stored in `execution_verification_nonce`, or whose `nonce` is not exactly `next_canonical_nonce` for the given (chain_id, wallet_address).

Common situations: A deployer updated the verification manifest but the in-memory scan builder still carries the old version/digest; two replacement scans raced and one used a nonce already consumed; running against the wrong chain_id/wallet pair so the loaded ledger row belongs to another identity.

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


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