nautechsystems/nautilus_trader · error · anyhow::Error
Failed to lock finalized header tip: {e}
Error message
Failed to lock finalized header tip: {e} What it means
This error wraps a sqlx failure on the SELECT that locks/reads the current tip (highest `number`) of `execution_verified_finalized_header` when the ledger is already initialized. The code fetches the latest stored header to confirm the incoming header extension starts exactly at the durable tip; if that fetch fails (no rows, table missing, connection error) the error is raised with this message and initialization aborts.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:4432
"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_digest
FROM execution_verified_finalized_header
WHERE chain_id = $1 AND wallet_address = $2
ORDER BY number DESC
LIMIT 1
",
)
.bind(chain_id)
.bind(bootstrap.wallet_address)
.fetch_one(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
anyhow::ensure!(
stored_tip
== (
i64::try_from(first_header.number)
.context("Verified finalized height exceeds PostgreSQL BIGINT")?,
first_header.hash.clone(),
first_header.parent_hash.clone(),
i64::try_from(first_header.timestamp)
.context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?,
first_header.base_fee_per_gas.map(|value| value.to_string()),
bootstrap.manifest_digest.to_string(),
),
"Verified finalized header extension does not start at the durable tip"
);
} else {
anyhow::ensure!(
first_header.number == bootstrap.checkpoint_number
&& first_header.hash == bootstrap.checkpoint_hashView on GitHub (pinned to 18893faf8b)
Solutions
- Read the wrapped sqlx error: RowNotFound means the ledger has no rows — reset the `initialized` state (or the whole wallet ledger) so bootstrap takes the fresh-checkpoint path instead of the tip-extension path.
- Check for concurrent writers/cleaners touching `execution_verified_finalized_header`; serialize access or use advisory locking between adapter instances.
- Run schema migrations if the error indicates missing table/columns.
- Verify connection stability and statement timeouts; re-run initialization — it is transactional and idempotent.
Example fix
// before
.fetch_one(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
// after
.fetch_one(&mut *transaction)
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to lock finalized header tip for chain {chain_id}: {e:#}"
)
})?; Defensive patterns
Strategy: validation
Validate before calling
// Confirm tip row presence before the tip-extension path
let tip: Option<(i64, String)> = sqlx::query_as(
"SELECT number, hash FROM execution_verified_finalized_header WHERE chain_id=$1 AND wallet_address=$2 ORDER BY number DESC LIMIT 1",
).bind(chain_id).bind(wallet).fetch_optional(&pool).await?;
anyhow::ensure!(
tip.is_some(),
"Ledger marked initialized but has no headers — reset initialization state so bootstrap takes the fresh-checkpoint path"
); Try / catch
match result {
Err(sqlx::Error::RowNotFound) => {
warn!("tip row missing while initialized=true; falling back to fresh checkpoint bootstrap");
initialize_from_checkpoint(bootstrap)
}
Err(e) => Err(anyhow!("tip lock failed: {e:#}")),
Ok(tip) => validate_tip_extension(tip),
} Prevention
- Do not run multiple adapter instances against the same (chain_id, wallet_address) ledger
- Block external deletion/truncation of execution_verified_finalized_header while initialized
- Take the fresh-checkpoint path when the ledger is empty rather than assuming a tip exists
- Monitor for DB failovers and restart the bootstrap transaction cleanly
When it happens
Trigger: During re-initialization of an existing ledger: the tip SELECT (ORDER BY number DESC LIMIT 1, fetch_one) returns zero rows because the table was truncated between the initialized flag and this query; schema drift; connection loss mid-transaction.
Common situations: Another process or job deleted all verified-header rows for this (chain_id, wallet_address) after initialization; running two adapter instances against the same ledger where one wiped rows; Postgres failover killing the transaction; deploying against a half-migrated schema.
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/321fe859b72be8a6.
Report an issue: GitHub.