nautechsystems/nautilus_trader · error

Replacement transaction {transaction_hash} has no retained p

Error message

Replacement transaction {transaction_hash} has no retained payload

What it means

A replacement transaction hash can only be attached if a row exists in `execution_transaction_hash` for (intent_id, chain_id, transaction_hash). This row is created when the transaction payload was first retained/authenticated. If the locked lookup returns no row, the hash being claimed as a verified replacement was never retained by the system, so attachment is refused.

Source

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

                "Intent cannot attach a verified replacement from status {current_status}"
            );
            let (hash_id, payload_expected, already_current) =
                sqlx::query_as::<_, (i64, bool, bool)>(
                    "
                    SELECT id, payload_expected, current
                    FROM execution_transaction_hash
                    WHERE intent_id = $1 AND chain_id = $2 AND transaction_hash = $3
                    FOR UPDATE
                    ",
                )
                .bind(scan.intent_id)
                .bind(chain_id)
                .bind(transaction_hash)
                .fetch_optional(&mut *transaction)
                .await
                .context("failed to lock authenticated replacement payload")?
                .ok_or_else(|| {
                    anyhow::anyhow!(
                        "Replacement transaction {transaction_hash} has no retained payload"
                    )
                })?;
            anyhow::ensure!(
                payload_expected,
                "Replacement transaction {transaction_hash} is not authenticated"
            );

            if !already_current {
                sqlx::query(
                    "
                    UPDATE execution_transaction_hash
                    SET current = FALSE, status = 'replaced', updated_at = NOW()
                    WHERE intent_id = $1 AND current
                    ",
                )
                .bind(scan.intent_id)
                .execute(&mut *transaction)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Retain and authenticate the replacement transaction payload through the normal retention API first, then re-run the scan record step.
  2. Normalize the hash casing/encoding so it matches how it was stored (lowercase hex, same bytes).
  3. Verify the hash belongs to this intent_id and chain_id, not another intent's replacement.

Example fix

// before: hash straight from an explorer lookup
let scan = ExecutionReplacementScan { matched_transaction_hash: Some(explorer_tx_hash), .. };
// after: retain first
let retained = db.retain_transaction_payload(chain_id, intent_id, explorer_tx_hash, payload).await?;
let scan = ExecutionReplacementScan { matched_transaction_hash: Some(retained.hash), .. };
Defensive patterns

Strategy: validation

Validate before calling

let retained = sqlx::query_scalar::<_, i64>("SELECT id FROM execution_transaction_hash WHERE intent_id=$1 AND chain_id=$2 AND transaction_hash=$3").bind(scan.intent_id).bind(chain_id).bind(tx_hash).fetch_optional(&pool).await?;
if retained.is_none() { return Err(anyhow!("replacement tx {tx_hash} was never retained for this intent")); }

Type guard

async fn hash_is_retained(pool: &PgPool, intent_id: i64, chain_id: i32, hash: &str) -> anyhow::Result<bool> {
    Ok(sqlx::query_scalar::<_, i64>("SELECT id FROM execution_transaction_hash WHERE intent_id=$1 AND chain_id=$2 AND transaction_hash=$3")
        .bind(intent_id).bind(chain_id).bind(hash).fetch_optional(pool).await?.is_some())
}

Prevention

When it happens

Trigger: Setting `scan.matched_transaction_hash` to a tx hash that was never persisted to `execution_transaction_hash` for this intent/chain — e.g. a hash observed off-chain (mempool, explorer) without going through the authenticated retention path, or a hash belonging to a different intent.

Common situations: Scanning an explorer API found the replacement but the internal retention step never ran or used a different intent id; a copy/paste or encoding difference (checksummed vs lowercase hex) makes the hash not match the stored key.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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