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
- Regenerate the scan from the current manifest so `manifest_version` and `manifest_digest` match the row in `execution_verification_nonce`.
- Re-read `next_canonical_nonce` for the (chain_id, wallet_address) pair and rebuild the scan with that exact nonce.
- Ensure only one scan pipeline advances the canonical nonce at a time; serialize scans per wallet to avoid nonce races.
- 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
- Always derive the scan from a fresh read of the nonce ledger, never from cached manifest metadata.
- Single-writer serialization of replacement scans per wallet prevents nonce races.
- Bump and propagate manifest version/digest together whenever the verification manifest changes.
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
- Replacement scan cursor regressed or changed
- 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/c9c42bfef8dab01b.
Report an issue: GitHub.