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_end

View on GitHub (pinned to 18893faf8b)

Solutions

  1. 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.
  2. Ensure the assignment is built from the same chain_id and wallet_address used when locking the canonical nonce ledger row.
  3. Handle the already-assigned case idempotently: if intent.nonce equals the requested nonce, treat assignment as already done instead of re-assigning.
  4. 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

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


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