nautechsystems/nautilus_trader · error · anyhow::Error

Canonical nonce ledger changed during finality transition

Error message

Canonical nonce ledger changed during finality transition

What it means

This is an optimistic-concurrency guard: after the nonce UPDATE, the code asserts exactly one row was affected (database.rs:7192). Zero rows means the ledger row for this chain/wallet was changed concurrently — either the expected next_canonical_nonce or the revision no longer matches — so committing would fork the canonical nonce sequence, and the library deliberately fails instead.

Source

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

        .map_err(|e| anyhow::anyhow!("Failed to record verified finality transition: {e}"))?;

        let nonce_result = sqlx::query(
            "
            UPDATE execution_verification_nonce
            SET next_canonical_nonce = $3, revision = revision + 1, updated_at = NOW()
            WHERE chain_id = $1 AND wallet_address = $2
              AND next_canonical_nonce = $4 AND revision = $5
            ",
        )
        .bind(chain_id)
        .bind(finality.wallet_address)
        .bind(next_nonce)
        .bind(nonce)
        .bind(revision)
        .execute(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to advance canonical nonce ledger: {e}"))?;
        anyhow::ensure!(
            nonce_result.rows_affected() == 1,
            "Canonical nonce ledger changed during finality transition"
        );
        transaction
            .commit()
            .await
            .map_err(|e| anyhow::anyhow!("Failed to commit verified finality transition: {e}"))?;
        Ok(())
    }

    /// Loads one durable execution intent by ID.
    pub(crate) async fn get_execution_intent(
        &self,
        intent_id: i64,
    ) -> anyhow::Result<ExecutionIntentRow> {
        sqlx::query_as::<_, ExecutionIntentRow>(
            "
            SELECT

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Re-read the current execution_verification_nonce row (next_canonical_nonce and revision) and retry the finality transition with the fresh nonce/revision.
  2. Check for concurrent workers processing finality for the same wallet and ensure only one actor (or a serialized queue / advisory lock) owns the signer.
  3. Confirm a ledger row exists for the chain_id + wallet_address; create/initialize it if it is missing.
  4. Refresh the finality payload's nonce/revision from the database rather than reusing a cached snapshot in retries.
  5. Ensure all instances run against the same database and no manual edits reset revisions.

Example fix

// before: blind retry with the stale snapshot
// Err(Canonical nonce ledger changed during finality transition)
// after: reload the ledger state, then retry
let ledger = db.get_verification_nonce(chain_id, wallet).await?;
anyhow::ensure!(ledger.next_canonical_nonce == expected_nonce, "nonce mismatch; refresh finality payload");
db.record_verified_finality(&finality).await?;
Defensive patterns

Strategy: retry

Validate before calling

let ledger = sqlx::query_as::<_, (i64, i64)>(
    "SELECT next_canonical_nonce, revision FROM execution_verification_nonce WHERE chain_id = $1 AND wallet_address = $2")
    .bind(chain_id).bind(wallet).fetch_one(&pool).await?;
assert_eq!(ledger.0, expected_nonce, "stale nonce snapshot; reload finality payload");

Type guard

fn is_nonce_conflict(msg: &str) -> bool {
    msg.contains("Canonical nonce ledger changed during finality transition")
}

Try / catch

loop {
    match db.record_verified_finality(&finality).await {
        Err(e) if is_nonce_conflict(&e.to_string()) => {
            finality = refresh_nonce_state(finality).await?; // reload nonce+revision and continue
            continue;
        }
        other => break other,
    }
}

Prevention

When it happens

Trigger: Two concurrent finality transitions (or a nonce reservation) for the same chain_id + wallet_address read the same revision and one commits first; the ledger row was advanced or reset by another process between read and update; the wallet/chain has no ledger row at all (rows_affected == 0), e.g. after a reorg or manual data fix; the caller passed a stale nonce/revision pair from a cached read.

Common situations: Multiple strategy instances or replicas processing finality for the same signer wallet; a wallet's nonce ledger deleted/reseeded by an operator; retrying an old finality payload whose nonce/revision snapshot is outdated; the ledger row was never created for a newly configured wallet.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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