nautechsystems/nautilus_trader · error · anyhow::Error

Execution payload contains both representations during…

Error message

Execution payload {} contains both representations during rollback

What it means

During a rollback batch, each row selected for conversion (sealed_transaction present) must not already contain a raw_transaction — a row holding both representations simultaneously is an invariant breach of the single-representation rule enforced while protection is active. The code asserts raw_transaction IS NONE per row before unsealing, and aborts the whole transaction naming the offending row id when both representations coexist.

Solutions

  1. Inspect the offending row (id from the message) and decide which representation is authoritative; typically keep raw_transaction and set sealed_transaction = NULL.
  2. Remove the duplicate representation with a targeted UPDATE so the row holds only one payload, then rerun the rollback.
  3. Audit for other rows with both columns set (WHERE raw_transaction IS NOT NULL AND sealed_transaction IS NOT NULL) and fix them all before retrying.
  4. Ensure no script bypasses the library's two-step UPDATE with its own writes; always roll back through rollback_execution_payload.

Example fix

// before: row with both representations aborts rollback
// -- execution_transaction_hash: raw_transaction='0xabc...', sealed_transaction='0xsealed...'

// after: clear the duplicate representation, then retry
// UPDATE execution_transaction_hash
// SET sealed_transaction = NULL, updated_at = NOW()
// WHERE id = $offending_id AND raw_transaction IS NOT NULL;
Defensive patterns

Strategy: validation

Validate before calling

let dupes: Vec<i64> = sqlx::query_scalar(
    "SELECT id FROM execution_transaction_hash \
     WHERE raw_transaction IS NOT NULL AND sealed_transaction IS NOT NULL",
).fetch_all(&mut conn).await?;
if !dupes.is_empty() {
    return Err(anyhow!("rows hold both payload representations: {:?}", dupes));
}

Try / catch

match db.rollback_execution_payload(&keys, batch).await {
    Err(e) if e.to_string().contains("both representations") => {
        // parse row id from message, clear sealed_transaction for that row, retry
    },
    other => other?,
}

Prevention

When it happens

Trigger: A row in execution_transaction_hash has both raw_transaction and sealed_transaction set while payload_expected=true and the rollback batch selects it. This happens if a previous partially-failed rollback committed the raw write but not the envelope clear in a non-atomic way, or if manual edits/other tooling wrote both columns.

Common situations: Crash/recovery between a manual replay of the two UPDATE steps; custom maintenance scripts writing raw_transaction without clearing sealed_transaction; restored rows from backups with both columns populated; bypassing the app's fence trigger with direct SQL updates.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

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

            for statement in [
                "DROP TRIGGER IF EXISTS execution_transaction_payload_fence ON execution_transaction_hash",
                "DELETE FROM execution_payload_state WHERE component = 'signed_transactions'",
                "DELETE FROM execution_schema_version WHERE component = 'evm_execution_payload'",
            ] {
                sqlx::query(statement)
                    .execute(&mut *transaction)
                    .await
                    .context("failed to complete execution payload rollback")?;
            }
            transaction
                .commit()
                .await
                .context("failed to commit execution payload rollback completion")?;
            return Ok(true);
        }

        for hash in rows {
            anyhow::ensure!(
                hash.raw_transaction.is_none(),
                "Execution payload {} contains both representations during rollback",
                hash.id
            );
            let envelope = hash
                .sealed_transaction
                .as_deref()
                .expect("rollback query requires envelope");
            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
            let context = payload_context(&intent, &hash, keys.deployment_id())?;
            let raw_transaction = keys.unseal(envelope, &context)?;
            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;
            let result = sqlx::query(
                "UPDATE execution_transaction_hash SET raw_transaction = $2, updated_at = NOW() \
                 WHERE id = $1 AND raw_transaction IS NULL AND sealed_transaction = $3",
            )
            .bind(hash.id)
            .bind(&raw_transaction)

View on GitHub (pinned to 18893faf8b)