nautechsystems/nautilus_trader · error
rollback query requires envelope
Error message
rollback query requires envelope
What it means
During the rollback query path, a transaction-hash row contains both a raw plaintext and a sealed representation, and the code dereferences `sealed_transaction` expecting the sealed envelope — panicking with "rollback query requires envelope" if it is `None`. The rollback path decrypts from the sealed envelope, so a missing envelope is treated as an invariant violation.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:5699
.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)
.bind(envelope)
.execute(&mut *transaction)
.await
.context("failed to recreate plaintext execution payload")?;
anyhow::ensure!(
result.rows_affected() == 1,
"Execution payload {} changed during rollback",
hash.idView on GitHub (pinned to 18893faf8b)
Solutions
- Seal the plaintext payload (or re-run the migration that produces envelopes) before rolling back.
- Filter plaintext-only rows and handle them without unsealing in the rollback path.
- Backfill `sealed_transaction` for affected rows.
- Ensure the rollback code path matches the storage representation of the rows being processed.
Example fix
// before
let envelope = hash.sealed_transaction.as_deref()
.expect("rollback query requires envelope");
// after
let envelope = match hash.sealed_transaction.as_deref() {
Some(env) => env,
None => {
let raw = hash.raw_transaction.as_deref()
.ok_or_else(|| anyhow!("row {} has neither payload form", hash.id))?;
// proceed using the plaintext form directly
raw
}
}; Defensive patterns
Strategy: validation
Validate before calling
if hash.sealed_transaction.is_none() {
// handle as plaintext-only row, or seal it before rollback
} Type guard
fn has_envelope(h: &ExecutionTransactionHashRow) -> bool { h.sealed_transaction.is_some() } Try / catch
let Some(envelope) = hash.sealed_transaction.as_deref() else {
return Err(anyhow!("row {} stored plaintext-only; seal before rollback", hash.id));
}; Prevention
- Ensure the sealing migration completes before running rollback queries.
- Detect and handle plaintext-only rows explicitly in rollback code.
- Audit payload representation columns before schema rollbacks.
When it happens
Trigger: Calling the rollback query on an `execution_transaction_hash` row whose `sealed_transaction` is NULL (the row was stored plaintext-only), after detecting both representations present in the row logic.
Common situations: Rolling back a database where rows were written plaintext-only by an older code path; partial migration leaving rows without sealed envelopes; schema/data written by a version that never sealed payloads.
Related errors
- migration query requires plaintext
- Implement FromRow for FuturesSpread
- Implement FromRow for OptionSpread
- Execution payload storage is in {operation} maintenance, not
- Missing block timestamp for pool snapshot {} at block {}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/9357bbf7e9709977.
Report an issue: GitHub.