nautechsystems/nautilus_trader · critical · anyhow::Error

Finalized block {} changed from {} to {} before fill emissio

Error message

Finalized block {} changed from {} to {} before fill emission

What it means

Post-finality reorg detector in emit_finalized_swap_fill: the client re-fetches the block by included.block_number and requires its hash to equal receipt.block_hash. Under proof-of-stake finality a finalized block hash must never change, so a mismatch means the RPC endpoint served data from a different fork (or the transaction was finalized against a non-canonical block).

Source

Thrown at crates/adapters/blockchain/src/execution/client.rs:2259

            )
        })?;
    let event = dex.parse_swap_event_rpc(log)?;
    let base_amount = if plan.pool.get_base_token().address == plan.pool.token0.address {
        event.amount0
    } else {
        event.amount1
    };
    anyhow::ensure!(
        base_amount.is_positive() && base_amount.unsigned_abs() == plan.amount_in,
        "Finalized Swap input {base_amount} does not match the persisted amount {}",
        plan.amount_in
    );

    let block = executor
        .http_rpc_client
        .block_by_number(included.block_number, false)
        .await?;
    anyhow::ensure!(
        block.hash == included.receipt.block_hash,
        "Finalized block {} changed from {} to {} before fill emission",
        included.block_number,
        included.receipt.block_hash,
        block.hash
    );
    let timestamp_ns = block
        .timestamp
        .checked_mul(NANOSECONDS_IN_SECOND)
        .ok_or_else(|| anyhow::anyhow!("Finalized block timestamp overflows nanoseconds"))?;
    let mut swap = event.to_pool_swap(
        plan.pool.chain.clone(),
        plan.instrument_id,
        plan.pool.pool_identifier,
        UnixNanos::from(timestamp_ns),
    );
    swap.calculate_trade_info(&plan.pool.token0, &plan.pool.token1, None)?;
    let trade = swap

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Pin the execution client to a single, reputable, fully synced endpoint that supports the finalized tag instead of a rotating pool.
  2. Immediately re-query the block hash from a second independent provider to decide which fork view is canonical.
  3. Wait for the next finalized checkpoint and let reconciliation retry the record; do not emit fills from conflicting data.
  4. If one provider persistently disagrees, drop it and investigate its health/sync status.
Defensive patterns

Strategy: retry

Validate before calling

let b1 = rpc1.block_by_number(n, false).await?;
let b2 = rpc2.block_by_number(n, false).await?;
if b1.hash != b2.hash {
    log::error!("providers disagree on block {n}: {} vs {}", b1.hash, b2.hash);
}

Try / catch

On this error, pause fill emission, re-verify the hash against a second independent provider, and retry at the next finalized checkpoint; never emit a fill while views disagree.

Prevention

When it happens

Trigger: A load-balanced RPC provider routing consecutive calls to nodes on different forks or with different sync states; a node still on a pre-finality view; an actual consensus-rule violation (extremely rare); block data misparsed so block_number maps to the wrong height.

Common situations: Shared/public RPC endpoints behind a balancer; pointing execution at a node that lags finality; mixing an execution endpoint from one provider with state from another.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21). Data as JSON: /api/errors/5c629ef6cfe81a6f. Report an issue: GitHub.