nautechsystems/nautilus_trader · error · anyhow::Error
Execution payload rollback left {invalid} invalid row(s)
Error message
Execution payload rollback left {invalid} invalid row(s) What it means
When the rollback batch finds no remaining sealed rows, it locks the whole execution_transaction_hash table and counts rows whose payload representation contradicts their payload_expected flag (raw/sealed present or missing in the wrong combination). If that count is nonzero, the rollback is incomplete or inconsistent, so it refuses to drop the fence trigger and protection state, throwing this error instead of committing a partially converted table.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:5668
.bind(batch_size)
.fetch_all(&mut *transaction)
.await
.context("failed to load execution payload rollback batch")?;
if rows.is_empty() {
sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE ROW EXCLUSIVE MODE")
.execute(&mut *transaction)
.await
.context("failed to lock execution payload rollback completion")?;
let invalid = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM execution_transaction_hash \
WHERE (payload_expected AND (raw_transaction IS NULL OR sealed_transaction IS NOT NULL)) \
OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL))",
)
.fetch_one(&mut *transaction)
.await
.context("failed to verify execution payload rollback completion")?;
anyhow::ensure!(
invalid == 0,
"Execution payload rollback left {invalid} invalid row(s)"
);
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")?;View on GitHub (pinned to 18893faf8b)
Solutions
- Run the same COUNT query to identify the offending rows: SELECT id FROM execution_transaction_hash WHERE (payload_expected AND (raw_transaction IS NULL OR sealed_transaction IS NOT NULL)) OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL)).
- Fix the inconsistent rows (backfill raw_transaction for payload_expected rows, or clear payloads for rows not expecting them) so each row matches its payload_expected flag.
- Ensure all writers are stopped or using a schema version consistent with the rollback before retrying rollback_execution_payload.
- If rows are unrecoverable, quarantine or delete them deliberately, then rerun the rollback to completion.
Example fix
-- diagnose
SELECT id, payload_expected, raw_transaction IS NULL AS no_raw,
sealed_transaction IS NOT NULL AS has_sealed
FROM execution_transaction_hash
WHERE (payload_expected AND (raw_transaction IS NULL OR sealed_transaction IS NOT NULL))
OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL));
// before: retrying rollback with inconsistent rows
// db.rollback_execution_payload(&keys, 1000).await?; // fails with "left N invalid row(s)"
// after: repair rows first, then retry
// UPDATE execution_transaction_hash SET sealed_transaction = NULL WHERE id = $bad_id; -- after restoring raw_transaction Defensive patterns
Strategy: validation
Validate before calling
let invalid: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM execution_transaction_hash \
WHERE (payload_expected AND (raw_transaction IS NULL OR sealed_transaction IS NOT NULL)) \
OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL))",
).fetch_one(&mut conn).await?;
if invalid != 0 {
return Err(anyhow!("{invalid} payload rows inconsistent; repair before rollback"));
} Try / catch
match db.rollback_execution_payload(&keys, batch).await {
Err(e) if e.to_string().contains("invalid row(s)") => {
// run the diagnostic COUNT query, repair rows, retry once
},
other => other?,
} Prevention
- Add a CHECK constraint mirroring the payload_expected/raw/sealed invariants so bad rows cannot be inserted.
- Quiesce writers before starting a rollback.
- Never restore partial backups directly into a live execution_transaction_hash table.
- Periodically run the consistency COUNT query as a health check.
When it happens
Trigger: Completing the final rollback batch while some rows still have raw_transaction IS NULL despite payload_expected, sealed_transaction NOT NULL, or rows with payloads despite payload_expected=false — i.e. rows modified outside the rollback loop, inserted during rollback with wrong representation, or corrupted by manual edits.
Common situations: Rows inserted by a concurrent writer (with a stale trigger/fence) during a long rollback; manual data fixes that set raw_transaction or sealed_transaction inconsistently; restoring the table from a partial backup; a previous failed migration that left mixed-representation rows.
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
- Execution payload {} contains both representations during ro
- Cannot safely migrate {unresolved_legacy} unresolved executi
- Failed to activate execution schema version 2: {e}
- Failed to commit execution schema migration: {e}
- Failed to install verification schema: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/0a97af296c6066a2.
Report an issue: GitHub.