nautechsystems/nautilus_trader · error · anyhow::Error

Failed to persist finality verification: {e}

Error message

Failed to persist finality verification: {e}

What it means

Wraps a sqlx failure while INSERTing/UPSERTing a per-decision finality verification row (with failure_domain_ids, normalized_value_digest, revision, transition_key) into its verification table. The whole finality transaction rolls back, so nothing (headers, receipt, intent status) is committed. The root database error is embedded via `{e}`.

Source

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

                )
                ",
            )
            .bind(finality.intent_id)
            .bind(nonce)
            .bind(decision.read_class)
            .bind(height_start)
            .bind(height_end)
            .bind(finality.manifest_version)
            .bind(finality.manifest_digest)
            .bind(finality.provider_ids)
            .bind(finality.operator_ids)
            .bind(finality.failure_domain_ids)
            .bind(&decision.normalized_value_digest)
            .bind(revision)
            .bind(transition_key)
            .execute(&mut *transaction)
            .await
            .map_err(|e| anyhow::anyhow!("Failed to persist finality verification: {e}"))?;
        }

        let hash_result = sqlx::query(
            "
            UPDATE execution_transaction_hash
            SET status = $3, block_number = $4, block_hash = $5,
                receipt_success = $6, gas_used = $7, effective_gas_price = $8,
                updated_at = NOW()
            WHERE intent_id = $1 AND transaction_hash = $2
            ",
        )
        .bind(finality.intent_id)
        .bind(finality.transaction_hash)
        .bind(finality.status.as_str())
        .bind(block_number)
        .bind(finality.block_hash)
        .bind(finality.receipt_success)
        .bind(gas_used)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Read the embedded `{e}` for the exact Postgres error (constraint, type, connection)
  2. If it is a duplicate transition_key, the verification was already recorded — make the insert idempotent (ON CONFLICT DO NOTHING) or skip
  3. Run pending migrations to align the verification table schema
  4. Check bind parameter types/sizes (digest, revision, failure_domain_ids array) against column definitions

Example fix

// before
.execute(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to persist finality verification: {e}"))?;
// after
.execute(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to persist finality verification (intent={}, decision={index}): {e}", finality.intent_id))?;
Defensive patterns

Strategy: validation

Validate before calling

// Check whether this transition_key was already persisted
let seen = sqlx::query_scalar::<_, i64>(
    "SELECT COUNT(*) FROM execution_finality_verification WHERE transition_key = $1"
)
.bind(&transition_key)
.fetch_one(&mut *conn).await?;
if seen > 0 { /* already recorded — skip */ }

Try / catch

match persist_finality(...).await {
    Err(e) if e.to_string().contains("duplicate key") => Ok(()), // idempotent replay
    Err(e) if is_transient_db_error(&e) => retry_with_backoff(3, || persist_finality(...)),
    Err(e) => Err(e),
    Ok(v) => Ok(v),
}

Prevention

When it happens

Trigger: The per-decision INSERT fails: unique conflict on transition_key for a replayed finality event, schema drift (missing columns), oversized failure_domain_ids array, connection loss, or bind type mismatch on the digest/revision fields.

Common situations: Re-applying the same transition_key after a partial retry; migrations out of sync between environments; Postgres array/bytea type changes after an upgrade; large decision batches exceeding statement or packet limits.

Related errors


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