nautechsystems/nautilus_trader · error · anyhow::Error

Migrated terminal transaction hash was not found

Error message

Migrated terminal transaction hash was not found

What it means

This is an anyhow::ensure! invariant inside the migration: after updating execution_transaction_hash, rows_affected() must be exactly 1. Zero rows means the expected row (matching intent_id + transaction_hash, flagged current AND payload_expected) does not exist in its expected state, so the migration cannot reconstruct terminal state and aborts the transaction. The library throws it to fail loudly instead of silently migrating an intent whose hash row is missing or superseded.

Source

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

                            SET status = $3, block_number = $4, block_hash = $5,
                                receipt_success = $6, gas_used = $7,
                                effective_gas_price = $8, updated_at = NOW()
                            WHERE intent_id = $1 AND transaction_hash = $2 AND current
                              AND payload_expected
                            ",
                        )
                        .bind(record.intent_id)
                        .bind(transaction_hash)
                        .bind(status.as_str())
                        .bind(block_number)
                        .bind(block_hash)
                        .bind(receipt_success)
                        .bind(gas_used)
                        .bind(record.effective_gas_price.as_deref())
                        .execute(&mut *transaction)
                        .await
                        .map_err(|e| anyhow::anyhow!("Failed to reconstruct migrated hash: {e}"))?;
                        anyhow::ensure!(
                            hash_result.rows_affected() == 1,
                            "Migrated terminal transaction hash was not found"
                        );
                        sqlx::query(
                            "UPDATE execution_intent SET status = $2, updated_at = NOW() WHERE id = $1",
                        )
                        .bind(record.intent_id)
                        .bind(status.as_str())
                        .execute(&mut *transaction)
                        .await
                        .map_err(|e| anyhow::anyhow!("Failed to reconstruct migrated intent: {e}"))?;
                        if current_status != status.as_str() {
                            sqlx::query(
                                "
                                INSERT INTO execution_transaction_transition (
                                    intent_id, transaction_hash_id, transition_key,
                                    from_status, to_status, block_number, block_hash
                                )

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Query the DB for the exact pair: SELECT transaction_hash, current, payload_expected FROM execution_transaction_hash WHERE intent_id = '<id>' — identify which condition excludes the row.
  2. Normalize transaction hashes to lowercase hex in both the migration evidence and the database so they match exactly.
  3. Regenerate the migration evidence from the current database snapshot if it is stale; do not migrate against a diverged DB.
  4. If current = false, update the evidence to reference the currently-current row, or restore current = true if the evidence is authoritative.
  5. Re-run the migration only after evidence and DB agree; the aborted transaction leaves the DB unchanged.

Example fix

// before: evidence hash possibly checksummed
.bind(transaction_hash)

// after: normalize before binding
let tx_hash = transaction_hash.to_lowercase();
.bind(tx_hash)
Defensive patterns

Strategy: validation

Validate before calling

let matches = sqlx::query_scalar::<_, i64>(
    "SELECT COUNT(*) FROM execution_transaction_hash \
     WHERE intent_id = $1 AND transaction_hash = $2 AND current AND payload_expected",
).bind(&record.intent_id).bind(record.transaction_hash.to_lowercase())
 .fetch_one(&pool).await?;
if matches != 1 {
    anyhow::bail!("evidence hash for intent {} does not match a current, payload_expected row", record.intent_id);
}

Type guard

fn evidence_hash_matches_row(record: &MigrationRecord, stored: &str) -> bool {
    record.transaction_hash.as_deref()
        .map(|h| h.to_lowercase() == stored.to_lowercase())
        .unwrap_or(false)
}

Try / catch

match run_migration(&pool, records).await {
    Err(e) if format!("{e:#}").contains("Migrated terminal transaction hash was not found") => {
        tracing::error!("evidence/DB divergence: hash row missing, not current, or payload_expected=false");
        // regenerate evidence from current DB before retrying
    }
    other => other?,
}

Prevention

When it happens

Trigger: Running the verification migration when (1) no execution_transaction_hash row exists for the record's (intent_id, transaction_hash) pair, (2) the row exists but current = false (a later hash superseded it), (3) payload_expected = false (payload no longer expected on-chain), or (4) the transaction_hash in the evidence differs in casing/whitespace from the stored hash.

Common situations: Evidence exported from an environment where the hash row was later re-keyed or superseded; hashes stored with different casing (checksummed vs lowercase hex) between evidence and DB; migration re-run after a partial correction flipped current/payload_expected; stale evidence against a live database that has advanced.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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