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

  1. Re-fetch the receipt and canonical block header at `included.block_number` and rebuild the IncludedTransaction after the mismatch
  2. Check for a reorg: compare the header hash against the current canonical chain at that height
  3. Ensure both header and receipt come from the same provider/fork within the same session
  4. 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

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


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