Compare commits

...

7 Commits

Author SHA1 Message Date
yongkangc
51cd4d1523 fix: remove Send + Sync bounds from Provider associated type 2025-12-22 10:21:06 +00:00
yongkangc
a6a7b6021c Revert "bench(engine): add state_provider_builder micro-benchmark"
This reverts commit 994ed1bbd8.
2025-12-22 07:56:32 +00:00
yongkangc
994ed1bbd8 bench(engine): add state_provider_builder micro-benchmark 2025-12-22 07:55:49 +00:00
yongkangc
29fdbdb7b3 chore: remove redundant comment 2025-12-22 05:44:07 +00:00
yongkangc
3a82e17436 chore: resolve merge conflict in payload_validator.rs 2025-12-22 05:00:01 +00:00
yongkangc
c5c9cd96d0 perf(engine): deduplicate state_provider_builder calls with type-safe TreeCtx
Avoid redundant state_provider_builder lookups during block validation by
passing both the prebuilt StateProviderBox and the StateProviderBuilder
through a type-safe TreeCtx.

Key changes:
- Add generic P to TreeCtx for type-safe builder storage (no downcast needed)
- Add Provider associated type to EngineValidator trait
- StateProviderBuilder::build() now consumes self, avoiding Vec clone
- TreeCtx::with_precomputed() builder pattern for cleaner API
- StateProviderAndBuilder holds both provider and builder

This eliminates duplicate blocks_by_hash traversals during payload validation
while maintaining full type safety.

Closes #20435

Amp-Thread-ID: https://ampcode.com/threads/T-019b2af5-3809-7349-958c-134380d47dfa
2025-12-22 05:00:01 +00:00
yongkangc
f84f6f02d9 perf(engine): deduplicate state_provider_builder calls
Avoid redundant state_provider_builder lookups during block validation by
passing both the prebuilt StateProviderBox and the builder through TreeCtx.

Key changes:
- StateProviderBuilder::build() now consumes self, avoiding Vec clone
- TreeCtx stores PrecomputedState with both provider and type-erased builder
- EngineApiTreeHandler builds provider immediately and passes both to validator
- Validator uses prebuilt provider for execution, builder for parallel tasks

This eliminates duplicate blocks_by_hash traversals that were occurring
during payload validation.

Closes #20435
2025-12-22 05:00:01 +00:00
4 changed files with 115 additions and 39 deletions

View File

@@ -86,7 +86,7 @@ where
evm_config: C,
) -> Self
where
V: EngineValidator<N::Payload>,
V: EngineValidator<N::Payload, Provider = BlockchainProvider<N>>,
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
{
let engine_kind =

View File

@@ -116,10 +116,10 @@ impl<N: NodePrimitives, P> StateProviderBuilder<N, P>
where
P: BlockReader + StateProviderFactory + StateReader + Clone,
{
/// Creates a new state provider from this builder.
pub fn build(&self) -> ProviderResult<StateProviderBox> {
/// Consumes the builder and creates a new state provider.
pub fn build(self) -> ProviderResult<StateProviderBox> {
let mut provider = self.provider_factory.state_by_block_hash(self.historical)?;
if let Some(overlay) = self.overlay.clone() {
if let Some(overlay) = self.overlay {
provider = Box::new(MemoryOverlayStateProvider::new(provider, overlay))
}
Ok(provider)
@@ -316,12 +316,14 @@ where
+ HashedPostStateProvider
+ TrieReader
+ Clone
+ Send
+ Sync
+ 'static,
<P as DatabaseProviderFactory>::Provider:
BlockReader<Block = N::Block, Header = N::BlockHeader>,
C: ConfigureEvm<Primitives = N> + 'static,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
V: EngineValidator<T>,
V: EngineValidator<T, Provider = P>,
{
/// Creates a new [`EngineApiTreeHandler`].
#[expect(clippy::too_many_arguments)]
@@ -2475,7 +2477,7 @@ where
&mut self,
block_id: BlockWithParent,
input: Input,
execute: impl FnOnce(&mut V, Input, TreeCtx<'_, N>) -> Result<ExecutedBlock<N>, Err>,
execute: impl FnOnce(&mut V, Input, TreeCtx<'_, N, P>) -> Result<ExecutedBlock<N>, Err>,
convert_to_block: impl FnOnce(&mut Self, Input) -> Result<SealedBlock<N::Block>, Err>,
) -> Result<InsertPayloadOk, Err>
where
@@ -2499,8 +2501,7 @@ where
_ => {}
};
// Ensure that the parent state is available.
match self.state_provider_builder(block_id.parent) {
let provider_builder = match self.state_provider_builder(block_id.parent) {
Err(err) => {
let block = convert_to_block(self, input)?;
return Err(InsertBlockError::new(block, err.into()).into());
@@ -2524,8 +2525,18 @@ where
missing_ancestor,
}))
}
Ok(Some(_)) => {}
}
Ok(Some(builder)) => builder,
};
// Build the state provider. The builder is cloned because it's also needed for parallel
// tasks.
let state_provider = match provider_builder.clone().build() {
Ok(provider) => provider,
Err(err) => {
let block = convert_to_block(self, input)?;
return Err(InsertBlockError::new(block, err.into()).into());
}
};
// determine whether we are on a fork chain by comparing the block number with the
// canonical head. This is a simple check that is sufficient for the event emission below.
@@ -2533,7 +2544,12 @@ where
// as this indicates there's already a canonical block at that height.
let is_fork = block_id.block.number <= self.state.tree_state.current_canonical_head.number;
let ctx = TreeCtx::new(&mut self.state, &self.canonical_in_memory_state);
let ctx = TreeCtx::with_precomputed(
&mut self.state,
&self.canonical_in_memory_state,
state_provider,
provider_builder,
);
let start = Instant::now();

View File

@@ -36,7 +36,7 @@ use reth_primitives_traits::{
use reth_provider::{
providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockReader,
DatabaseProviderFactory, DatabaseProviderROFactory, ExecutionOutcome, HashedPostStateProvider,
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider,
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider, StateProviderBox,
StateProviderFactory, StateReader, TrieReader,
};
use reth_revm::db::State;
@@ -55,30 +55,73 @@ use tracing::{debug, debug_span, error, info, instrument, trace, warn};
/// Context providing access to tree state during validation.
///
/// This context is provided to the [`EngineValidator`] and includes the state of the tree's
/// internals
pub struct TreeCtx<'a, N: NodePrimitives> {
/// internals.
///
/// The generic parameter `P` represents the provider type used for state lookups.
pub struct TreeCtx<'a, N: NodePrimitives, P> {
/// The engine API tree state
state: &'a mut EngineApiTreeState<N>,
/// Reference to the canonical in-memory state
canonical_in_memory_state: &'a CanonicalInMemoryState<N>,
/// Optional precomputed state provider and builder to avoid redundant lookups.
/// This is set by [`crate::tree::EngineApiTreeHandler`] after validating parent state exists.
pub precomputed: Option<StateProviderAndBuilder<N, P>>,
}
impl<'a, N: NodePrimitives> std::fmt::Debug for TreeCtx<'a, N> {
/// Precomputed state provider and builder for block validation.
///
/// Contains both the built state provider (for main execution) and the builder
/// (for spawning parallel tasks that need their own providers).
pub struct StateProviderAndBuilder<N: NodePrimitives, P> {
/// The built state provider for main execution.
pub provider: StateProviderBox,
/// The builder for spawning parallel tasks.
pub builder: StateProviderBuilder<N, P>,
}
impl<N: NodePrimitives, P> std::fmt::Debug for StateProviderAndBuilder<N, P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TreeCtx")
.field("state", &"EngineApiTreeState")
.field("canonical_in_memory_state", &self.canonical_in_memory_state)
f.debug_struct("StateProviderAndBuilder")
.field("provider", &"StateProviderBox")
.field("builder", &"StateProviderBuilder")
.finish()
}
}
impl<'a, N: NodePrimitives> TreeCtx<'a, N> {
/// Creates a new tree context
impl<'a, N: NodePrimitives, P> std::fmt::Debug for TreeCtx<'a, N, P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TreeCtx")
.field("state", &"EngineApiTreeState")
.field("canonical_in_memory_state", &self.canonical_in_memory_state)
.field("precomputed", &self.precomputed.as_ref().map(|_| "Some(...)"))
.finish()
}
}
impl<'a, N: NodePrimitives, P> TreeCtx<'a, N, P> {
/// Creates a new tree context.
pub const fn new(
state: &'a mut EngineApiTreeState<N>,
canonical_in_memory_state: &'a CanonicalInMemoryState<N>,
) -> Self {
Self { state, canonical_in_memory_state }
Self { state, canonical_in_memory_state, precomputed: None }
}
/// Creates a new tree context with precomputed state provider and builder.
///
/// This is the preferred constructor when the provider and builder are available,
/// as it avoids redundant state lookups in the validator.
pub const fn with_precomputed(
state: &'a mut EngineApiTreeState<N>,
canonical_in_memory_state: &'a CanonicalInMemoryState<N>,
provider: StateProviderBox,
builder: StateProviderBuilder<N, P>,
) -> Self {
Self {
state,
canonical_in_memory_state,
precomputed: Some(StateProviderAndBuilder { provider, builder }),
}
}
/// Returns a reference to the engine tree state
@@ -322,7 +365,7 @@ where
pub fn validate_block_with_state<T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
&mut self,
input: BlockOrPayload<T>,
mut ctx: TreeCtx<'_, N>,
mut ctx: TreeCtx<'_, N, P>,
) -> ValidationOutcome<N, InsertPayloadError<N::Block>>
where
V: PayloadValidator<T, Block = N::Block>,
@@ -359,20 +402,29 @@ where
let parent_hash = input.parent_hash();
let block_num_hash = input.num_hash();
trace!(target: "engine::tree::payload_validator", "Fetching block state provider");
trace!(target: "engine::tree::payload_validator", "Building state provider");
let _enter =
debug_span!(target: "engine::tree::payload_validator", "state provider").entered();
let Some(provider_builder) =
ensure_ok!(self.state_provider_builder(parent_hash, ctx.state()))
else {
// this is pre-validated in the tree
return Err(InsertBlockError::new(
self.convert_to_block(input)?,
ProviderError::HeaderNotFound(parent_hash.into()).into(),
)
.into())
// Use precomputed state from TreeCtx (set by EngineApiTreeHandler) to avoid
// redundant state lookups. Fall back to computing if not available (legacy callers).
let (mut state_provider, provider_builder) = if let Some(precomputed) =
ctx.precomputed.take()
{
(precomputed.provider, precomputed.builder)
} else {
// Legacy path: compute both builder and provider
let Some(builder) = ensure_ok!(self.state_provider_builder(parent_hash, ctx.state()))
else {
return Err(InsertBlockError::new(
self.convert_to_block(input)?,
ProviderError::HeaderNotFound(parent_hash.into()).into(),
)
.into())
};
let provider = ensure_ok!(builder.clone().build());
(provider, builder)
};
let mut state_provider = ensure_ok!(provider_builder.build());
drop(_enter);
// Fetch parent block. This goes to memory most of the time unless the parent block is
@@ -729,7 +781,7 @@ where
block: &RecoveredBlock<N::Block>,
parent_block: &SealedHeader<N::BlockHeader>,
output: &BlockExecutionOutput<N::Receipt>,
ctx: &mut TreeCtx<'_, N>,
ctx: &mut TreeCtx<'_, N, P>,
) -> Result<HashedPostState, InsertBlockErrorKind>
where
V: PayloadValidator<T, Block = N::Block>,
@@ -1052,7 +1104,7 @@ where
&self,
block: RecoveredBlock<N::Block>,
execution_outcome: Arc<ExecutionOutcome<N::Receipt>>,
ctx: &TreeCtx<'_, N>,
ctx: &TreeCtx<'_, N, P>,
hashed_state: HashedPostState,
trie_output: TrieUpdates,
) -> ExecutedBlock<N> {
@@ -1129,6 +1181,9 @@ pub trait EngineValidator<
N: NodePrimitives = <<Types as PayloadTypes>::BuiltPayload as BuiltPayload>::Primitives,
>: Send + Sync + 'static
{
/// The provider type used for state lookups.
type Provider;
/// Validates the payload attributes with respect to the header.
///
/// By default, this enforces that the payload attributes timestamp is greater than the
@@ -1161,14 +1216,14 @@ pub trait EngineValidator<
fn validate_payload(
&mut self,
payload: Types::ExecutionData,
ctx: TreeCtx<'_, N>,
ctx: TreeCtx<'_, N, Self::Provider>,
) -> ValidationOutcome<N>;
/// Validates a block downloaded from the network.
fn validate_block(
&mut self,
block: SealedBlock<N::Block>,
ctx: TreeCtx<'_, N>,
ctx: TreeCtx<'_, N, Self::Provider>,
) -> ValidationOutcome<N>;
/// Hook called after an executed block is inserted directly into the tree.
@@ -1187,12 +1242,16 @@ where
+ StateReader
+ HashedPostStateProvider
+ Clone
+ Send
+ Sync
+ 'static,
N: NodePrimitives,
V: PayloadValidator<Types, Block = N::Block>,
Evm: ConfigureEngineEvm<Types::ExecutionData, Primitives = N> + 'static,
Types: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
{
type Provider = P;
fn validate_payload_attributes_against_header(
&self,
attr: &Types::PayloadAttributes,
@@ -1212,7 +1271,7 @@ where
fn validate_payload(
&mut self,
payload: Types::ExecutionData,
ctx: TreeCtx<'_, N>,
ctx: TreeCtx<'_, N, P>,
) -> ValidationOutcome<N> {
self.validate_block_with_state(BlockOrPayload::Payload(payload), ctx)
}
@@ -1220,7 +1279,7 @@ where
fn validate_block(
&mut self,
block: SealedBlock<N::Block>,
ctx: TreeCtx<'_, N>,
ctx: TreeCtx<'_, N, P>,
) -> ValidationOutcome<N> {
self.validate_block_with_state(BlockOrPayload::Block(block), ctx)
}

View File

@@ -1279,6 +1279,7 @@ pub trait EngineValidatorBuilder<Node: FullNodeComponents>: Send + Sync + Clone
type EngineValidator: EngineValidator<
<Node::Types as NodeTypes>::Payload,
<Node::Types as NodeTypes>::Primitives,
Provider = Node::Provider,
>;
/// Builds the tree validator for the consensus engine.