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_hash

View on GitHub (pinned to 18893faf8b)

Solutions

  1. 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.
  2. 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.
  3. If there is a height gap, fetch verified finalized headers starting exactly at stored_tip.number + 1 (contiguous extension) instead of skipping ahead.
  4. If a reorg occurred, rebuild the ledger from the last common ancestor / trusted checkpoint rather than appending the divergent branch.
  5. 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

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


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/f21423fd0b760e98. Report an issue: GitHub.