Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
be4e8cd
refactor(chain-state): derive lazy overlay anchor from blocks
mediocregopher Apr 20, 2026
cacb69a
refactor(chain-state): address lazy overlay review feedback
mediocregopher Apr 20, 2026
ebfaa6f
refactor(chain-state): cache lazy overlays by anchor
mediocregopher Apr 20, 2026
5041d55
refactor(provider): resolve lazy overlay anchors at use time
mediocregopher Apr 20, 2026
812e479
refactor(provider): separate overlay anchors from revert state
mediocregopher Apr 20, 2026
5036eb5
refactor(provider): infer overlay anchors from sources
mediocregopher Apr 20, 2026
b5ad001
refactor(provider): thread explicit requested anchors
mediocregopher Apr 20, 2026
d92ad5a
fix(provider): anchor overlay state providers by hash
mediocregopher Apr 21, 2026
134a7f3
fix(provider): pass overlay anchors via constructor
mediocregopher Apr 21, 2026
7db14d0
fix(engine): anchor state-root test overlay factory
mediocregopher Apr 21, 2026
45db5e0
fix(trie): initialize test overlay anchors
mediocregopher Apr 21, 2026
ffb0587
fix(provider): satisfy overlay lint checks
mediocregopher Apr 21, 2026
87b5240
Merge branch 'main' into mediocregopher/lazyoverlay-refactor
mediocregopher Apr 22, 2026
d5169ed
fix(engine): update sparse trie overlay factory test
mediocregopher Apr 22, 2026
c4d0949
style(engine): format sparse trie overlay test
mediocregopher Apr 22, 2026
6e8dbe3
Merge branch 'main' into mediocregopher/lazyoverlay-refactor
mediocregopher Apr 23, 2026
b60758e
fix(trie): remove unused parallel test dependency
mediocregopher Apr 24, 2026
31d0c78
fix(ci): clean bench checkouts and lock cargo builds
mediocregopher Apr 24, 2026
b6eec2e
refactor(provider): require overlay builder anchor hash
mediocregopher Apr 24, 2026
dd7c9a8
test(trie): initialize canonical genesis in overlay root test
mediocregopher Apr 27, 2026
2065ca1
Merge remote-tracking branch 'origin/main' into mediocregopher/lazyov…
mediocregopher Apr 27, 2026
eaefe88
fix(provider): make historical state use provider primitives
mediocregopher Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 64 additions & 44 deletions crates/chain-state/src/lazy_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,75 @@
//! lazily on first access. This allows execution to start before the trie overlay
//! is fully computed.

use crate::DeferredTrieData;
use crate::{EthPrimitives, ExecutedBlock};
use alloy_primitives::B256;
use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives};
use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted, TrieInputSorted};
use std::sync::{Arc, OnceLock};
use tracing::{debug, trace};

/// Inputs captured for lazy overlay computation.
#[derive(Clone)]
struct LazyOverlayInputs {
/// The persisted ancestor hash (anchor) this overlay should be built on.
anchor_hash: B256,
/// Deferred trie data handles for all in-memory blocks (newest to oldest).
blocks: Vec<DeferredTrieData>,
struct LazyOverlayInputs<N: NodePrimitives = EthPrimitives> {
/// In-memory blocks from tip to anchor child.
///
/// Blocks must be provided in reverse chain order (newest to oldest). The overlay anchor is
Comment thread
mediocregopher marked this conversation as resolved.
Outdated
/// derived from the last block's parent hash.
blocks: Vec<ExecutedBlock<N>>,
}

/// Lazily computed trie overlay.
///
/// Captures the inputs needed to compute a [`TrieInputSorted`] and defers the actual
/// computation until first access. This is conceptually similar to [`DeferredTrieData`]
/// but for overlay computation.
/// computation until first access.
///
/// Blocks must be provided in reverse chain order (newest to oldest), so the first block is the
/// chain tip and the last block is the child of the persisted anchor. The anchor hash for the
Comment thread
mediocregopher marked this conversation as resolved.
Outdated
/// overlay is derived from `blocks.last().parent_hash()`.
///
/// # Fast Path vs Slow Path
///
/// - **Fast path**: If the tip block's cached `anchored_trie_input` is ready and its `anchor_hash`
/// matches our expected anchor, we can reuse it directly (O(1)).
/// - **Slow path**: Otherwise, we merge all ancestor blocks' trie data into a new overlay.
#[derive(Clone)]
pub struct LazyOverlay {
pub struct LazyOverlay<N: NodePrimitives = EthPrimitives> {
/// Computed result, cached after first access.
inner: Arc<OnceLock<TrieInputSorted>>,
/// Inputs for lazy computation.
inputs: LazyOverlayInputs,
inputs: LazyOverlayInputs<N>,
}

impl std::fmt::Debug for LazyOverlay {
impl<N: NodePrimitives> std::fmt::Debug for LazyOverlay<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LazyOverlay")
.field("anchor_hash", &self.inputs.anchor_hash)
.field("anchor_hash", &self.anchor_hash())
.field("num_blocks", &self.inputs.blocks.len())
.field("computed", &self.inner.get().is_some())
.finish()
}
}

impl LazyOverlay {
/// Create a new lazy overlay with the given anchor hash and block handles.
impl<N: NodePrimitives> LazyOverlay<N> {
/// Create a new lazy overlay from in-memory blocks.
///
/// # Arguments
///
/// * `anchor_hash` - The persisted ancestor hash this overlay is built on top of
/// * `blocks` - Deferred trie data handles for in-memory blocks (newest to oldest)
pub fn new(anchor_hash: B256, blocks: Vec<DeferredTrieData>) -> Self {
Self { inner: Arc::new(OnceLock::new()), inputs: LazyOverlayInputs { anchor_hash, blocks } }
/// * `blocks` - Executed blocks in reverse chain order (newest to oldest)
pub fn new(blocks: Vec<ExecutedBlock<N>>) -> Self {
debug_assert!(
blocks.windows(2).all(|window| {
window[0].recovered_block().parent_hash() == window[1].recovered_block().hash()
}),
"LazyOverlay blocks must be ordered newest to oldest along a single chain"
);

Self { inner: Arc::new(OnceLock::new()), inputs: LazyOverlayInputs { blocks } }
}

/// Returns the anchor hash this overlay is built on.
pub const fn anchor_hash(&self) -> B256 {
self.inputs.anchor_hash
pub fn anchor_hash(&self) -> Option<B256> {
Comment thread
mediocregopher marked this conversation as resolved.
self.inputs.blocks.last().map(|block| block.recovered_block().parent_hash())
}

/// Returns the number of in-memory blocks this overlay covers.
Expand Down Expand Up @@ -90,18 +101,17 @@ impl LazyOverlay {

/// Compute the trie input overlay.
fn compute(&self) -> TrieInputSorted {
let anchor_hash = self.inputs.anchor_hash;
let blocks = &self.inputs.blocks;

if blocks.is_empty() {
let Some(anchor_hash) = self.anchor_hash() else {
debug!(target: "chain_state::lazy_overlay", "No in-memory blocks, returning empty overlay");
return TrieInputSorted::default();
}
};

// Fast path: Check if tip block's overlay is ready and anchor matches.
// The tip block (first in list) has the cumulative overlay from all ancestors.
if let Some(tip) = blocks.first() {
let data = tip.wait_cloned();
let data = tip.trie_data();
if let Some(anchored) = &data.anchored_trie_input {
if anchored.anchor_hash == anchor_hash {
trace!(target: "chain_state::lazy_overlay", %anchor_hash, "Reusing tip block's cached overlay (fast path)");
Expand All @@ -124,15 +134,18 @@ impl LazyOverlay {
/// Merge all blocks' trie data into a single [`TrieInputSorted`].
///
/// Blocks are ordered newest to oldest.
fn merge_blocks(blocks: &[DeferredTrieData]) -> TrieInputSorted {
fn merge_blocks(blocks: &[ExecutedBlock<N>]) -> TrieInputSorted {
if blocks.is_empty() {
return TrieInputSorted::default();
}

let state =
HashedPostStateSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().hashed_state));
let nodes =
TrieUpdatesSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().trie_updates));
let trie_data: Vec<_> = blocks.iter().map(|block| block.trie_data()).collect();
Comment thread
mediocregopher marked this conversation as resolved.
Outdated
let state = HashedPostStateSorted::merge_batch(
trie_data.iter().map(|trie_data| Arc::clone(&trie_data.hashed_state)),
);
let nodes = TrieUpdatesSorted::merge_batch(
trie_data.iter().map(|trie_data| Arc::clone(&trie_data.trie_updates)),
);

TrieInputSorted { state, nodes, prefix_sets: Default::default() }
}
Expand All @@ -141,30 +154,20 @@ impl LazyOverlay {
#[cfg(test)]
mod tests {
use super::*;
use reth_trie::{updates::TrieUpdates, HashedPostState};

fn empty_deferred(anchor: B256) -> DeferredTrieData {
DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
anchor,
Vec::new(),
)
}
use crate::{test_utils::TestBlockBuilder, EthPrimitives};

#[test]
fn empty_blocks_returns_default() {
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
let overlay = LazyOverlay::<EthPrimitives>::new(vec![]);
let result = overlay.get();
assert!(result.state.is_empty());
assert!(result.nodes.is_empty());
}

#[test]
fn single_block_uses_data_directly() {
let anchor = B256::random();
let deferred = empty_deferred(anchor);
let overlay = LazyOverlay::new(anchor, vec![deferred]);
let block = TestBlockBuilder::eth().get_executed_block_with_number(1, B256::random());
let overlay = LazyOverlay::new(vec![block]);

assert!(!overlay.is_computed());
let _ = overlay.get();
Expand All @@ -173,7 +176,7 @@ mod tests {

#[test]
fn cached_after_first_access() {
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
let overlay = LazyOverlay::<EthPrimitives>::new(vec![]);

// First access computes
let _ = overlay.get();
Expand All @@ -183,4 +186,21 @@ mod tests {
let _ = overlay.get();
assert!(overlay.is_computed());
}

#[test]
fn anchor_hash_comes_from_oldest_parent() {
let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
let overlay = LazyOverlay::new(blocks.into_iter().rev().collect());

assert_eq!(overlay.anchor_hash(), Some(B256::ZERO));
}

#[test]
#[should_panic(
expected = "LazyOverlay blocks must be ordered newest to oldest along a single chain"
)]
fn misordered_blocks_panic() {
let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
let _ = LazyOverlay::new(blocks);
}
}
7 changes: 5 additions & 2 deletions crates/engine/tree/src/tree/payload_processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ mod tests {
use rand::Rng;
use reth_chainspec::ChainSpec;
use reth_db_common::init::init_genesis;
use reth_ethereum_primitives::TransactionSigned;
use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
use reth_evm::OnStateHook;
use reth_evm_ethereum::EthEvmConfig;
use reth_primitives_traits::{Account, Recovered, StorageEntry};
Expand Down Expand Up @@ -1236,7 +1236,10 @@ mod tests {
std::convert::identity,
),
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
OverlayStateProviderFactory::new(provider_factory, ChangesetCache::new()),
OverlayStateProviderFactory::<_, EthPrimitives>::new(
provider_factory,
ChangesetCache::new(),
),
&TreeConfig::default(),
None, // No BAL for test
);
Expand Down
17 changes: 7 additions & 10 deletions crates/engine/tree/src/tree/payload_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ where
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
fn compute_state_root_parallel(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
overlay_factory: OverlayStateProviderFactory<P, N>,
hashed_state: &LazyHashedPostState,
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
let hashed_state = hashed_state.get();
Expand All @@ -1107,7 +1107,7 @@ where
/// [`HashedPostState`] containing the changes of this block, to compute the state root and
/// trie updates for this block.
fn compute_state_root_serial(
overlay_factory: OverlayStateProviderFactory<P>,
overlay_factory: OverlayStateProviderFactory<P, N>,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<(B256, TrieUpdates)> {
let hashed_state = hashed_state.get();
Expand Down Expand Up @@ -1147,7 +1147,7 @@ where
fn await_state_root_with_timeout<Tx, Err, R: Send + Sync + 'static>(
&self,
handle: &mut PayloadHandle<Tx, Err, R>,
overlay_factory: OverlayStateProviderFactory<P>,
overlay_factory: OverlayStateProviderFactory<P, N>,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<Result<StateRootComputeOutcome, ParallelStateRootError>> {
let Some(timeout) = self.config.state_root_task_timeout() else {
Expand Down Expand Up @@ -1239,7 +1239,7 @@ where
/// updates.
fn compare_trie_updates_with_serial(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
overlay_factory: OverlayStateProviderFactory<P, N>,
hashed_state: &LazyHashedPostState,
task_trie_updates: TrieUpdates,
) -> bool {
Expand Down Expand Up @@ -1437,7 +1437,7 @@ where
env: ExecutionEnv<Evm>,
txs: T,
provider_builder: StateProviderBuilder<N, P>,
overlay_factory: OverlayStateProviderFactory<P>,
overlay_factory: OverlayStateProviderFactory<P, N>,
strategy: StateRootStrategy,
block_access_list: Option<Arc<BlockAccessList>>,
) -> Result<
Expand Down Expand Up @@ -1563,7 +1563,7 @@ where
fn get_parent_lazy_overlay(
parent_hash: B256,
state: &EngineApiTreeState<N>,
) -> (Option<LazyOverlay>, B256) {
) -> (Option<LazyOverlay<N>>, B256) {
// Get blocks leading to the parent to determine the anchor
let (anchor_hash, blocks) =
state.tree_state.blocks_by_hash(parent_hash).unwrap_or_else(|| (parent_hash, vec![]));
Expand Down Expand Up @@ -1591,10 +1591,7 @@ where
"Creating lazy overlay for in-memory blocks"
);

// Extract deferred trie data handles (non-blocking)
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();

(Some(LazyOverlay::new(anchor_hash, handles)), anchor_hash)
(Some(LazyOverlay::new(blocks)), anchor_hash)
}

/// Spawns a background task to compute and sort trie data for the executed block.
Expand Down
27 changes: 12 additions & 15 deletions crates/engine/tree/src/tree/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use alloy_primitives::{
map::{B256Map, B256Set},
BlockNumber, B256,
};
use reth_chain_state::{DeferredTrieData, EthPrimitives, ExecutedBlock, LazyOverlay};
use reth_chain_state::{EthPrimitives, ExecutedBlock, LazyOverlay};
use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives, SealedHeader};
use std::{
collections::{btree_map, hash_map, BTreeMap, VecDeque},
Expand Down Expand Up @@ -43,7 +43,7 @@ pub struct TreeState<N: NodePrimitives = EthPrimitives> {
/// This is optimistically prepared after the canonical head changes, so that
/// the next payload building on the canonical head can use it immediately
/// without recomputing.
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay>,
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay<N>>,
}

impl<N: NodePrimitives> TreeState<N> {
Expand Down Expand Up @@ -109,7 +109,7 @@ impl<N: NodePrimitives> TreeState<N> {
/// Returns a clone of the [`LazyOverlay`] so the caller can spawn a background
/// task to trigger computation via [`LazyOverlay::get`]. This ensures the overlay
/// is actually computed before the next payload arrives.
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<LazyOverlay> {
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<LazyOverlay<N>> {
let canonical_hash = self.current_canonical_head.hash;

// Get blocks leading to the canonical head
Expand All @@ -119,10 +119,7 @@ impl<N: NodePrimitives> TreeState<N> {
return None;
};

// Extract deferred trie data handles from blocks (newest to oldest)
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();

let overlay = LazyOverlay::new(anchor_hash, handles);
let overlay = LazyOverlay::new(blocks.clone());
Comment thread
mediocregopher marked this conversation as resolved.
Outdated
self.cached_canonical_overlay = Some(PreparedCanonicalOverlay {
parent_hash: canonical_hash,
overlay: overlay.clone(),
Expand All @@ -148,7 +145,7 @@ impl<N: NodePrimitives> TreeState<N> {
&self,
parent_hash: B256,
expected_anchor: B256,
) -> Option<&PreparedCanonicalOverlay> {
) -> Option<&PreparedCanonicalOverlay<N>> {
self.cached_canonical_overlay.as_ref().filter(|cached| {
cached.parent_hash == parent_hash && cached.anchor_hash == expected_anchor
})
Expand Down Expand Up @@ -429,27 +426,27 @@ impl<N: NodePrimitives> TreeState<N> {
/// the next payload (which typically builds on the canonical head) to reuse
/// the pre-computed overlay immediately without re-traversing in-memory blocks.
///
/// The overlay captures deferred trie data handles from all in-memory blocks
/// The overlay captures executed blocks from all in-memory blocks
/// between the canonical head and the persisted anchor. When a new payload
/// arrives building on the canonical head, this cached overlay can be used
/// directly instead of calling `blocks_by_hash` and collecting handles again.
/// directly instead of calling `blocks_by_hash` again.
///
/// # Invalidation
///
/// The cached overlay is invalidated when:
/// - Persistence completes (anchor changes)
/// - The canonical head changes to a different block
#[derive(Debug, Clone)]
pub struct PreparedCanonicalOverlay {
pub struct PreparedCanonicalOverlay<N: NodePrimitives = EthPrimitives> {
/// The block hash for which this overlay is prepared as a parent.
///
/// When a payload arrives with this parent hash, the overlay can be reused.
pub parent_hash: B256,
/// The pre-computed lazy overlay containing deferred trie data handles.
/// The pre-computed lazy overlay containing executed blocks for the canonical segment.
///
/// This is computed optimistically after `set_canonical_head` so subsequent
/// payloads don't need to re-collect the handles.
pub overlay: LazyOverlay,
/// This is computed optimistically after `set_canonical_head` so subsequent payloads don't
/// need to walk the in-memory chain again.
pub overlay: LazyOverlay<N>,
/// The anchor hash (persisted ancestor) this overlay is based on.
///
/// Used to verify the overlay is still valid (anchor hasn't changed due to persistence).
Expand Down
Loading
Loading