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_hash

View on GitHub (pinned to 18893faf8b)

Solutions

  1. 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.
  2. Check for concurrent writers/cleaners touching `execution_verified_finalized_header`; serialize access or use advisory locking between adapter instances.
  3. Run schema migrations if the error indicates missing table/columns.
  4. 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

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


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