Compare commits

...

1 Commits

Author SHA1 Message Date
Georgios Konstantopoulos
6363897e30 perf(engine): parallelize transaction root computation for payloads
Spawn a background task to compute the transaction root from the raw
encoded payload transactions concurrently with block execution. The
pre-computed root is passed through to validate_block_pre_execution,
skipping the expensive re-encode + MPT build during validation.

Only applies to payloads (new_payload path); regular downloaded blocks
already have their tx roots validated.

Changes:
- Add raw_transactions() to ExecutionPayload trait
- Add optional transactions_root param to Consensus::validate_block_pre_execution
- Spawn tx root task using ordered_trie_root_encoded in validate_block_with_state
- Plumb pre-computed root through validate_post_execution -> validate_block_inner

Resolves reth-384

Amp-Thread-ID: https://ampcode.com/threads/T-019c4289-1157-7258-b967-75aa4dec2d38
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 13:34:15 +00:00
16 changed files with 94 additions and 18 deletions

1
Cargo.lock generated
View File

@@ -8336,6 +8336,7 @@ dependencies = [
"alloy-primitives",
"alloy-rlp",
"alloy-rpc-types-engine",
"alloy-trie",
"assert_matches",
"codspeed-criterion-compat",
"crossbeam-channel",

View File

@@ -19,6 +19,7 @@ reth-consensus.workspace = true
reth-primitives-traits.workspace = true
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-primitives.workspace = true
[dev-dependencies]
alloy-primitives = { workspace = true, features = ["rand"] }

View File

@@ -2,6 +2,7 @@
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams};
use alloy_primitives::B256;
use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
use reth_consensus::ConsensusError;
use reth_primitives_traits::{
@@ -137,9 +138,13 @@ where
/// - Compares the ommer hash in the block header to the block body
/// - Compares the transactions root in the block header to the block body
/// - Pre-execution transaction validation
///
/// If `transactions_root` is provided, the pre-computed transaction root is used instead of
/// recomputing it from the block body.
pub fn validate_block_pre_execution<B, ChainSpec>(
block: &SealedBlock<B>,
chain_spec: &ChainSpec,
transactions_root: Option<B256>,
) -> Result<(), ConsensusError>
where
B: Block,
@@ -148,7 +153,13 @@ where
post_merge_hardfork_fields(block, chain_spec)?;
// Check transaction root
if let Err(error) = block.ensure_transaction_root_valid() {
if let Some(root) = transactions_root {
if block.header().transactions_root() != root {
return Err(ConsensusError::BodyTransactionRootDiff(
GotExpected { got: root, expected: block.header().transactions_root() }.into(),
))
}
} else if let Err(error) = block.ensure_transaction_root_valid() {
return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
}
@@ -485,7 +496,7 @@ mod tests {
// validate blob, it should fail blob gas used validation
assert!(matches!(
validate_block_pre_execution(&block, &chain_spec).unwrap_err(),
validate_block_pre_execution(&block, &chain_spec, None).unwrap_err(),
ConsensusError::BlobGasUsedDiff(diff)
if diff.got == 1 && diff.expected == expected_blob_gas_used
));

View File

@@ -76,8 +76,15 @@ pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
///
/// **This should not be called for the genesis block**.
///
/// If `transactions_root` is provided, the implementation should use the pre-computed
/// transaction root instead of recomputing it from the block body.
///
/// Note: validating blocks does not include other validations of the Consensus
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError>;
fn validate_block_pre_execution(
&self,
block: &SealedBlock<B>,
transactions_root: Option<B256>,
) -> Result<(), ConsensusError>;
}
/// `HeaderValidator` is a protocol that validates headers and their relationships.

View File

@@ -20,6 +20,7 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloc::sync::Arc;
use alloy_primitives::B256;
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -65,7 +66,11 @@ impl<B: Block> Consensus<B> for NoopConsensus {
}
/// Validates block before execution (no-op implementation).
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), ConsensusError> {
fn validate_block_pre_execution(
&self,
_block: &SealedBlock<B>,
_transactions_root: Option<B256>,
) -> Result<(), ConsensusError> {
Ok(())
}
}

View File

@@ -1,4 +1,5 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloy_primitives::B256;
use core::sync::atomic::{AtomicBool, Ordering};
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -74,7 +75,11 @@ impl<B: Block> Consensus<B> for TestConsensus {
}
}
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), ConsensusError> {
fn validate_block_pre_execution(
&self,
_block: &SealedBlock<B>,
_transactions_root: Option<B256>,
) -> Result<(), ConsensusError> {
if self.fail_validation() {
Err(ConsensusError::BaseFeeMissing)
} else {

View File

@@ -45,6 +45,7 @@ alloy-eip7928.workspace = true
alloy-primitives.workspace = true
alloy-rlp.workspace = true
alloy-rpc-types-engine.workspace = true
alloy-trie.workspace = true
revm.workspace = true
revm-primitives.workspace = true

View File

@@ -2145,7 +2145,7 @@ where
return Err(e)
}
if let Err(e) = self.consensus.validate_block_pre_execution(block) {
if let Err(e) = self.consensus.validate_block_pre_execution(block, None) {
error!(target: "engine::tree", ?block, "Failed to validate block {}: {e}", block.hash());
return Err(e)
}

View File

@@ -299,7 +299,7 @@ where
let block = self.convert_to_block(input)?;
// Validate block consensus rules which includes header validation
if let Err(consensus_err) = self.validate_block_inner(&block) {
if let Err(consensus_err) = self.validate_block_inner(&block, None) {
// Header validation error takes precedence over execution error
return Err(InsertBlockError::new(block, consensus_err.into()).into())
}
@@ -341,6 +341,23 @@ where
V: PayloadValidator<T, Block = N::Block>,
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
{
// For payloads, spawn a background task to compute the transaction root from the
// raw encoded transactions. This runs concurrently with setup + execution, avoiding
// the cost of re-encoding all transactions into an MPT during validation.
// Regular blocks skip this since they were already validated when downloaded.
let tx_root_rx = match &input {
BlockOrPayload::Payload(payload) => {
let raw_txs = payload.raw_transactions();
let (tx, rx) = tokio::sync::oneshot::channel();
self.payload_processor.executor().spawn_blocking(move || {
let root = alloy_trie::root::ordered_trie_root_encoded(raw_txs.as_slice());
let _ = tx.send(root);
});
Some(rx)
}
BlockOrPayload::Block(_) => None,
};
/// 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 {
@@ -496,13 +513,17 @@ where
})
.ok();
// Await the pre-computed transaction root (for payloads only).
let transactions_root = tx_root_rx.and_then(|rx| rx.blocking_recv().ok());
let hashed_state = ensure_ok_post_block!(
self.validate_post_execution(
&block,
&parent_block,
&output,
&mut ctx,
receipt_root_bloom
receipt_root_bloom,
transactions_root,
),
block
);
@@ -653,13 +674,17 @@ where
/// Validate if block is correct and satisfies all the consensus rules that concern the header
/// and block body itself.
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
fn validate_block_inner(&self, block: &SealedBlock<N::Block>) -> Result<(), ConsensusError> {
fn validate_block_inner(
&self,
block: &SealedBlock<N::Block>,
transactions_root: Option<B256>,
) -> Result<(), ConsensusError> {
if let Err(e) = self.consensus.validate_header(block.sealed_header()) {
error!(target: "engine::tree::payload_validator", ?block, "Failed to validate header {}: {e}", block.hash());
return Err(e)
}
if let Err(e) = self.consensus.validate_block_pre_execution(block) {
if let Err(e) = self.consensus.validate_block_pre_execution(block, transactions_root) {
error!(target: "engine::tree::payload_validator", ?block, "Failed to validate block {}: {e}", block.hash());
return Err(e)
}
@@ -973,6 +998,7 @@ where
output: &BlockExecutionOutput<N::Receipt>,
ctx: &mut TreeCtx<'_, N>,
receipt_root_bloom: Option<ReceiptRootBloom>,
transactions_root: Option<B256>,
) -> Result<HashedPostState, InsertBlockErrorKind>
where
V: PayloadValidator<T, Block = N::Block>,
@@ -981,7 +1007,7 @@ where
trace!(target: "engine::tree::payload_validator", block=?block.num_hash(), "Validating block consensus");
// validate block consensus rules
if let Err(e) = self.validate_block_inner(block) {
if let Err(e) = self.validate_block_inner(block, transactions_root) {
return Err(e.into())
}

View File

@@ -14,6 +14,7 @@ extern crate alloc;
use alloc::{fmt::Debug, sync::Arc};
use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, EMPTY_OMMER_ROOT_HASH};
use alloy_eips::eip7840::BlobParams;
use alloy_primitives::B256;
use reth_chainspec::{EthChainSpec, EthereumHardforks};
use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use reth_consensus_common::validation::{
@@ -99,8 +100,12 @@ where
validate_body_against_header(body, header.header())
}
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError> {
validate_block_pre_execution(block, &self.chain_spec)
fn validate_block_pre_execution(
&self,
block: &SealedBlock<B>,
transactions_root: Option<B256>,
) -> Result<(), ConsensusError> {
validate_block_pre_execution(block, &self.chain_spec, transactions_root)
}
}

View File

@@ -185,7 +185,7 @@ where
let block = SealedBlock::from_sealed_parts(next_header, next_body);
if let Err(error) = self.consensus.validate_block_pre_execution(&block) {
if let Err(error) = self.consensus.validate_block_pre_execution(&block, None) {
// Body is invalid, put the header back and return an error
let hash = block.hash();
let number = block.number();

View File

@@ -288,7 +288,7 @@ impl<B: FullBlock<Header: reth_primitives_traits::BlockHeader>> FromReader
}
// Validate block against header
self.consensus.validate_block_pre_execution(&block)?;
self.consensus.validate_block_pre_execution(&block, None)?;
// add to the internal maps
let block_hash = block.hash();

View File

@@ -85,7 +85,7 @@ where
};
let block = SealedBlock::from_sealed_parts(header, body);
consensus.validate_block_pre_execution(&block)?;
consensus.validate_block_pre_execution(&block, None)?;
Ok(block)
}

View File

@@ -58,6 +58,12 @@ pub trait ExecutionPayload:
/// Returns the number of transactions in the payload.
fn transaction_count(&self) -> usize;
/// Returns the raw RLP-encoded transactions from the payload.
///
/// These are the pre-encoded transaction bytes that can be used to compute the
/// transaction root without decoding and re-encoding.
fn raw_transactions(&self) -> Vec<Bytes>;
}
impl ExecutionPayload for ExecutionData {
@@ -96,6 +102,10 @@ impl ExecutionPayload for ExecutionData {
fn transaction_count(&self) -> usize {
self.payload.as_v1().transactions.len()
}
fn raw_transactions(&self) -> Vec<Bytes> {
self.payload.as_v1().transactions.clone()
}
}
/// A unified type for handling both execution payloads and payload attributes.
@@ -207,6 +217,10 @@ impl ExecutionPayload for op_alloy_rpc_types_engine::OpExecutionData {
fn transaction_count(&self) -> usize {
self.payload.as_v1().transactions.len()
}
fn raw_transactions(&self) -> Vec<Bytes> {
self.payload.as_v1().transactions.clone()
}
}
/// Extended functionality for Ethereum execution payloads

View File

@@ -131,7 +131,7 @@ where
self.validate_message_against_header(block.sealed_header(), &message)?;
self.consensus.validate_header(block.sealed_header())?;
self.consensus.validate_block_pre_execution(block.sealed_block())?;
self.consensus.validate_block_pre_execution(block.sealed_block(), None)?;
if !self.disallow.is_empty() {
if self.disallow.contains(&block.beneficiary()) {

View File

@@ -283,7 +283,7 @@ where
consensus.validate_header(block.sealed_header())?;
consensus.validate_header_against_parent(block.sealed_header(), parent)?;
consensus.validate_block_pre_execution(block)?;
consensus.validate_block_pre_execution(block, None)?;
Ok(())
}