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>(
"
SELECTView on GitHub (pinned to 18893faf8b)
Solutions
- Re-read the current execution_verification_nonce row (next_canonical_nonce and revision) and retry the finality transition with the fresh nonce/revision.
- Check for concurrent workers processing finality for the same wallet and ensure only one actor (or a serialized queue / advisory lock) owns the signer.
- Confirm a ledger row exists for the chain_id + wallet_address; create/initialize it if it is missing.
- Refresh the finality payload's nonce/revision from the database rather than reusing a cached snapshot in retries.
- 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
- Ensure only one worker owns finality processing per signer wallet (leader election or advisory lock).
- Never reuse cached nonce/revision snapshots across retries — re-read from the database.
- Verify the ledger row exists for every configured chain/wallet pair.
- Avoid manual edits to execution_verification_nonce; use the library's own flows.
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
- Execution nonce {} does not match canonical nonce {next_nonc
- Execution intent {} is not prepared for canonical nonce {}
- Verified finalized transaction count advanced without an act
- Failed to lock execution verification state: {e}
- Failed to read canonical nonce ledger: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/8b13ec9571a58a11.
Report an issue: GitHub.