diff --git a/crates/firehose-flashblocks/src/processor.rs b/crates/firehose-flashblocks/src/processor.rs index 4b7e0a9715..5eb059bcde 100644 --- a/crates/firehose-flashblocks/src/processor.rs +++ b/crates/firehose-flashblocks/src/processor.rs @@ -788,6 +788,13 @@ where } } state.start_block(flashblock); + // Do not carry the EVM `State` across block boundaries. The next block + // re-bootstraps a fresh `State` from the canonical parent on its first + // execution (or buffers pending → replays if the parent isn't committed + // yet). Reusing the carried `State` let its read cache drift from the + // validated bundle and corrupt subsequent blocks — the divergence was + // confined entirely to carried, incrementally-executed blocks. + state.accumulated_db = None; if awaiting { // The previous block was replayed and has not yet been confirmed // by a canonical-block notification. Defer this transition: the @@ -934,6 +941,9 @@ where let parent_block = block_number.saturating_sub(1); + // The carried `State` is dropped at every block boundary (see the `FirstOfNextBlock` + // transition), so this bootstraps a fresh `State` from the canonical parent on every + // block's first execution. if accumulated_db.is_none() { match self.try_bootstrap_provider(parent_block) { Some(provider) => { @@ -967,10 +977,7 @@ where // canonical-chain commit: the in-memory state may be queryable while // `header_by_number` (which reads the canonical chain) still returns None. // Treat that the same as a bootstrap failure — buffer the sequence so the - // canonical-block notification replays it once the header lands. Otherwise the - // error would bubble out of `process_inner`, the outer `process` would reset - // state, and subsequent flashblocks for the same block would be dropped as - // "no in-flight sequence" — losing the whole block on the flashblock stream. + // canonical-block notification replays it once the header lands. match self.client.header_by_number(parent_block) { Ok(Some(_)) => {} Ok(None) | Err(_) => { @@ -1696,6 +1703,12 @@ where } } + // The returned `BlockExecutionResult` is intentionally dropped. A fresh + // `BaseBlockExecutor` is created per `execute_flashblock` call, so its receipts' + // `cumulative_gas_used` restarts at 0 each pass (per-flashblock, not block-cumulative). + // That is harmless here: the FIRE BLOCK is built from the tracer's own per-tx accounting + // (not these receipts), the state root is derived from the bundle, and fee-vault credits + // use per-tx gas — nothing consumes this value. executor.finish().map_err(|e| Error::Execution(Box::new(e)))?; // Promote the per-tx cache transitions accumulated by the EVM into diff --git a/crates/firehose-flashblocks/tests/flashblock_sequence.rs b/crates/firehose-flashblocks/tests/flashblock_sequence.rs index bde37d91c4..83461bcd01 100644 --- a/crates/firehose-flashblocks/tests/flashblock_sequence.rs +++ b/crates/firehose-flashblocks/tests/flashblock_sequence.rs @@ -388,6 +388,9 @@ fn two_blocks_with_deltas() { let genesis = test_genesis(); let genesis_hash = BaseChainSpec::from_genesis(genesis.clone()).inner.genesis_hash(); let client = GenesisClient::new(genesis); + // The processor re-bootstraps each block's state from its (committed) parent rather than + // carrying the in-flight `State` forward, so block 2's parent (block 1) must be available. + client.mark_canonical_block_available(1); let ts = 0x67d00000u64; // Block 2's `parent_hash` must equal block 1's locally-recomputed hash so the @@ -821,6 +824,8 @@ fn is_final_emitted_on_next_base_match() { let genesis = test_genesis(); let genesis_hash = BaseChainSpec::from_genesis(genesis.clone()).inner.genesis_hash(); let client = GenesisClient::new(genesis); + // Re-bootstrap per block: block 2's parent (block 1) must be available. + client.mark_canonical_block_available(1); let ts = 0x67d00000u64; // First, build a placeholder block-1 sequence to extract the recomputed hash via @@ -932,6 +937,8 @@ fn squash_does_not_apply_across_block_boundaries() { let genesis = test_genesis(); let genesis_hash = BaseChainSpec::from_genesis(genesis.clone()).inner.genesis_hash(); let client = GenesisClient::new(genesis); + // Re-bootstrap per block: block 2's parent (block 1) must be available. + client.mark_canonical_block_available(1); let ts = 0x67d00000u64; let placeholder = @@ -1310,6 +1317,8 @@ fn next_base_accepted_when_delta_diff_block_hash_diverges_from_recompute() { let genesis = test_genesis(); let genesis_hash = BaseChainSpec::from_genesis(genesis.clone()).inner.genesis_hash(); let client = GenesisClient::new(genesis); + // Re-bootstrap per block: block 2's parent (block 1) must be available. + client.mark_canonical_block_available(1); let ts = 0x67d00000u64; let placeholder = vec![ @@ -1481,6 +1490,8 @@ fn next_base_accepted_without_peek_when_recompute_matches() { let genesis = test_genesis(); let genesis_hash = BaseChainSpec::from_genesis(genesis.clone()).inner.genesis_hash(); let client = GenesisClient::new(genesis); + // Re-bootstrap per block: block 2's parent (block 1) must be available. + client.mark_canonical_block_available(1); let ts = 0x67d00000u64; let placeholder =