nautechsystems/nautilus_trader · critical · anyhow::Error
Verified inclusion header does not match the finalized recei
Error message
Verified inclusion header does not match the finalized receipt
What it means
To timestamp a finalized order event, the adapter cross-checks the verified inclusion block header against the finalized receipt: the header's block number must equal `included.block_number` and its hash must equal `receipt.block_hash`. This ensure! fires when they disagree — the header the finality layer attests to is not the block the receipt was mined in. It is an internal consistency guard against reorgs, mis-attributed receipts, or corrupted finality data.
Source
Thrown at crates/adapters/blockchain/src/execution/client.rs:4681
let rejected = OrderRejected::new(
emitter.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(),
emitter.account_id(),
format!("Transaction {} reverted on-chain", included.tx_hash).into(),
execution_event_id(included.tx_hash, b"reverted"),
ts_event,
ts_event,
false,
false,
);
emitter.try_send_order_event(OrderEventAny::Rejected(rejected))
}
fn finalized_inclusion_time(included: &IncludedTransaction) -> anyhow::Result<UnixNanos> {
let inclusion = &included.finality.inclusion_header;
anyhow::ensure!(
inclusion.number == included.block_number
&& inclusion.hash == included.receipt.block_hash.to_string(),
"Verified inclusion header does not match the finalized receipt"
);
let timestamp = inclusion.timestamp;
let nanos = timestamp
.checked_mul(NANOSECONDS_IN_SECOND)
.ok_or_else(|| anyhow::anyhow!("Verified inclusion timestamp exceeds nanoseconds"))?;
Ok(UnixNanos::from(nanos))
}
fn execution_event_id(tx_hash: B256, event: &[u8]) -> UUID4 {
let mut identity = Vec::with_capacity(tx_hash.len() + event.len());
identity.extend_from_slice(tx_hash.as_slice());
identity.extend_from_slice(event);
let digest = keccak256(identity);
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&digest[..16]);View on GitHub (pinned to 18893faf8b)
Solutions
- Re-fetch the receipt and canonical block header at `included.block_number` and rebuild the IncludedTransaction after the mismatch
- Check for a reorg: compare the header hash against the current canonical chain at that height
- Ensure both header and receipt come from the same provider/fork within the same session
- If hashes are string-compared, normalize formatting (lowercase hex) on both sides
Example fix
// before let hash_a = inclusion.hash; let hash_b = included.receipt.block_hash.to_string(); // after let hash_a = inclusion.hash.to_lowercase(); let hash_b = included.receipt.block_hash.to_string().to_lowercase();
Defensive patterns
Strategy: type-guard
Validate before calling
fn inclusion_consistent(included: &IncludedTransaction) -> bool {
let h = &included.finality.inclusion_header;
h.number == included.block_number
&& h.hash.eq_ignore_ascii_case(&included.receipt.block_hash.to_string())
} Type guard
fn has_matching_inclusion_header(included: &IncludedTransaction) -> bool {
let h = &included.finality.inclusion_header;
h.number == included.block_number && h.hash.eq_ignore_ascii_case(&included.receipt.block_hash.to_string())
} Try / catch
match finalized_inclusion_time(included) {
Ok(ts) => emit_rejected(included, ts),
Err(e) if e.to_string().contains("does not match the finalized receipt") => {
tracing::error!("header/receipt mismatch for {} — possible reorg; refetching", included.tx_hash);
// rebuild IncludedTransaction from canonical chain before proceeding
}
Err(e) => return Err(e),
} Prevention
- Rebuild IncludedTransaction from fresh canonical data after any detected reorg
- Never cache finality headers across sessions or fork transitions
- Normalize block hash casing (lowercase hex) wherever hashes are compared as strings
- Use a single provider session for header and receipt reads so they can't come from different forks
When it happens
Trigger: `finalized_inclusion_time` is called on an IncludedTransaction where `finality.inclusion_header.number != block_number` or `inclusion_header.hash != receipt.block_hash` — e.g. after a reorg the receipt points at a block different from the finality-verified header, or finality data was populated from a mismatched source.
Common situations: A chain reorg moved the transaction to a different block after finality data was captured, finality headers cached from an earlier verification run, mismatched block-hash string formatting (e.g. checksummed vs hex) between sources, or a provider returning headers/receipts from different forks.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Persisted terminal transaction {tx_hash} is not stable at th
- Decision header changed before signing
- Durable finalized header tip conflicts with independent sour
- Pool state block {} changed from {} to {}; refresh the profi
- Swap decision header conflicts with profiler ancestry
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/6b06d9951142dfb8.
Report an issue: GitHub.