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 = swapView on GitHub (pinned to 2114cf6f76)
Solutions
- Pin the execution client to a single, reputable, fully synced endpoint that supports the finalized tag instead of a rotating pool.
- Immediately re-query the block hash from a second independent provider to decide which fork view is canonical.
- Wait for the next finalized checkpoint and let reconciliation retry the record; do not emit fills from conflicting data.
- 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
- Pin execution to one finality-aware, reputable endpoint instead of a rotating load balancer.
- Continuously cross-check finalized block hashes between two providers and page on disagreement.
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
- Persisted terminal transaction {tx_hash} is not stable at th
- Finalized block {} changed from {} to {} before intent valid
- Finalized execution transaction {tx_hash} no longer has a re
- Canonical head changed during signer-nonce replacement scan
- Finalized block {} does not contain transaction {}
AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21).
Data as JSON: /api/errors/5c629ef6cfe81a6f.
Report an issue: GitHub.