nautechsystems/nautilus_trader · error
Execution intent {} cannot own canonical nonce {}
Error message
Execution intent {} cannot own canonical nonce {} What it means
After locking the intent row, an `anyhow::ensure!` verifies the intent's chain_id, wallet_address, status (must be "prepared"), active flag, and prior nonce all match the assignment being made (an already-assigned nonce must equal the requested one). If any check fails, this error is thrown. It is an ownership/state-consistency guard: the intent being assigned does not actually belong to this chain/wallet at this point in the state machine, or already owns a different nonce.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:5957
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| {
anyhow::anyhow!("Failed to lock execution intent for nonce assignment: {e}")
})?
.ok_or_else(|| {
anyhow::anyhow!("Execution intent {} was not found", assignment.intent_id)
})?;
anyhow::ensure!(
intent_chain_id == chain_id
&& intent_wallet == assignment.wallet_address
&& intent_status == "prepared"
&& intent_active
&& intent_nonce.is_none_or(|assigned| assigned == nonce),
"Execution intent {} cannot own canonical nonce {}",
assignment.intent_id,
assignment.nonce
);
for (index, decision) in assignment.decisions.iter().enumerate() {
let height_start = decision
.height_start
.map(i64::try_from)
.transpose()
.context("Verification height exceeds PostgreSQL BIGINT")?;
let height_end = decision
.height_endView on GitHub (pinned to 18893faf8b)
Solutions
- Re-read the intent (chain_id, wallet, status, active, nonce) before assignment and only call this method for intents in `prepared`/active state with no nonce or the matching nonce.
- Ensure the assignment is built from the same chain_id and wallet_address used when locking the canonical nonce ledger row.
- Handle the already-assigned case idempotently: if intent.nonce equals the requested nonce, treat assignment as already done instead of re-assigning.
- Check for concurrent assignment races and cancel/re-create intents that have left the prepared state.
Example fix
// before: blindly assigning
let assignment = ExecutionNonceAssignment { intent_id, chain_id, wallet: other_wallet, nonce, .. };
db.assign_execution_intent_nonce_verified(&assignment).await?;
// after: validate intent state first
let intent = db.get_execution_intent(intent_id).await?;
anyhow::ensure!(intent.status == "prepared" && intent.active, "intent {} not assignable", intent_id);
anyhow::ensure!(intent.chain_id == assignment.chain_id && intent.wallet_address == assignment.wallet, "intent ownership mismatch");
db.assign_execution_intent_nonce_verified(&assignment).await?; Defensive patterns
Strategy: validation
Validate before calling
let (chain_id, wallet, status, active, nonce): (i32, String, String, bool, Option<i64>) = sqlx::query_as(
"SELECT chain_id, wallet_address, status, active, nonce FROM execution_intent WHERE id = $1")
.bind(&assignment.intent_id).fetch_one(pool).await?;
anyhow::ensure!(status == "prepared" && active, "intent {} not in prepared+active state", assignment.intent_id);
anyhow::ensure!(chain_id == assignment.chain_id && wallet == assignment.wallet_address, "intent ownership mismatch");
anyhow::ensure!(nonce.is_none() || nonce == Some(assignment.nonce), "intent already owns a different nonce"); Try / catch
match db.assign_execution_intent_nonce_verified(&assignment).await {
Err(e) if e.to_string().contains("cannot own canonical nonce") => { /* re-fetch intent; handle already-assigned or invalid state */ }
other => other?,
} Prevention
- Only assign nonces to intents you can prove are `prepared` and `active`.
- Treat assignment as idempotent when intent.nonce already equals the target nonce.
- Build assignments from the same (chain_id, wallet) pair used to prepare the intent; validate before calling.
- Route every intent mutation through the database API so the state machine cannot be bypassed.
When it happens
Trigger: Calling `assign_execution_intent_nonce_verified` when: the intent belongs to a different chain_id or wallet than the ledger row selected; the intent status is not "prepared" (already assigned, executed, cancelled, or expired); the intent is marked inactive; or the intent already has a nonce recorded that differs from the requested canonical nonce.
Common situations: Double-assignment attempts after a first assignment succeeded (status moved past "prepared"); a cancelled or superseded intent reused from stale state; caller mixing up wallets or chains when building the assignment; a nonce re-fetch produced a newer canonical nonce than the one already written to the intent.
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
- 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
- Finalized header ledger conflicts at height {}
- Execution payload storage is not rewrapping
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/b61a0a632d2b697d.
Report an issue: GitHub.