nautechsystems/nautilus_trader · error · anyhow::Error
Failed to validate finalized header ledger: {e}
Error message
Failed to validate finalized header ledger: {e} What it means
This error wraps a sqlx/PostgreSQL failure on the read-back verification SELECT that follows each header insert during ledger extension. After inserting a header, the code immediately re-selects the stored (hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest) row by (chain_id, wallet_address, number) using `fetch_one` and maps any query error to this message. The read-back exists to prove the durable row matches what was written; failing to even execute the read (connection, timeout, missing table) surfaces here.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:4497
.bind(timestamp)
.bind(&base_fee)
.bind(bootstrap.manifest_digest)
.execute(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to extend finalized header ledger: {e}"))?;
let stored = sqlx::query_as::<_, (String, String, i64, Option<String>, String)>(
"
SELECT hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
FROM execution_verified_finalized_header
WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
",
)
.bind(chain_id)
.bind(bootstrap.wallet_address)
.bind(number)
.fetch_one(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to validate finalized header ledger: {e}"))?;
anyhow::ensure!(
stored
== (
header.hash.clone(),
header.parent_hash.clone(),
timestamp,
base_fee,
bootstrap.manifest_digest.to_string(),
),
"Finalized header ledger conflicts at height {}",
header.number
);
}
let finalized_height = bootstrap
.finalized_headers
.last()
.expect("verified finalized headers are nonempty")View on GitHub (pinned to 18893faf8b)
Solutions
- Inspect the wrapped `{e}` cause to determine whether it is connectivity, timeout, permission, or missing relation.
- Run pending schema migrations so `execution_verified_finalized_header` exists with the expected columns.
- Grant SELECT on the table to the database role used by the adapter, and verify you are not pointed at a read-only replica during a write transaction.
- Increase statement/idle-in-transaction timeouts or shrink the batch so the insert+verify pair completes within limits.
- Retry the bootstrap on transient connection errors; the transaction design makes it safe to re-run.
Example fix
// before: verification read with no timeout budget on huge batches
let stored = sqlx::query_as::<_, Row>(SELECT ...).fetch_one(&mut *tx).await?;
// after: give the verification read an explicit statement timeout
sqlx::query("SET LOCAL statement_timeout = '30s'").execute(&mut *tx).await?;
let stored = sqlx::query_as::<_, Row>(SELECT ...)
.fetch_one(&mut *tx).await
.context("ledger verification read failed")?; Defensive patterns
Strategy: try-catch
Validate before calling
let can_read = sqlx::query(
"SELECT has_table_privilege(current_user, 'execution_verified_finalized_header', 'SELECT')",
).fetch_one(&pool).await?;
if !can_read.get::<bool, _>(0) {
return Err(anyhow!("db role lacks SELECT on execution_verified_finalized_header"));
} Try / catch
match verify_stored_header(&mut tx, chain_id, wallet, number).await {
Err(e) if e.to_string().contains("Failed to validate finalized header ledger") => {
// check connectivity/schema/permissions, then retry whole bootstrap
Err(anyhow!("verification read failed: {e:#}")).context("bootstrap aborted")
}
other => other,
} Prevention
- Grant the adapter role SELECT on the ledger table; never point writes at read-only replicas.
- Apply schema migrations before upgrading the adapter.
- Set realistic statement/idle-in-transaction timeouts for insert+verify pairs.
- Alert on connection-pool exhaustion during bootstrap.
When it happens
Trigger: PostgreSQL errors on the verification SELECT inside the bootstrap transaction: connection loss mid-transaction, statement timeout, relation `execution_verified_finalized_header` missing (schema not migrated), permission denied on SELECT, or the transaction aborted by a prior error leaving the connection in a failed state.
Common situations: Deploying a new adapter version against a database that has not run migrations; read-only replica or restricted role lacking SELECT on the table; long-running transaction exceeding idle-in-transaction/statement timeouts during a big bootstrap; transient network failure between insert and verification.
Understand the failure class
Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.
Related errors
- Failed to seed chain table: {e}
- Failed to call create_block_partition for chain {}: {e}
- Failed to call create_token_partition for chain {}: {e}
- Failed to get block info for chain {}: {}
- Failed to insert into block table: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/fa3d0f9b765c25c0.
Report an issue: GitHub.