nautechsystems/nautilus_trader · error · anyhow::Error

Signed transaction {transaction_hash} conflicts with its per

Error message

Signed transaction {transaction_hash} conflicts with its persisted identity

What it means

The upsert's conflict-update WHERE clause matched no row, so RETURNING produced nothing: an existing execution_transaction_hash row for (chain_id, transaction_hash) exists but belongs to a different intent, lacks payload_expected, or stores a different payload than the one being persisted. The library rejects this identity conflict rather than overwriting the persisted record.

Source

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

            RETURNING
                id, intent_id, chain_id, transaction_hash, payload_expected,
                raw_transaction, sealed_transaction, status,
                block_number, block_hash, receipt_success, gas_used,
                effective_gas_price, current
            ",
        )
        .bind(intent_id)
        .bind(chain_id_db)
        .bind(transaction_hash)
        .bind(raw_transaction)
        .bind(sealed_transaction)
        .fetch_optional(&mut *transaction)
        .await
        .map_err(|e| {
            anyhow::anyhow!("Failed to persist signed transaction {transaction_hash}: {e}")
        })?
        .ok_or_else(|| {
            anyhow::anyhow!(
                "Signed transaction {transaction_hash} conflicts with its persisted identity"
            )
        })?;

        sqlx::query(
            "UPDATE execution_intent SET status = 'signed', updated_at = NOW() WHERE id = $1",
        )
        .bind(intent_id)
        .execute(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to mark execution intent signed: {e}"))?;
        sqlx::query(
            "
            INSERT INTO execution_transaction_transition (
                intent_id, transaction_hash_id, transition_key, from_status, to_status
            ) VALUES ($1, $2, $3, $4, 'signed')
            ON CONFLICT (intent_id, transition_key) DO NOTHING
            ",

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Query the existing row: SELECT intent_id, payload_expected FROM execution_transaction_hash WHERE chain_id=$1 AND transaction_hash=$2 and reconcile which intent actually owns the hash.
  2. Fix nonce allocation so two intents never produce the same transaction hash; re-issue the losing intent with a new nonce.
  3. Do not retry with the same payload expecting success — the conflict is deterministic; investigate the differing payload bytes instead.
  4. If the row was created by an aborted/incorrect flow, correct it through the supported recovery path, never by direct UPDATE.

Example fix

// before
let existing: Option<_> = sqlx::query_scalar(
    "SELECT intent_id FROM execution_transaction_hash WHERE chain_id=$1 AND transaction_hash=$2")
    .bind(chain_id).bind(hash).fetch_optional(&pool).await?;
// after
if let Some(owner) = existing {
    anyhow::ensure!(owner == intent_id, "hash {hash} owned by intent {owner}; allocate a fresh nonce");
}
Defensive patterns

Strategy: validation

Validate before calling

let owner: Option<i64> = sqlx::query_scalar(
    "SELECT intent_id FROM execution_transaction_hash WHERE chain_id=$1 AND transaction_hash=$2")
    .bind(chain_id as i32).bind(transaction_hash).fetch_optional(&pool).await?;
anyhow::ensure!(owner.map_or(true, |o| o == intent_id), "hash {transaction_hash} already persisted under intent {owner:?}");

Try / catch

match db.add_execution_transaction(intent_id, chain_id, hash, sealed).await {
    Err(e) if e.to_string().contains("conflicts with its persisted identity") => {
        // deterministic conflict: reconcile ownership / reissue intent, do not blind-retry
        alert_identity_conflict(intent_id, hash);
    }
    r => r?,
}

Prevention

When it happens

Trigger: Persisting a signed transaction whose (chain_id, transaction_hash) already exists under another intent_id (reused nonce producing the same hash), or re-persisting the same hash with different raw/sealed bytes than stored, or the existing row has payload_expected = FALSE.

Common situations: Duplicate nonce assignment causing two intents to produce the identical transaction hash; a corrupted or partially-migrated row; replaying a signing job against a database where the hash row was created by a different intent; mixed deployments producing different sealed bytes for the same transaction.

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/f0880570e4ea526c. Report an issue: GitHub.