nautechsystems/nautilus_trader · error · anyhow::Error

Execution nonce {} does not match canonical nonce {next_nonc

Error message

Execution nonce {} does not match canonical nonce {next_nonce}

What it means

The locked ledger row's next_canonical_nonce does not equal the nonce of the assignment being recorded. Canonical nonce assignment must be strictly sequential: each assignment must consume exactly the ledger's next nonce. This guard prevents gaps, duplicates, or out-of-order execution nonces from being committed to the canonical ledger.

Source

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

                "
                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
                FROM execution_verification_nonce
                WHERE chain_id = $1 AND wallet_address = $2
                FOR UPDATE
                ",
            )
            .bind(chain_id)
            .bind(assignment.wallet_address)
            .fetch_optional(&mut *transaction)
            .await
            .map_err(|e| anyhow::anyhow!("Failed to lock canonical nonce ledger: {e}"))?
            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
        anyhow::ensure!(
            manifest_version == assignment.manifest_version
                && manifest_digest == assignment.manifest_digest,
            "Verified nonce assignment manifest identity changed"
        );
        anyhow::ensure!(
            next_nonce == nonce,
            "Execution nonce {} does not match canonical nonce {next_nonce}",
            assignment.nonce
        );

        let (intent_chain_id, intent_wallet, intent_nonce, intent_status, intent_active) =
            sqlx::query_as::<_, (i32, String, Option<i64>, String, bool)>(
                "
            SELECT chain_id, wallet_address, nonce, status, active
            FROM execution_intent
            WHERE id = $1
            FOR UPDATE
            ",
            )
            .bind(assignment.intent_id)
            .fetch_optional(&mut *transaction)
            .await
            .map_err(|e| {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Refresh the assignment's nonce from the canonical ledger: read next_canonical_nonce and rebuild the assignment.
  2. Serialize assignments per (chain_id, wallet) through a single writer or the ledger's locking path so no races occur.
  3. Drop/reject stale assignments from before the ledger advanced; do not retry them verbatim.
  4. Inspect the ledger row (`SELECT next_canonical_nonce ...`) to see how far the incoming nonce has drifted.
  5. If nonces legitimately diverged, reconcile via the project's recovery/resync procedure rather than forcing an update.

Example fix

// before
 let assignment = Assignment { nonce: cached_nonce, .. };
 db.assign_verified_nonce(assignment).await?;
// after
 let next = db.current_canonical_nonce(chain_id, wallet).await?;
 let assignment = Assignment { nonce: next, .. };
 db.assign_verified_nonce(assignment).await?;
Defensive patterns

Strategy: validation

Validate before calling

let next: i64 = sqlx::query_scalar("SELECT 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 assignment.nonce != next as u64 {
    return Err(anyhow!("stale nonce {}: canonical is {next}; refetch and rebuild", assignment.nonce));
}

Type guard

fn nonce_is_current(assignment: &Assignment, next_canonical: i64) -> bool {
    assignment.nonce == next_canonical as u64
}

Try / catch

match result {
    Err(e) if e.to_string().contains("does not match canonical nonce") => {
        let next = db.current_canonical_nonce(chain_id, wallet).await?;
        let fresh = assignment.with_nonce(next);
        db.assign_verified_nonce(&fresh).await
    }
    other => other,
}

Prevention

When it happens

Trigger: Submitting an assignment whose `assignment.nonce` is not equal to the row's `next_canonical_nonce`: replaying an old assignment, two writers racing where one lost (ledger advanced), a skipped nonce due to a partially failed earlier batch, or client-side nonce computation diverging from the canonical ledger.

Common situations: Retrying a batch of assignments after a partial failure without refreshing next_nonce from the DB; running two processes against the same wallet concurrently; manual nonce management drifting from the ledger; restoring an old DB snapshot while in-flight assignments reference newer nonces.

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/68de2898c6833c65. Report an issue: GitHub.