nautechsystems/nautilus_trader · error · anyhow::Error
Verified finalized header extension does not start at the du
Error message
Verified finalized header extension does not start at the durable tip
What it means
This error is raised while extending the durable `execution_verified_finalized_header` ledger during a verified-finalized bootstrap in PostgreSQL. When the ledger is already initialized (rows exist), the code reads the durable tip (highest-numbered header for this chain_id/wallet_address) and `anyhow::ensure!`s that the first header of the incoming verified batch is byte-identical to that tip (number, hash, parent_hash, timestamp, base_fee, manifest_digest). If it differs, the new batch would create a gap or a fork in the on-disk ledger, so the write is aborted inside the transaction. It is a data-integrity guard against appending finalized headers that do not contiguously extend the stored chain.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:4433
);
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_hash
&& first_header.parent_hash == bootstrap.checkpoint_parent_hashView on GitHub (pinned to 18893faf8b)
Solutions
- Compare the first verified header (number, hash, parent_hash, timestamp, base_fee) with the durable tip via `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` to identify the exact mismatching field.
- If the manifest changed, re-run the full bootstrap with the manifest digest that is stored in the ledger, or wipe and re-initialize the ledger for this (chain_id, wallet_address) from a trusted checkpoint.
- If there is a height gap, fetch verified finalized headers starting exactly at stored_tip.number + 1 (contiguous extension) instead of skipping ahead.
- If a reorg occurred, rebuild the ledger from the last common ancestor / trusted checkpoint rather than appending the divergent branch.
- Ensure only one writer process owns the ledger for a given chain_id and wallet_address.
Example fix
// before: bootstrap starts at an arbitrary verified height let headers = fetch_verified_finalized(from: latest_snapshot_number); persist_finalized_headers(headers); // after: start exactly at the durable tip so the extension is contiguous let tip = query_durable_tip(chain_id, wallet_address).await?; let headers = fetch_verified_finalized(from: tip.number); // headers[0] == tip assert_eq!(headers[0].hash, tip.hash, "batch must extend the durable tip"); persist_finalized_headers(headers).await?;
Defensive patterns
Strategy: validation
Validate before calling
let tip: Option<(i64, String, String, i64, Option<String>, String)> = sqlx::query_as(
"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(wallet_address).fetch_optional(&pool).await?;
if let Some(tip) = tip {
assert_eq!(tip.0, first.number as i64, "batch must start at durable tip height");
assert_eq!(tip.1, first.hash, "batch must start at durable tip hash");
} Type guard
fn extends_durable_tip(tip: &TipRow, first: &VerifiedHeader, manifest: &str) -> bool {
tip.number == first.number as i64
&& tip.hash == first.hash
&& tip.parent_hash == first.parent_hash
&& tip.timestamp == first.timestamp as i64
&& tip.base_fee_per_gas == first.base_fee_per_gas.as_ref().map(|v| v.to_string())
&& tip.manifest_digest == manifest
} Try / catch
match bootstrap_verified_finalized(&pool, &bootstrap).await {
Err(e) if e.to_string().contains("does not start at the durable tip") => {
// inspect stored tip, realign batch start or rebuild ledger
}
Err(e) => return Err(e),
Ok(()) => {}
} Prevention
- Always fetch the durable tip before selecting the verified batch start height.
- Keep one writer per (chain_id, wallet_address) ledger.
- Pin and record the manifest digest; never mix batches across manifests.
- Monitor for reorgs before resuming an extension.
When it happens
Trigger: Calling the bootstrap/persist routine for verified finalized headers when the ledger already has rows (`initialized == true`) and the first header in `bootstrap.finalized_headers` does not exactly match the stored tip: wrong starting height (gap), different hash at same height (fork/reorg not represented in ledger), differing parent_hash/timestamp/base_fee_per_gas, or a mismatched `bootstrap.manifest_digest` (e.g. bootstrapping from a different manifest than the one recorded).
Common situations: Pointing the adapter at a database previously populated by a different deployment/manifest; resuming from a snapshot/pruned node whose verified-finalized range starts later than the stored tip; a chain reorg after the tip was durably recorded; switching RPC providers or consensus checkpoints so the verified header set starts elsewhere; running two writers against the same (chain_id, wallet_address) ledger.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Finalized header ledger conflicts at height {}
- Verified finalized transaction count advanced without an act
- Verified finalized headers are not one continuous parent-lin
- Verified finalized transaction count is outside the owned re
- Verified finalized headers do not start at the trusted check
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/f21423fd0b760e98.
Report an issue: GitHub.