{"record":{"id":"028016c0b11ed4ec","repo":"nautechsystems/nautilus_trader","slug":"execution-payload-contains-both-representations","errorCode":null,"errorMessage":"Execution payload {} contains both representations during rollback","messagePattern":"Execution payload (.+?) contains both representations during rollback","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/cache/database.rs","lineNumber":5691,"sourceCode":"            for statement in [\n                \"DROP TRIGGER IF EXISTS execution_transaction_payload_fence ON execution_transaction_hash\",\n                \"DELETE FROM execution_payload_state WHERE component = 'signed_transactions'\",\n                \"DELETE FROM execution_schema_version WHERE component = 'evm_execution_payload'\",\n            ] {\n                sqlx::query(statement)\n                    .execute(&mut *transaction)\n                    .await\n                    .context(\"failed to complete execution payload rollback\")?;\n            }\n            transaction\n                .commit()\n                .await\n                .context(\"failed to commit execution payload rollback completion\")?;\n            return Ok(true);\n        }\n\n        for hash in rows {\n            anyhow::ensure!(\n                hash.raw_transaction.is_none(),\n                \"Execution payload {} contains both representations during rollback\",\n                hash.id\n            );\n            let envelope = hash\n                .sealed_transaction\n                .as_deref()\n                .expect(\"rollback query requires envelope\");\n            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;\n            let context = payload_context(&intent, &hash, keys.deployment_id())?;\n            let raw_transaction = keys.unseal(envelope, &context)?;\n            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;\n            let result = sqlx::query(\n                \"UPDATE execution_transaction_hash SET raw_transaction = $2, updated_at = NOW() \\\n                 WHERE id = $1 AND raw_transaction IS NULL AND sealed_transaction = $3\",\n            )\n            .bind(hash.id)\n            .bind(&raw_transaction)","sourceCodeStart":5673,"sourceCodeEnd":5709,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/blockchain/src/cache/database.rs#L5673-L5709","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the offending row (id from the message) and decide which representation is authoritative; typically keep raw_transaction and set sealed_transaction = NULL.","Remove the duplicate representation with a targeted UPDATE so the row holds only one payload, then rerun the rollback.","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.","Ensure no script bypasses the library's two-step UPDATE with its own writes; always roll back through rollback_execution_payload."],"exampleFix":"// before: row with both representations aborts rollback\n// -- execution_transaction_hash: raw_transaction='0xabc...', sealed_transaction='0xsealed...'\n\n// after: clear the duplicate representation, then retry\n// UPDATE execution_transaction_hash\n// SET sealed_transaction = NULL, updated_at = NOW()\n// WHERE id = $offending_id AND raw_transaction IS NOT NULL;","handlingStrategy":"validation","validationCode":"let dupes: Vec<i64> = sqlx::query_scalar(\n    \"SELECT id FROM execution_transaction_hash \\\n     WHERE raw_transaction IS NOT NULL AND sealed_transaction IS NOT NULL\",\n).fetch_all(&mut conn).await?;\nif !dupes.is_empty() {\n    return Err(anyhow!(\"rows hold both payload representations: {:?}\", dupes));\n}","typeGuard":null,"tryCatchPattern":"match db.rollback_execution_payload(&keys, batch).await {\n    Err(e) if e.to_string().contains(\"both representations\") => {\n        // parse row id from message, clear sealed_transaction for that row, retry\n    },\n    other => other?,\n}","preventionTips":["Keep the payload fence trigger active outside of controlled maintenance windows.","Perform any payload repair via the library API, not ad-hoc SQL.","Check for both-representation rows after any crash during seal/rollback operations.","Keep backups of the table per-representation, never merged."],"tags":["database","data-integrity","rollback","encryption","postgresql"],"backgroundTag":"internal-invariant-violation","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}