nautechsystems/nautilus_trader · error · anyhow::Error

Finalized block {} changed from {} to {} before intent valid

Error message

Finalized block {} changed from {} to {} before intent validation

What it means

finalized_transaction_matches re-fetches the block at the receipt's block_number and compares its hash to the receipt's block_hash; a mismatch means a block previously reported as finalized now has a different hash. The validation aborts because the included-transaction evidence (receipt) no longer matches the canonical chain the node is serving.

Source

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

            executor.release_slot();
            Ok(())
        }
        Ok(InclusionOutcome::Pending(message)) => anyhow::bail!(message),
        Err(e) => Err(e),
    }
}

async fn finalized_transaction_matches(
    included: &IncludedTransaction,
    intent: &ExecutionIntentRow,
    nonce: u64,
    executor: &TransactionExecutor,
) -> anyhow::Result<bool> {
    let block = executor
        .http_rpc_client
        .block_by_number(included.block_number, true)
        .await?;
    anyhow::ensure!(
        block.hash == included.receipt.block_hash,
        "Finalized block {} changed from {} to {} before intent validation",
        included.block_number,
        included.receipt.block_hash,
        block.hash
    );
    let Some(transaction) = block
        .transactions
        .iter()
        .find(|transaction| transaction.hash == included.tx_hash)
    else {
        anyhow::bail!(
            "Finalized block {} does not contain transaction {}",
            included.block_number,
            included.tx_hash
        );
    };
    let (expected_to, expected_input, expected_value) = persisted_call_fields(intent)?;

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Retry reconciliation against a trusted, canonical RPC provider; a genuinely finalized block will not change again on the canonical chain
  2. Cross-check the block hash across multiple independent providers (two different companies) to identify the inconsistent endpoint
  3. If providers agree the block really did change, follow the network's incident guidance and reconcile the intent against the new canonical state
  4. Pin the client to a single high-quality endpoint rather than a rotating pool during reconciliation
Defensive patterns

Strategy: retry

Validate before calling

from web3 import Web3

# Cross-check the finalized block across providers before reconciling
h1 = Web3(Web3.HTTPProvider(rpc_a)).eth.get_block(block_number)['hash']
h2 = Web3(Web3.HTTPProvider(rpc_b)).eth.get_block(block_number)['hash']
assert h1 == h2 == receipt_block_hash, 'providers disagree on finalized block; pick a canonical endpoint'

Try / catch

for attempt in range(5):
    try:
        client.connect()
        break
    except Exception as e:
        if 'changed from' not in str(e) or 'before intent validation' not in str(e):
            raise
        time.sleep(20 * (attempt + 1))  # inconsistent/forked node view; retry or switch endpoint
else:
    raise RuntimeError('finalized-block view never stabilized')

Prevention

When it happens

Trigger: Intent validation during reconciliation while the RPC node's view of a finalized block changes: deep reorgs crossing the finalized boundary (rare but possible on some networks), or inconsistent load-balanced RPC backends disagreeing about the canonical chain.

Common situations: Post-incident reconciliation after a network-level reorg event; RPC load balancers routing the second request to a node on a different fork; misconfigured/attacking endpoints serving non-canonical blocks.

Related errors


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