From 22a68756c71a4a4e3408600c69ef5b6cf7169461 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 23 Jan 2026 12:26:57 +0100 Subject: [PATCH] fix(tree): evict changeset cache even when finalized block is unset (#21354) --- crates/engine/tree/src/tree/mod.rs | 40 ++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/crates/engine/tree/src/tree/mod.rs b/crates/engine/tree/src/tree/mod.rs index 1e04d19dba..1e4a631594 100644 --- a/crates/engine/tree/src/tree/mod.rs +++ b/crates/engine/tree/src/tree/mod.rs @@ -85,6 +85,12 @@ pub mod state; /// backfill this gap. pub(crate) const MIN_BLOCKS_FOR_PIPELINE_RUN: u64 = EPOCH_SLOTS; +/// The minimum number of blocks to retain in the changeset cache after eviction. +/// +/// This ensures that recent trie changesets are kept in memory for potential reorgs, +/// even when the finalized block is not set (e.g., on L2s like Optimism). +const CHANGESET_CACHE_RETENTION_BLOCKS: u64 = 64; + /// A builder for creating state providers that can be used across threads. #[derive(Clone, Debug)] pub struct StateProviderBuilder { @@ -1378,19 +1384,27 @@ where debug!(target: "engine::tree", ?last_persisted_block_hash, ?last_persisted_block_number, elapsed=?start_time.elapsed(), "Finished persisting, calling finish"); self.persistence_state.finish(last_persisted_block_hash, last_persisted_block_number); - // Evict trie changesets for blocks below the finalized block, but keep at least 64 blocks - if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() { - let min_threshold = last_persisted_block_number.saturating_sub(64); - let eviction_threshold = finalized.number.min(min_threshold); - debug!( - target: "engine::tree", - last_persisted = last_persisted_block_number, - finalized_number = finalized.number, - eviction_threshold, - "Evicting changesets below threshold" - ); - self.changeset_cache.evict(eviction_threshold); - } + // Evict trie changesets for blocks below the eviction threshold. + // Keep at least CHANGESET_CACHE_RETENTION_BLOCKS from the persisted tip, and also respect + // the finalized block if set. + let min_threshold = + last_persisted_block_number.saturating_sub(CHANGESET_CACHE_RETENTION_BLOCKS); + let eviction_threshold = + if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() { + // Use the minimum of finalized block and retention threshold to be conservative + finalized.number.min(min_threshold) + } else { + // When finalized is not set (e.g., on L2s), use the retention threshold + min_threshold + }; + debug!( + target: "engine::tree", + last_persisted = last_persisted_block_number, + finalized_number = ?self.canonical_in_memory_state.get_finalized_num_hash().map(|f| f.number), + eviction_threshold, + "Evicting changesets below threshold" + ); + self.changeset_cache.evict(eviction_threshold); self.on_new_persisted_block()?; Ok(())