perf(engine): overlap block conversion with execution in payload validation (#21957)

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
This commit is contained in:
Georgios Konstantopoulos
2026-02-16 15:46:45 -08:00
committed by GitHub
parent 10c6bdb5ff
commit 02513ecf3b
2 changed files with 42 additions and 10 deletions

View File

@@ -326,9 +326,41 @@ where
mut ctx: TreeCtx<'_, N>,
) -> ValidationOutcome<N, InsertPayloadError<N::Block>>
where
V: PayloadValidator<T, Block = N::Block>,
V: PayloadValidator<T, Block = N::Block> + Clone,
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
{
// Spawn block conversion on a background thread so it runs concurrently with the
// rest of the function (setup + execution). For payloads this overlaps the cost of
// RLP decoding + header hashing; for already-converted blocks this is a no-op.
let convert_to_block = match &input {
BlockOrPayload::Payload(_) => {
let payload_clone = input.clone();
let validator = self.validator.clone();
let (tx, rx) = tokio::sync::oneshot::channel();
self.payload_processor.executor().spawn_blocking(move || {
let BlockOrPayload::Payload(payload) = payload_clone else { unreachable!() };
let _ = tx.send(validator.convert_payload_to_block(payload));
});
Either::Left(rx)
}
BlockOrPayload::Block(_) => Either::Right(()),
};
// Returns the sealed block, either by awaiting the background conversion task (for
// payloads) or by extracting the already-converted block directly.
let convert_to_block =
move |input: BlockOrPayload<T>| -> Result<SealedBlock<N::Block>, NewPayloadError> {
match convert_to_block {
Either::Left(rx) => rx.blocking_recv().map_err(|_| {
NewPayloadError::Other("block conversion task panicked".into())
})?,
Either::Right(()) => {
let BlockOrPayload::Block(block) = input else { unreachable!() };
Ok(block)
}
}
};
/// A helper macro that returns the block in case there was an error
/// This macro is used for early returns before block conversion
macro_rules! ensure_ok {
@@ -336,7 +368,7 @@ where
match $expr {
Ok(val) => val,
Err(e) => {
let block = self.convert_to_block(input)?;
let block = convert_to_block(input)?;
return Err(InsertBlockError::new(block, e.into()).into())
}
}
@@ -367,7 +399,7 @@ where
else {
// this is pre-validated in the tree
return Err(InsertBlockError::new(
self.convert_to_block(input)?,
convert_to_block(input)?,
ProviderError::HeaderNotFound(parent_hash.into()).into(),
)
.into())
@@ -380,7 +412,7 @@ where
let Some(parent_block) = ensure_ok!(self.sealed_header_by_hash(parent_hash, ctx.state()))
else {
return Err(InsertBlockError::new(
self.convert_to_block(input)?,
convert_to_block(input)?,
ProviderError::HeaderNotFound(parent_hash.into()).into(),
)
.into())
@@ -473,7 +505,7 @@ where
// needed. This frees up resources while state root computation continues.
let valid_block_tx = handle.terminate_caching(Some(output.clone()));
let block = self.convert_to_block(input)?.with_senders(senders);
let block = convert_to_block(input)?.with_senders(senders);
// Wait for the receipt root computation to complete.
let receipt_root_bloom = receipt_root_rx
@@ -1541,7 +1573,7 @@ where
+ Clone
+ 'static,
N: NodePrimitives,
V: PayloadValidator<Types, Block = N::Block>,
V: PayloadValidator<Types, Block = N::Block> + Clone,
Evm: ConfigureEngineEvm<Types::ExecutionData, Primitives = N> + 'static,
Types: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
{
@@ -1595,7 +1627,7 @@ where
}
/// Enum representing either block or payload being validated.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum BlockOrPayload<T: PayloadTypes> {
/// Payload.
Payload(T::ExecutionData),

View File

@@ -1327,9 +1327,9 @@ where
>,
EV: PayloadValidatorBuilder<Node>,
EV::Validator: reth_engine_primitives::PayloadValidator<
<Node::Types as NodeTypes>::Payload,
Block = BlockTy<Node::Types>,
>,
<Node::Types as NodeTypes>::Payload,
Block = BlockTy<Node::Types>,
> + Clone,
{
type EngineValidator = BasicEngineValidator<Node::Provider, Node::Evm, EV::Validator>;