nautechsystems/nautilus_trader · error · anyhow::Error

Execution transaction {} contains both plaintext and sealed

Error message

Execution transaction {} contains both plaintext and sealed payloads

What it means

While migrating plaintext rows to sealed envelopes, each fetched row must be plaintext-only. A row carrying both raw_transaction and sealed_transaction is ambiguous (it would be double-sealed or hand-modified), so migration aborts citing the offending transaction hash id to protect data integrity.

Source

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

            ",
        )
        .bind(batch_size)
        .fetch_all(&mut *transaction)
        .await
        .context("failed to load legacy signed transaction batch")?;

        if rows.is_empty() {
            self.complete_execution_payload_migration(&mut transaction, keys)
                .await?;
            transaction
                .commit()
                .await
                .context("failed to commit execution payload migration completion")?;
            return Ok(true);
        }

        for hash in rows {
            anyhow::ensure!(
                hash.sealed_transaction.is_none(),
                "Execution transaction {} contains both plaintext and sealed payloads",
                hash.id
            );
            let raw_transaction = hash
                .raw_transaction
                .as_deref()
                .expect("migration query requires plaintext");
            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
            let context = authenticate_retained_payload(
                raw_transaction,
                &intent,
                &hash,
                keys.deployment_id(),
            )
            .with_context(|| format!("failed to authenticate execution payload {}", hash.id))?;
            reserve_execution_payload_seal(&mut transaction, keys.active_key_id()).await?;
            let envelope = keys.seal(raw_transaction, &context)?;

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Re-run the batch — the fenced transaction should re-select only consistent rows
  2. If the row genuinely holds both payloads, repair it via the library (re-seal from plaintext, clearing the stale sealed value) or restore from backup
  3. Ensure only one migration/sealing process runs at a time against the table
  4. Audit for manual writes to execution_transaction_hash and restrict direct table access

Example fix

-- find inconsistent rows
SELECT id FROM execution_transaction_hash
WHERE raw_transaction IS NOT NULL AND sealed_transaction IS NOT NULL;
-- repair: clear stale sealed value (only if plaintext is authoritative) then retry migration
UPDATE execution_transaction_hash SET sealed_transaction = NULL
WHERE id = <offending_id>;
Defensive patterns

Strategy: retry

Validate before calling

let bad: i64 = sqlx::query_scalar(
    "SELECT COUNT(*) FROM execution_transaction_hash
     WHERE raw_transaction IS NOT NULL AND sealed_transaction IS NOT NULL",
).fetch_one(&pool).await?;
if bad > 0 { anyhow::bail!("{bad} rows hold both plaintext and sealed payloads; repair before migrating"); }

Try / catch

match db.migrate_execution_payload_batch(&keys, 500).await {
    Ok(done) => { /* continue batches */ }
    Err(e) if e.to_string().contains("both plaintext and sealed") => {
        // extract hash id from message, repair the row, resume migration
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: During migrate_execution_payload_batch, an execution_transaction_hash row is returned with sealed_transaction set even though the query selected pending (plaintext) rows — the row changed concurrently or was written inconsistently.

Common situations: Another process sealed the row between selection and processing without proper fencing; manual UPDATEs on execution_transaction_hash; a previous failed migration wrote sealed values without clearing plaintext.

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/56ea68165ae43483. Report an issue: GitHub.