nautechsystems/nautilus_trader · error · anyhow::Error
Failed to validate finalized checkpoint ledger: {e}
Error message
Failed to validate finalized checkpoint ledger: {e} What it means
This error wraps a sqlx failure on the SELECT that reads back the just-written checkpoint row from `execution_verified_finalized_header`. After inserting the checkpoint, the code fetches it (fetch_one) to verify what was durably stored; if the row cannot be fetched — table missing, connection failure, or the row genuinely absent — the error surfaces with this message.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:4403
.bind(checkpoint_timestamp)
.bind(base_fee)
.bind(bootstrap.manifest_digest)
.execute(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to initialize finalized header ledger: {e}"))?;
let stored_checkpoint = sqlx::query_as::<_, (String, String, i64, Option<String>)>(
"
SELECT hash, parent_hash, timestamp, base_fee_per_gas
FROM execution_verified_finalized_header
WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
",
)
.bind(chain_id)
.bind(bootstrap.wallet_address)
.bind(checkpoint_number)
.fetch_one(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to validate finalized checkpoint ledger: {e}"))?;
anyhow::ensure!(
stored_checkpoint
== (
bootstrap.checkpoint_hash.to_string(),
bootstrap.checkpoint_parent_hash.to_string(),
checkpoint_timestamp,
bootstrap
.checkpoint_base_fee_per_gas
.map(|value| value.to_string()),
),
"Finalized checkpoint ledger conflicts with the trusted chain anchor"
);
if initialized {
let stored_tip =
sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
"
SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digestView on GitHub (pinned to 18893faf8b)
Solutions
- Read the wrapped sqlx error: 'no rows returned' (RowNotFound) means the checkpoint row vanished — check for external jobs modifying `execution_verified_finalized_header`; 'relation does not exist' means run schema migrations.
- Verify nothing outside this transaction deletes or modifies rows at the checkpoint (chain_id, wallet_address, number).
- Confirm the preceding checkpoint INSERT succeeded and committed in the same transaction (no partial commits).
- Check connection health/pool settings; re-run bootstrap after transient DB issues — it is idempotent.
Example fix
// before
.fetch_one(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to validate finalized checkpoint ledger: {e}"))?;
// after
.fetch_one(&mut *transaction)
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to validate finalized checkpoint ledger at number {checkpoint_number}: {e:#}"
)
})?; Defensive patterns
Strategy: validation
Validate before calling
// After inserting the checkpoint, use fetch_optional and distinguish 'missing' from 'error'
let stored: Option<(String, String, i64, Option<String>)> = sqlx::query_as(
"SELECT hash, parent_hash, timestamp, base_fee_per_gas FROM execution_verified_finalized_header WHERE chain_id=$1 AND wallet_address=$2 AND number=$3",
).bind(chain_id).bind(wallet).bind(number).fetch_optional(&mut *tx).await?;
anyhow::ensure!(stored.is_some(), "checkpoint row missing after insert — external deletion detected"); Try / catch
match result {
Err(sqlx::Error::RowNotFound) => return Err(anyhow!("checkpoint row vanished between insert and read — check for external table mutations")),
Err(e) => return Err(anyhow!("checkpoint validation failed: {e:#}")),
Ok(row) => verify(row),
} Prevention
- Never run external cleanup/pruning jobs against execution_verified_finalized_header while the adapter runs
- Keep the insert and read-back in the same transaction (already the case) so visibility is consistent
- Alert on RowNotFound for rows the adapter itself just wrote
- Restore databases from matching-version snapshots only
When it happens
Trigger: fetch_one returns zero rows (RowNotFound) because the preceding ON CONFLICT DO NOTHING insert was skipped while the row was later deleted, or because a trigger/cleanup removed it; schema drift on the table; connection drop mid-transaction.
Common situations: External cleanup jobs pruning rows between the INSERT and SELECT (same transaction, so usually a connection issue); running against a database where another process manages the table; Postgres failover breaking the transaction; stale schema missing columns hash/parent_hash/timestamp/base_fee_per_gas.
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/aa4d76f258dd6878.
Report an issue: GitHub.