nautechsystems/nautilus_trader · error · anyhow::Error
Canonical nonce ledger changed during verification bootstrap
Error message
Canonical nonce ledger changed during verification bootstrap
What it means
During verification bootstrap for an already-initialized signer, the stored next_canonical_nonce in execution_verification_nonce no longer equals the caller-supplied next_canonical_nonce (crates/adapters/blockchain/src/cache/database.rs:4032). The canonical nonce ledger is the durable record of how many finalized transactions this wallet has sent; if it moved between the caller's snapshot and this bootstrap transaction, state changed underneath and the bootstrap refuses to proceed rather than verify against a stale ledger view.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:4032
.bind(bootstrap.wallet_address)
.fetch_optional(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to read canonical nonce ledger: {e}"))?;
let initialized = current.is_some();
let revision = if let Some((manifest_version, manifest_digest, stored_nonce, revision)) =
current
{
anyhow::ensure!(
bootstrap.migration.is_none(),
"Verification migration was supplied for an initialized signer"
);
anyhow::ensure!(
manifest_version == bootstrap.manifest_version
&& manifest_digest == bootstrap.manifest_digest,
"Execution verification manifest identity changed"
);
anyhow::ensure!(
stored_nonce == next_canonical_nonce,
"Canonical nonce ledger changed during verification bootstrap"
);
if observed_canonical_nonce != stored_nonce {
let expected_observed_nonce = stored_nonce
.checked_add(1)
.ok_or_else(|| anyhow::anyhow!("Canonical nonce overflow"))?;
anyhow::ensure!(
observed_canonical_nonce == expected_observed_nonce,
"Verified finalized transaction count is outside the owned recovery range"
);
let recovery = sqlx::query_as::<_, (Option<i64>, String, i64)>(
"
SELECT
intent.nonce,
intent.status,
COUNT(hash.id) FILTER (View on GitHub (pinned to 18893faf8b)
Solutions
- Re-read the current canonical nonce (SELECT next_canonical_nonce FROM execution_verification_nonce WHERE chain_id=$1 AND wallet_address=$2) and retry bootstrap with the fresh value.
- Ensure only one instance performs verification bootstrap per signer at a time (leader election / advisory lock).
- Minimize the window between reading the nonce and bootstrapping; do not bootstrap from long-lived cached snapshots.
- If a stale snapshot was replayed after recovery, rebuild the snapshot from the current database state.
Example fix
// before let next_canonical_nonce = cached_snapshot.next_canonical_nonce; // stale bootstrap(chain_id, wallet, next_canonical_nonce, observed)?; // after let next_canonical_nonce = db.current_canonical_nonce(chain_id, wallet).await?; // read fresh, inside the same lock window bootstrap(chain_id, wallet, next_canonical_nonce, observed).await?;
Defensive patterns
Strategy: validation
Validate before calling
let current = sqlx::query_scalar::<_, i64>(
"SELECT next_canonical_nonce FROM execution_verification_nonce WHERE chain_id=$1 AND wallet_address=$2",
).bind(chain_id).bind(wallet).fetch_optional(&pool).await?;
if current != Some(next_canonical_nonce) {
return Err(anyhow::anyhow!("stale canonical nonce snapshot; re-read before bootstrap"));
} Try / catch
match bootstrap_verification(...).await {
Err(e) if e.to_string().contains("Canonical nonce ledger changed") => {
// re-read the fresh nonce and retry once; if it fails again another writer is active
}
other => other?,
} Prevention
- Read the canonical nonce immediately before bootstrap, never from a long-lived cache
- Ensure a single writer/leader performs bootstrap per (chain_id, wallet)
- Use advisory locks or leader election to serialize bootstrap across replicas
When it happens
Trigger: Calling verification bootstrap with a next_canonical_nonce argument captured from an earlier read while another process/instance concurrently finalized transactions and advanced the ledger row for the same chain_id and wallet_address; passing a nonce snapshot from a stale cache or a different environment.
Common situations: Two replicas of the execution engine bootstrapping against the same database; a long gap between snapshotting the nonce and running bootstrap while live trading finalized transactions; replaying an old process state after a crash.
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
- Execution schema version {} is newer than supported version
- Verified finalized transaction count advanced without an act
- Restored transaction purpose is inconsistent
- Failed to lock execution verification state: {e}
- Verified finalized transaction count is outside the owned re
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/d58809974cb026f1.
Report an issue: GitHub.