nautechsystems/nautilus_trader · error · anyhow::Error
Verified finalized header tip does not match the ledger
Error message
Verified finalized header tip does not match the ledger
What it means
Thrown by `record_execution_finality_verified` when the first header of finality.finalized_headers does not exactly equal the durable tip stored in execution_verified_finalized_header (number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest are all compared as a tuple). This guard ensures each new finality extension continues exactly from the previously persisted chain tip — no forks, reorgs, or gaps are accepted. The adjacent message about the ledger tip ("Verified finality extension does not start at the durable tip") is the paired ensure at line 6985.
Solutions
- Align the finalized_headers batch to begin exactly at the stored tip: headers[0] must be the persisted tip header itself, with subsequent headers extending it contiguously
- Check for a reorg; if the stored tip is stale or invalid, rebuild the verified finalized header chain from the canonical chain before recording
- Ensure the manifest digest is unchanged (a digest mismatch also fails this tuple comparison); re-verify under the stored manifest
- Verify field encodings match (base_fee_per_gas string formatting, timestamp units) so the tuple equality holds for identical headers
Example fix
// before: batch skips past the durable tip let headers = fetch_finalized_headers(start = tip.number + 2); cache.record_execution_finality_verified(&finality_with(headers)).await?; // after: include the tip header as the first element of the extension let headers = fetch_finalized_headers(start = tip.number); assert_eq!(headers[0].hash, tip.hash); cache.record_execution_finality_verified(&finality_with(headers)).await?;
Defensive patterns
Strategy: validation
Validate before calling
let (tip_number, tip_hash): (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_address).fetch_one(&pool).await?;
let first = &finality.finalized_headers[0];
anyhow::ensure!(
first.hash == tip_hash && first.number == tip_number as u64,
"Finality batch must extend the durable tip exactly"
); Try / catch
match cache.record_execution_finality_verified(&finality).await {
Err(e) if e.to_string().contains("does not start at the durable tip") => {
// detect reorg: compare parent hashes and rebuild the header chain if needed
}
result => result?,
} Prevention
- Build each finality batch to start at the persisted tip height and hash, chaining headers contiguously
- Monitor for chain reorgs and rebuild verified header state after any detected reorg depth
- Keep header field encoding (base fee formatting, timestamp units) consistent between recorder and persister
When it happens
Trigger: Calling record_execution_finality_verified when finality.finalized_headers[0] differs from the stored tip in any field: the batch starts at a later/earlier height than the tip, continues from a different parent (fork/reorg), or any header field (hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest) diverges from what was persisted.
Common situations: A chain reorg replaced the header at the tip height so hashes no longer match; recording finality batches with overlaps or gaps (tip is height N but the new batch starts at N+2 or N); an incompatible manifest digest after a manifest change; a clock/base-fee normalization difference making stored and incoming field encodings differ.
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 nonce does not match canonical nonce
- Verified finality manifest identity changed
- Execution schema version
- Verified finalized transaction count advanced without an…
- Account not found after cache update
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/05e0d27e69e9f287.
Report an issue: GitHub.
Appendix: source
Thrown at crates/adapters/blockchain/src/cache/database.rs:6973
"Finalized nonce {} does not match canonical nonce {stored_nonce}",
finality.nonce
);
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(finality.wallet_address)
.fetch_one(&mut *transaction)
.await
.map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
let first_header = &finality.finalized_headers[0];
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()),
finality.manifest_digest.to_string(),
),
"Verified finality extension does not start at the durable tip"
);
for header in finality.finalized_headers.iter().skip(1) {
let number = i64::try_from(header.number)
.context("Verified finalized height exceeds PostgreSQL BIGINT")?;
let timestamp = i64::try_from(header.timestamp)View on GitHub (pinned to 18893faf8b)