Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c11348f
fix(provider): require changeset cache for historical state providers
mediocregopher Apr 21, 2026
31c0f96
test(node): tighten ethereum e2e helper provider bounds
mediocregopher Apr 21, 2026
58d2a3e
refactor(engine): simplify state provider builder cache wiring
mediocregopher Apr 21, 2026
5bd1a99
fix(provider): restore minimal historical cache plumbing
mediocregopher Apr 21, 2026
f652994
refactor(provider): extract overlay builder
mediocregopher Apr 21, 2026
52eec46
refactor(provider): keep overlay caching in factory
mediocregopher Apr 21, 2026
73f8609
refactor(provider): add historical overlay helper
mediocregopher Apr 21, 2026
2fde20f
refactor(provider): preserve historical overlay prefix sets
mediocregopher Apr 21, 2026
56112da
refactor(provider): warn on old historical overlay reverts
mediocregopher Apr 21, 2026
93b0568
refactor(provider): use build_overlay in historical trie paths
mediocregopher Apr 21, 2026
79fc5c5
fix(engine): remove redundant overlay-builder clones
mediocregopher Apr 21, 2026
5561eb0
fix(provider): avoid db-tip lookup for unanchored overlays
mediocregopher Apr 21, 2026
5357dc9
fix(engine): use state provider for serial state root fallback
mediocregopher Apr 22, 2026
93cb59e
refactor(engine): inline serial provider builder reuse
mediocregopher Apr 22, 2026
4b9af81
refactor(provider): restore overlay comments
mediocregopher Apr 22, 2026
7fd39e6
refactor(provider): rename historical changeset cache field
mediocregopher Apr 22, 2026
ef63a7a
fix(provider): avoid extra clones in historical proofs
mediocregopher Apr 22, 2026
ecf87ff
Merge remote-tracking branch 'origin/main' into mediocregopher/histor…
mediocregopher Apr 22, 2026
2bb7e7f
fix(engine): address payload validator CI failures
mediocregopher Apr 22, 2026
17d58a7
fix(provider): wait for readers before unwind static-file commit
mediocregopher Apr 23, 2026
63f3b1d
test(stages): drop reader before execution unwind
mediocregopher Apr 23, 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
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 @@ -974,7 +974,7 @@ mod tests {
use reth_evm_ethereum::EthEvmConfig;
use reth_primitives_traits::{Account, Recovered, StorageEntry};
use reth_provider::{
providers::{BlockchainProvider, OverlayStateProviderFactory},
providers::{BlockchainProvider, OverlayBuilder, OverlayStateProviderFactory},
test_utils::create_test_provider_factory_with_chain_spec,
ChainSpecProvider, HashingWriter,
};
Expand Down Expand Up @@ -1249,7 +1249,10 @@ mod tests {
std::convert::identity,
),
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
OverlayStateProviderFactory::new(provider_factory, ChangesetCache::new()),
OverlayStateProviderFactory::new(
provider_factory,
OverlayBuilder::new(ChangesetCache::new()),
),
&TreeConfig::default(),
None, // No BAL for test
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,8 @@ mod tests {
use super::*;
use alloy_primitives::{keccak256, Address, B256, U256};
use reth_provider::{
providers::OverlayStateProviderFactory, test_utils::create_test_provider_factory,
providers::{OverlayBuilder, OverlayStateProviderFactory},
test_utils::create_test_provider_factory,
};
use reth_trie_db::ChangesetCache;
use reth_trie_parallel::proof_task::ProofTaskCtx;
Expand Down Expand Up @@ -983,8 +984,10 @@ mod tests {
fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
let runtime = reth_tasks::Runtime::test();
let provider_factory = create_test_provider_factory();
let overlay_factory =
OverlayStateProviderFactory::new(provider_factory, ChangesetCache::new());
let overlay_factory = OverlayStateProviderFactory::new(
provider_factory,
OverlayBuilder::new(ChangesetCache::new()),
);
let proof_worker_handle =
ProofWorkerHandle::new(&runtime, ProofTaskCtx::new(overlay_factory), false);

Expand Down
94 changes: 53 additions & 41 deletions crates/engine/tree/src/tree/payload_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ use reth_primitives_traits::{
RecoveredBlock, SealedBlock, SealedHeader, SignerRecoverable,
};
use reth_provider::{
providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockNumReader, BlockReader,
ChangeSetReader, DatabaseProviderFactory, DatabaseProviderROFactory, HashedPostStateProvider,
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider,
StateProviderFactory, StateReader, StorageChangeSetReader, StorageSettingsCache,
providers::{OverlayBuilder, OverlayStateProviderFactory},
BlockExecutionOutput, BlockNumReader, BlockReader, ChangeSetReader, DatabaseProviderFactory,
DatabaseProviderROFactory, HashedPostStateProvider, ProviderError, PruneCheckpointReader,
StageCheckpointReader, StateProvider, StateProviderBox, StateProviderFactory, StateReader,
StorageChangeSetReader, StorageSettingsCache,
};
use reth_revm::db::{states::bundle_state::BundleRetention, BundleAccount, State};
use reth_trie::{trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState, StateRoot};
use reth_trie::{trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState};
use reth_trie_db::ChangesetCache;
use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError};
use revm_primitives::{Address, KECCAK_EMPTY};
Expand Down Expand Up @@ -523,16 +524,18 @@ where

// Create overlay factory for payload processor (StateRootTask path needs it for
// multiproofs)
let provider_factory = self.provider.clone();
let overlay_builder = OverlayBuilder::new(self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
.with_lazy_overlay(lazy_overlay);
let overlay_factory =
OverlayStateProviderFactory::new(self.provider.clone(), self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
.with_lazy_overlay(lazy_overlay);
OverlayStateProviderFactory::new(provider_factory.clone(), overlay_builder.clone());

// Spawn the appropriate processor based on strategy
let mut handle = ensure_ok!(self.spawn_payload_processor(
env.clone(),
txs,
provider_builder,
provider_builder.clone(),
overlay_factory.clone(),
strategy,
block_access_list,
Expand Down Expand Up @@ -664,7 +667,7 @@ where
let task_result = ensure_ok_post_block!(
self.await_state_root_with_timeout(
&mut handle,
overlay_factory.clone(),
provider_builder.clone(),
&hashed_state,
),
block
Expand All @@ -688,7 +691,9 @@ where
// Compare trie updates with serial computation if configured
if self.config.always_compare_trie_updates() {
let _has_diff = self.compare_trie_updates_with_serial(
overlay_factory.clone(),
provider_builder.clone(),
provider_factory,
overlay_builder,
&hashed_state,
trie_updates.as_ref().clone(),
);
Expand Down Expand Up @@ -727,7 +732,11 @@ where
}
StateRootStrategy::Parallel => {
debug!(target: "engine::tree::payload_validator", "Using parallel state root algorithm");
match self.compute_state_root_parallel(overlay_factory.clone(), &hashed_state) {
match self.compute_state_root_parallel(
provider_factory,
overlay_builder,
&hashed_state,
) {
Comment thread
klkvr marked this conversation as resolved.
Ok(result) => {
let elapsed = root_time.elapsed();
info!(
Expand Down Expand Up @@ -763,7 +772,9 @@ where
}

let (root, updates) = ensure_ok_post_block!(
Self::compute_state_root_serial(overlay_factory.clone(), &hashed_state),
provider_builder
.build()
.and_then(|provider| Self::compute_state_root_serial(provider, &hashed_state)),
block
);

Expand Down Expand Up @@ -1087,42 +1098,33 @@ where
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
fn compute_state_root_parallel(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
provider_factory: P,
overlay_builder: OverlayBuilder,
hashed_state: &LazyHashedPostState,
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
let hashed_state = hashed_state.get();
// The `hashed_state` argument will be taken into account as part of the overlay, but we
// need to use the prefix sets which were generated from it to indicate to the
// ParallelStateRoot which parts of the trie need to be recomputed.
let prefix_sets = hashed_state.construct_prefix_sets().freeze();
let overlay_factory =
overlay_factory.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted());
let overlay_factory = OverlayStateProviderFactory::new(
provider_factory,
overlay_builder.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted()),
);
ParallelStateRoot::new(overlay_factory, prefix_sets, self.runtime.clone())
.incremental_root_with_updates()
}

/// Compute state root for the given hashed post state in serial.
///
/// Uses an overlay factory which provides the state of the parent block, along with the
/// [`HashedPostState`] containing the changes of this block, to compute the state root and
/// trie updates for this block.
/// Uses the same provider construction path as main execution and computes the state root and
/// trie updates for this block directly via
/// [`reth_provider::StateRootProvider::state_root_with_updates`].
fn compute_state_root_serial(
overlay_factory: OverlayStateProviderFactory<P>,
state_provider: StateProviderBox,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<(B256, TrieUpdates)> {
let hashed_state = hashed_state.get();
// The `hashed_state` argument will be taken into account as part of the overlay, but we
// need to use the prefix sets which were generated from it to indicate to the
// StateRoot which parts of the trie need to be recomputed.
let prefix_sets = hashed_state.construct_prefix_sets().freeze();
let overlay_factory =
overlay_factory.with_extended_hashed_state_overlay(hashed_state.clone_into_sorted());

let provider = overlay_factory.database_provider_ro()?;

Ok(StateRoot::new(&provider, &provider)
.with_prefix_sets(prefix_sets)
.root_with_updates()?)
state_provider.state_root_with_updates(hashed_state.get().clone())
}

/// Awaits the state root from the background task, with an optional timeout fallback.
Expand All @@ -1147,7 +1149,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>,
state_provider_builder: StateProviderBuilder<N, P>,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<Result<StateRootComputeOutcome, ParallelStateRootError>> {
let Some(timeout) = self.config.state_root_task_timeout() else {
Expand All @@ -1172,10 +1174,11 @@ where
let (seq_tx, seq_rx) =
std::sync::mpsc::channel::<ProviderResult<(B256, TrieUpdates)>>();

let seq_overlay = overlay_factory;
let seq_hashed_state = hashed_state.clone();
self.payload_processor.executor().spawn_blocking_named("serial-root", move || {
let result = Self::compute_state_root_serial(seq_overlay, &seq_hashed_state);
let result = state_provider_builder.build().and_then(|provider| {
Self::compute_state_root_serial(provider, &seq_hashed_state)
});
let _ = seq_tx.send(result);
});

Expand Down Expand Up @@ -1239,13 +1242,18 @@ where
/// updates.
fn compare_trie_updates_with_serial(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
state_provider_builder: StateProviderBuilder<N, P>,
provider_factory: P,
overlay_builder: OverlayBuilder,
hashed_state: &LazyHashedPostState,
task_trie_updates: TrieUpdates,
) -> bool {
debug!(target: "engine::tree::payload_validator", "Comparing trie updates with serial computation");

match Self::compute_state_root_serial(overlay_factory.clone(), hashed_state) {
match state_provider_builder
.build()
.and_then(|provider| Self::compute_state_root_serial(provider, hashed_state))
{
Ok((serial_root, serial_trie_updates)) => {
debug!(
target: "engine::tree::payload_validator",
Expand All @@ -1254,6 +1262,8 @@ where
);

// Get a database provider to use as trie cursor factory
let overlay_factory =
OverlayStateProviderFactory::new(provider_factory, overlay_builder);
match overlay_factory.database_provider_ro() {
Ok(provider) => {
match super::trie_updates::compare_trie_updates(
Expand Down Expand Up @@ -2026,10 +2036,12 @@ where
state: &EngineApiTreeState<N>,
) -> Option<StateRootHandle> {
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, state);
let overlay_factory =
OverlayStateProviderFactory::new(self.provider.clone(), self.changeset_cache.clone())
let overlay_factory = OverlayStateProviderFactory::new(
self.provider.clone(),
OverlayBuilder::new(self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
.with_lazy_overlay(lazy_overlay);
.with_lazy_overlay(lazy_overlay),
);

Some(self.payload_processor.spawn_state_root(
overlay_factory,
Expand Down
8 changes: 5 additions & 3 deletions crates/node/builder/src/launch/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,12 @@ impl EngineNodeLauncher {
ctx.blockchain_db().clone(),
ctx.components().evm_config().clone(),
|| async {
// Create a separate cache for reorg validator (not shared with main engine)
let reorg_cache = ChangesetCache::new();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tangential change, but it's not necessary to have a separate ChangesetCache for debug reorgs, the cache is keyed by block hash anyway.

validator_builder
.build_tree_validator(&add_ons_ctx, engine_tree_config.clone(), reorg_cache)
.build_tree_validator(
&add_ons_ctx,
engine_tree_config.clone(),
changeset_cache.clone(),
)
.await
},
node_config.debug.reorg_frequency,
Expand Down
75 changes: 40 additions & 35 deletions crates/stages/stages/src/stages/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,41 +1020,46 @@ mod tests {
done: true
} if processed == total && total == block.gas_used);

let provider = factory.provider().unwrap();

// check post state
let account1 = address!("0x1000000000000000000000000000000000000000");
let account1_info =
Account { balance: U256::ZERO, nonce: 0x00, bytecode_hash: Some(code_hash) };
let account2 = address!("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba");
let account2_info = Account {
balance: U256::from(0x1bc16d674ece94bau128),
nonce: 0x00,
bytecode_hash: None,
};
let account3 = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
let account3_info = Account {
balance: U256::from(0x3635c9adc5de996b46u128),
nonce: 0x01,
bytecode_hash: None,
};

// assert accounts
assert!(
matches!(provider.basic_account(&account1), Ok(Some(acc)) if acc == account1_info)
);
assert!(
matches!(provider.basic_account(&account2), Ok(Some(acc)) if acc == account2_info)
);
assert!(
matches!(provider.basic_account(&account3), Ok(Some(acc)) if acc == account3_info)
);
// assert storage
// Get on dupsort would return only first value. This is good enough for this test.
assert!(matches!(
provider.tx_ref().get::<tables::PlainStorageState>(account1),
Ok(Some(entry)) if entry.key == B256::with_last_byte(1) && entry.value == U256::from(2)
));
{
let provider = factory.provider().unwrap();

// check post state
let account1 = address!("0x1000000000000000000000000000000000000000");
let account1_info =
Account { balance: U256::ZERO, nonce: 0x00, bytecode_hash: Some(code_hash) };
let account2 = address!("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba");
let account2_info = Account {
balance: U256::from(0x1bc16d674ece94bau128),
nonce: 0x00,
bytecode_hash: None,
};
let account3 = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
let account3_info = Account {
balance: U256::from(0x3635c9adc5de996b46u128),
nonce: 0x01,
bytecode_hash: None,
};

// assert accounts
assert!(matches!(
provider.basic_account(&account1),
Ok(Some(acc)) if acc == account1_info
));
assert!(matches!(
provider.basic_account(&account2),
Ok(Some(acc)) if acc == account2_info
));
assert!(matches!(
provider.basic_account(&account3),
Ok(Some(acc)) if acc == account3_info
));
// assert storage
// Get on dupsort would return only first value. This is good enough for this test.
assert!(matches!(
provider.tx_ref().get::<tables::PlainStorageState>(account1),
Ok(Some(entry)) if entry.key == B256::with_last_byte(1) && entry.value == U256::from(2)
));
}

let mut provider = factory.database_provider_rw().unwrap();
let mut stage = stage();
Expand Down
Loading
Loading