From 28cbb8f599e7e00d487854b845b36d5b07247594 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 26 Mar 2026 18:19:29 +0400 Subject: [PATCH 1/3] feat: share execution cache with payload builder --- Cargo.lock | 4 ++ crates/engine/primitives/src/config.rs | 20 ++++++ crates/engine/tree/src/tree/mod.rs | 19 ++++-- .../tree/src/tree/payload_processor/mod.rs | 2 +- .../engine/tree/src/tree/payload_validator.rs | 9 ++- crates/ethereum/payload/Cargo.toml | 1 + crates/ethereum/payload/src/lib.rs | 20 +++++- crates/node/core/src/args/engine.rs | 29 ++++++++- crates/payload/basic/Cargo.toml | 1 + crates/payload/basic/src/lib.rs | 35 ++++++++--- crates/payload/basic/src/stack.rs | 4 +- crates/payload/builder/Cargo.toml | 3 + crates/payload/builder/src/lib.rs | 3 +- crates/payload/builder/src/noop.rs | 7 +-- crates/payload/builder/src/service.rs | 63 +++++++++---------- crates/payload/builder/src/test_utils.rs | 11 ++-- crates/payload/builder/src/traits.rs | 6 +- examples/custom-engine-types/src/main.rs | 3 +- .../custom-payload-builder/src/generator.rs | 18 +++--- 19 files changed, 179 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 078ab8c1e0b..ab4fcc25744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7619,6 +7619,7 @@ dependencies = [ "futures-util", "metrics", "reth-chain-state", + "reth-execution-cache", "reth-metrics", "reth-payload-builder", "reth-payload-builder-primitives", @@ -8678,6 +8679,7 @@ dependencies = [ "reth-ethereum-primitives", "reth-evm", "reth-evm-ethereum", + "reth-execution-cache", "reth-payload-builder", "reth-payload-builder-primitives", "reth-payload-primitives", @@ -9491,10 +9493,12 @@ dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-rpc-types", + "derive_more", "futures-util", "metrics", "reth-chain-state", "reth-ethereum-engine-primitives", + "reth-execution-cache", "reth-metrics", "reth-payload-builder-primitives", "reth-payload-primitives", diff --git a/crates/engine/primitives/src/config.rs b/crates/engine/primitives/src/config.rs index 68d347a89b8..a53f5f67899 100644 --- a/crates/engine/primitives/src/config.rs +++ b/crates/engine/primitives/src/config.rs @@ -151,6 +151,8 @@ pub struct TreeConfig { /// computation is spawned in parallel and whichever finishes first is used. /// If `None`, the timeout fallback is disabled. state_root_task_timeout: Option, + /// Whether to share execution cache with the payload builder. + share_execution_cache_with_payload_builder: bool, /// Maximum random jitter applied before each proof computation (trie-debug only). /// When set, each proof worker sleeps for a random duration up to this value /// before starting a proof calculation. @@ -186,6 +188,7 @@ impl Default for TreeConfig { slow_block_threshold: None, disable_sparse_trie_cache_pruning: false, state_root_task_timeout: Some(DEFAULT_STATE_ROOT_TASK_TIMEOUT), + share_execution_cache_with_payload_builder: false, #[cfg(feature = "trie-debug")] proof_jitter: None, } @@ -220,6 +223,7 @@ impl TreeConfig { sparse_trie_max_hot_accounts: usize, slow_block_threshold: Option, state_root_task_timeout: Option, + share_execution_cache_with_payload_builder: bool, ) -> Self { Self { persistence_threshold, @@ -247,6 +251,7 @@ impl TreeConfig { slow_block_threshold, disable_sparse_trie_cache_pruning: false, state_root_task_timeout, + share_execution_cache_with_payload_builder, #[cfg(feature = "trie-debug")] proof_jitter: None, } @@ -559,6 +564,21 @@ impl TreeConfig { self } + /// Returns whether to share execution cache with the payload builder. + pub const fn share_execution_cache_with_payload_builder(&self) -> bool { + self.share_execution_cache_with_payload_builder + } + + /// Setter for whether to share execution cache with the payload builder. + pub const fn with_share_execution_cache_with_payload_builder( + mut self, + share_execution_cache_with_payload_builder: bool, + ) -> Self { + self.share_execution_cache_with_payload_builder = + share_execution_cache_with_payload_builder; + self + } + /// Returns the proof jitter duration, if configured (trie-debug only). #[cfg(feature = "trie-debug")] pub const fn proof_jitter(&self) -> Option { diff --git a/crates/engine/tree/src/tree/mod.rs b/crates/engine/tree/src/tree/mod.rs index 6579afcee11..675ed263428 100644 --- a/crates/engine/tree/src/tree/mod.rs +++ b/crates/engine/tree/src/tree/mod.rs @@ -23,7 +23,7 @@ use reth_engine_primitives::{ }; use reth_errors::{ConsensusError, ProviderResult}; use reth_evm::ConfigureEvm; -use reth_payload_builder::PayloadBuilderHandle; +use reth_payload_builder::{BuildNewPayload, PayloadBuilderHandle}; use reth_payload_primitives::{BuiltPayload, NewPayloadError, PayloadTypes}; use reth_primitives_traits::{ FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader, @@ -3079,12 +3079,12 @@ where /// return an error if the payload attributes are invalid. fn process_payload_attributes( &self, - attrs: T::PayloadAttributes, + attributes: T::PayloadAttributes, head: &N::BlockHeader, state: ForkchoiceState, ) -> OnForkChoiceUpdated { if let Err(err) = - self.payload_validator.validate_payload_attributes_against_header(&attrs, head) + self.payload_validator.validate_payload_attributes_against_header(&attributes, head) { warn!(target: "engine::tree", %err, ?head, "Invalid payload attributes"); return OnForkChoiceUpdated::invalid_payload_attributes() @@ -3095,10 +3095,19 @@ where // payloadAttributes is not null and the forkchoice state has been updated successfully. // The build process is specified in the Payload building section. + let cache = if self.config.share_execution_cache_with_payload_builder() { + self.payload_validator.cache_for(state.head_block_hash) + } else { + None + }; + // send the payload to the builder and return the receiver for the pending payload // id, initiating payload job is handled asynchronously - let pending_payload_id = - self.payload_builder.send_new_payload(state.head_block_hash, attrs); + let pending_payload_id = self.payload_builder.send_new_payload(BuildNewPayload { + parent_hash: state.head_block_hash, + attributes, + cache, + }); // Client software MUST respond to this method call in the following way: // { diff --git a/crates/engine/tree/src/tree/payload_processor/mod.rs b/crates/engine/tree/src/tree/payload_processor/mod.rs index 14e7d5ffb57..9ddd0260c47 100644 --- a/crates/engine/tree/src/tree/payload_processor/mod.rs +++ b/crates/engine/tree/src/tree/payload_processor/mod.rs @@ -520,7 +520,7 @@ where /// If the given hash is different then what is recently cached, then this will create a new /// instance. #[instrument(level = "debug", target = "engine::caching", skip(self))] - fn cache_for(&self, parent_hash: B256) -> SavedCache { + pub fn cache_for(&self, parent_hash: B256) -> SavedCache { if let Some(cache) = self.execution_cache.get_cache_for(parent_hash) { debug!("reusing execution cache"); cache diff --git a/crates/engine/tree/src/tree/payload_validator.rs b/crates/engine/tree/src/tree/payload_validator.rs index 02952d74d65..9ece726513b 100644 --- a/crates/engine/tree/src/tree/payload_validator.rs +++ b/crates/engine/tree/src/tree/payload_validator.rs @@ -68,7 +68,7 @@ use reth_evm::{ block::BlockExecutor, execute::ExecutableTxFor, ConfigureEvm, EvmEnvFor, ExecutionCtxFor, OnStateHook, SpecFor, }; -use reth_execution_cache::CacheStats; +use reth_execution_cache::{CacheStats, SavedCache}; use reth_payload_primitives::{ BuiltPayload, InvalidPayloadAttributesError, NewPayloadError, PayloadTypes, }; @@ -1941,6 +1941,9 @@ pub trait EngineValidator< /// This is invoked when blocks are inserted via `InsertExecutedBlock` (e.g., locally built /// blocks by sequencers) to allow implementations to update internal state such as caches. fn on_inserted_executed_block(&self, block: ExecutedBlock); + + /// Returns [`SavedCache`] for the given block hash. + fn cache_for(&self, _block_hash: B256) -> Option; } impl EngineValidator for BasicEngineValidator @@ -2004,6 +2007,10 @@ where &block.execution_output.state, ); } + + fn cache_for(&self, block_hash: B256) -> Option { + Some(self.payload_processor.cache_for(block_hash)) + } } impl WaitForCaches for BasicEngineValidator diff --git a/crates/ethereum/payload/Cargo.toml b/crates/ethereum/payload/Cargo.toml index afdb88e6bb7..b19a384fcc8 100644 --- a/crates/ethereum/payload/Cargo.toml +++ b/crates/ethereum/payload/Cargo.toml @@ -15,6 +15,7 @@ workspace = true # reth reth-consensus-common.workspace = true reth-ethereum-primitives.workspace = true +reth-execution-cache.workspace = true reth-primitives-traits.workspace = true reth-revm.workspace = true reth-transaction-pool.workspace = true diff --git a/crates/ethereum/payload/src/lib.rs b/crates/ethereum/payload/src/lib.rs index 00a9915875c..4f6342374af 100644 --- a/crates/ethereum/payload/src/lib.rs +++ b/crates/ethereum/payload/src/lib.rs @@ -26,6 +26,7 @@ use reth_evm::{ ConfigureEvm, Evm, NextBlockEnvAttributes, }; use reth_evm_ethereum::EthEvmConfig; +use reth_execution_cache::CachedStateProvider; use reth_payload_builder::{BlobSidecars, EthBuiltPayload}; use reth_payload_builder_primitives::PayloadBuilderError; use reth_payload_primitives::PayloadAttributes; @@ -115,7 +116,13 @@ where &self, config: PayloadConfig, ) -> Result { - let args = BuildArguments::new(Default::default(), config, Default::default(), None); + let args = BuildArguments::new( + Default::default(), + Default::default(), + config, + Default::default(), + None, + ); default_ethereum_payload( self.evm_config.clone(), @@ -150,10 +157,17 @@ where Pool: TransactionPool>, F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter, { - let BuildArguments { mut cached_reads, config, cancel, best_payload } = args; + let BuildArguments { mut cached_reads, execution_cache, config, cancel, best_payload } = args; let PayloadConfig { parent_header, attributes, payload_id } = config; - let state_provider = client.state_by_block_hash(parent_header.hash())?; + let mut state_provider = client.state_by_block_hash(parent_header.hash())?; + if let Some(execution_cache) = execution_cache { + state_provider = Box::new(CachedStateProvider::new( + state_provider, + execution_cache.cache().clone(), + execution_cache.metrics().clone(), + )); + } let state = StateProviderDatabase::new(state_provider.as_ref()); let mut db = State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build(); diff --git a/crates/node/core/src/args/engine.rs b/crates/node/core/src/args/engine.rs index 96c5e268363..2d451b84915 100644 --- a/crates/node/core/src/args/engine.rs +++ b/crates/node/core/src/args/engine.rs @@ -45,6 +45,7 @@ pub struct DefaultEngineValues { slow_block_threshold: Option, disable_sparse_trie_cache_pruning: bool, state_root_task_timeout: Option, + share_execution_cache_with_payload_builder: bool, } impl DefaultEngineValues { @@ -204,6 +205,12 @@ impl DefaultEngineValues { self.state_root_task_timeout = v; self } + + /// Set whether to share the execution cache with the payload builder by default + pub const fn with_share_execution_cache_with_payload_builder(mut self, v: bool) -> Self { + self.share_execution_cache_with_payload_builder = v; + self + } } impl Default for DefaultEngineValues { @@ -233,6 +240,7 @@ impl Default for DefaultEngineValues { slow_block_threshold: None, disable_sparse_trie_cache_pruning: false, state_root_task_timeout: Some("1s".to_string()), + share_execution_cache_with_payload_builder: false, } } } @@ -398,6 +406,19 @@ pub struct EngineArgs { )] pub state_root_task_timeout: Option, + /// Whether to share execution cache with the payload builder. + /// + /// When enabled, each payload job will get an instance of cross-block execution cache from the + /// engine. + /// + /// Note: this should only be enabled if node would not be requested to process any payloads in + /// parallel with payload building. + #[arg( + long = "engine.share-execution-cache-with-payload-builder", + default_value_t = DefaultEngineValues::get_global().share_execution_cache_with_payload_builder, + )] + pub share_execution_cache_with_payload_builder: bool, + /// Add random jitter before each proof computation (trie-debug only). /// Each proof worker sleeps for a random duration up to this value before /// starting work. Useful for stress-testing timing-sensitive proof logic. @@ -440,6 +461,7 @@ impl Default for EngineArgs { slow_block_threshold, disable_sparse_trie_cache_pruning, state_root_task_timeout, + share_execution_cache_with_payload_builder, } = DefaultEngineValues::get_global().clone(); Self { persistence_threshold, @@ -472,6 +494,7 @@ impl Default for EngineArgs { state_root_task_timeout: state_root_task_timeout .as_deref() .map(|s| humantime::parse_duration(s).expect("valid default duration")), + share_execution_cache_with_payload_builder, #[cfg(feature = "trie-debug")] proof_jitter: None, } @@ -503,7 +526,10 @@ impl EngineArgs { .with_sparse_trie_max_hot_accounts(self.sparse_trie_max_hot_accounts) .with_slow_block_threshold(self.slow_block_threshold) .with_disable_sparse_trie_cache_pruning(self.disable_sparse_trie_cache_pruning) - .with_state_root_task_timeout(self.state_root_task_timeout.filter(|d| !d.is_zero())); + .with_state_root_task_timeout(self.state_root_task_timeout.filter(|d| !d.is_zero())) + .with_share_execution_cache_with_payload_builder( + self.share_execution_cache_with_payload_builder, + ); #[cfg(feature = "trie-debug")] let config = config.with_proof_jitter(self.proof_jitter); config @@ -561,6 +587,7 @@ mod tests { slow_block_threshold: None, disable_sparse_trie_cache_pruning: true, state_root_task_timeout: Some(Duration::from_secs(2)), + share_execution_cache_with_payload_builder: false, #[cfg(feature = "trie-debug")] proof_jitter: None, }; diff --git a/crates/payload/basic/Cargo.toml b/crates/payload/basic/Cargo.toml index 489726653d6..9aa2b478ced 100644 --- a/crates/payload/basic/Cargo.toml +++ b/crates/payload/basic/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] # reth +reth-execution-cache.workspace = true reth-primitives-traits.workspace = true reth-payload-builder.workspace = true reth-payload-builder-primitives.workspace = true diff --git a/crates/payload/basic/src/lib.rs b/crates/payload/basic/src/lib.rs index b74ebb47ed4..cf50c1c165e 100644 --- a/crates/payload/basic/src/lib.rs +++ b/crates/payload/basic/src/lib.rs @@ -14,7 +14,10 @@ use alloy_primitives::{B256, U256}; use futures_core::ready; use futures_util::FutureExt; use reth_chain_state::CanonStateNotification; -use reth_payload_builder::{KeepPayloadJobAlive, PayloadId, PayloadJob, PayloadJobGenerator}; +use reth_execution_cache::SavedCache; +use reth_payload_builder::{ + BuildNewPayload, KeepPayloadJobAlive, PayloadId, PayloadJob, PayloadJobGenerator, +}; use reth_payload_builder_primitives::PayloadBuilderError; use reth_payload_primitives::{BuiltPayload, PayloadAttributes, PayloadKind}; use reth_primitives_traits::{HeaderTy, NodePrimitives, SealedHeader}; @@ -140,11 +143,10 @@ where fn new_payload_job( &self, - parent: B256, - attributes: ::PayloadAttributes, + input: BuildNewPayload, id: PayloadId, ) -> Result { - let parent_header = if parent.is_zero() { + let parent_header = if input.parent_hash.is_zero() { // Use latest header for genesis block case self.client .latest_header() @@ -153,14 +155,14 @@ where } else { // Fetch specific header by hash self.client - .sealed_header_by_hash(parent) + .sealed_header_by_hash(input.parent_hash) .map_err(PayloadBuilderError::from)? - .ok_or_else(|| PayloadBuilderError::MissingParentHeader(parent))? + .ok_or_else(|| PayloadBuilderError::MissingParentHeader(input.parent_hash))? }; let cached_reads = self.maybe_pre_cached(parent_header.hash()); - let config = PayloadConfig::new(Arc::new(parent_header), attributes, id); + let config = PayloadConfig::new(Arc::new(parent_header), input.attributes, id); let until = self.job_deadline(config.attributes.timestamp()); let deadline = Box::pin(tokio::time::sleep_until(until)); @@ -174,6 +176,7 @@ where best_payload: PayloadState::Missing, pending_block: None, cached_reads, + execution_cache: input.cache, payload_task_guard: self.payload_task_guard.clone(), metrics: Default::default(), builder: self.builder.clone(), @@ -322,6 +325,8 @@ where /// This is used to avoid reading the same state over and over again when new attempts are /// triggered, because during the building process we'll repeatedly execute the transactions. cached_reads: Option, + /// Optional execution cache shared with the engine. + execution_cache: Option, /// metrics for this type metrics: PayloadBuilderMetrics, /// The type responsible for building payloads. @@ -347,12 +352,18 @@ where let best_payload = self.best_payload.payload().cloned(); self.metrics.inc_initiated_payload_builds(); let cached_reads = self.cached_reads.take().unwrap_or_default(); + let execution_cache = self.execution_cache.clone(); let builder = self.builder.clone(); self.executor.spawn_blocking_task(async move { // acquire the permit for executing the task let _permit = guard.acquire().await; - let args = - BuildArguments { cached_reads, config: payload_config, cancel, best_payload }; + let args = BuildArguments { + cached_reads, + execution_cache, + config: payload_config, + cancel, + best_payload, + }; let result = builder.try_build(args); let _ = tx.send(result); }); @@ -482,6 +493,7 @@ where let args = BuildArguments { cached_reads: self.cached_reads.take().unwrap_or_default(), + execution_cache: self.execution_cache.clone(), config: self.config.clone(), cancel: CancelOnDrop::default(), best_payload: None, @@ -809,6 +821,8 @@ impl BuildOutcomeKind { pub struct BuildArguments { /// Previously cached disk reads pub cached_reads: CachedReads, + /// Optional execution cache shared with the engine. + pub execution_cache: Option, /// How to configure the payload. pub config: PayloadConfig>, /// A marker that can be used to cancel the job. @@ -821,11 +835,12 @@ impl BuildArguments { /// Create new build arguments. pub const fn new( cached_reads: CachedReads, + execution_cache: Option, config: PayloadConfig>, cancel: CancelOnDrop, best_payload: Option, ) -> Self { - Self { cached_reads, config, cancel, best_payload } + Self { cached_reads, execution_cache, config, cancel, best_payload } } } diff --git a/crates/payload/basic/src/stack.rs b/crates/payload/basic/src/stack.rs index ddb0488e772..fde236bff8d 100644 --- a/crates/payload/basic/src/stack.rs +++ b/crates/payload/basic/src/stack.rs @@ -153,13 +153,14 @@ where &self, args: BuildArguments, ) -> Result, PayloadBuilderError> { - let BuildArguments { cached_reads, config, cancel, best_payload } = args; + let BuildArguments { cached_reads, execution_cache, config, cancel, best_payload } = args; let PayloadConfig { parent_header, attributes, payload_id } = config; match attributes { Either::Left(left_attr) => { let left_args: BuildArguments = BuildArguments { cached_reads, + execution_cache, config: PayloadConfig { parent_header, attributes: left_attr, payload_id }, cancel, best_payload: best_payload.and_then(|payload| { @@ -175,6 +176,7 @@ where Either::Right(right_attr) => { let right_args = BuildArguments { cached_reads, + execution_cache, config: PayloadConfig { parent_header, attributes: right_attr, payload_id }, cancel, best_payload: best_payload.and_then(|payload| { diff --git a/crates/payload/builder/Cargo.toml b/crates/payload/builder/Cargo.toml index 166c538f7a1..1de07938b6c 100644 --- a/crates/payload/builder/Cargo.toml +++ b/crates/payload/builder/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] # reth +reth-execution-cache.workspace = true reth-primitives-traits.workspace = true reth-chain-state.workspace = true reth-payload-builder-primitives.workspace = true @@ -34,6 +35,7 @@ reth-metrics.workspace = true metrics.workspace = true # misc +derive_more.workspace = true tracing.workspace = true [dev-dependencies] @@ -44,4 +46,5 @@ test-utils = [ "reth-chain-state/test-utils", "reth-primitives-traits/test-utils", "tokio/rt", + "reth-execution-cache/test-utils", ] diff --git a/crates/payload/builder/src/lib.rs b/crates/payload/builder/src/lib.rs index 7ef3b61287c..fe286933155 100644 --- a/crates/payload/builder/src/lib.rs +++ b/crates/payload/builder/src/lib.rs @@ -124,7 +124,8 @@ pub use alloy_rpc_types::engine::PayloadId; pub use reth_payload_builder_primitives::PayloadBuilderError; pub use reth_payload_primitives::PayloadKind; pub use service::{ - PayloadBuilderHandle, PayloadBuilderService, PayloadServiceCommand, PayloadStore, + BuildNewPayload, PayloadBuilderHandle, PayloadBuilderService, PayloadServiceCommand, + PayloadStore, }; pub use traits::{KeepPayloadJobAlive, PayloadJob, PayloadJobGenerator}; diff --git a/crates/payload/builder/src/noop.rs b/crates/payload/builder/src/noop.rs index 208ba4bec86..f268d22c5a0 100644 --- a/crates/payload/builder/src/noop.rs +++ b/crates/payload/builder/src/noop.rs @@ -2,7 +2,7 @@ use crate::{service::PayloadServiceCommand, PayloadBuilderHandle}; use futures_util::{ready, StreamExt}; -use reth_payload_primitives::{PayloadAttributes, PayloadTypes}; +use reth_payload_primitives::PayloadTypes; use std::{ future::Future, pin::Pin, @@ -45,9 +45,8 @@ where return Poll::Ready(()) }; match cmd { - PayloadServiceCommand::BuildNewPayload(parent_hash, attr, _, tx) => { - let id = attr.payload_id(&parent_hash); - tx.send(Ok(id)).ok() + PayloadServiceCommand::BuildNewPayload(input, _, tx) => { + tx.send(Ok(input.payload_id())).ok() } PayloadServiceCommand::BestPayload(_, tx) => tx.send(None).ok(), PayloadServiceCommand::PayloadTimestamp(_, tx) => tx.send(None).ok(), diff --git a/crates/payload/builder/src/service.rs b/crates/payload/builder/src/service.rs index b6eee22e42d..af19ad318bc 100644 --- a/crates/payload/builder/src/service.rs +++ b/crates/payload/builder/src/service.rs @@ -12,11 +12,11 @@ use alloy_primitives::{BlockTimestamp, B256}; use alloy_rpc_types::engine::PayloadId; use futures_util::{future::FutureExt, Stream, StreamExt}; use reth_chain_state::CanonStateNotification; +use reth_execution_cache::SavedCache; use reth_payload_builder_primitives::{Events, PayloadBuilderError, PayloadEvents}; use reth_payload_primitives::{BuiltPayload, PayloadAttributes, PayloadKind, PayloadTypes}; use reth_primitives_traits::{FastInstant as Instant, NodePrimitives}; use std::{ - fmt, future::Future, pin::Pin, sync::Arc, @@ -123,14 +123,12 @@ impl PayloadBuilderHandle { /// Returns a receiver that will receive the payload id. pub fn send_new_payload( &self, - parent: B256, - attr: T::PayloadAttributes, + input: BuildNewPayload, ) -> Receiver> { let (tx, rx) = oneshot::channel(); - let job_span = debug_span!(parent: Span::current(), "payload_job"); - let _ = self - .to_service - .send(PayloadServiceCommand::BuildNewPayload(parent, attr, job_span, tx)); + let span = debug_span!(parent: Span::current(), "payload_job"); + let _ = + self.to_service.send(PayloadServiceCommand::BuildNewPayload(input.into(), span, tx)); rx } @@ -425,17 +423,19 @@ where // drain all requests while let Poll::Ready(Some(cmd)) = this.command_rx.poll_next_unpin(cx) { match cmd { - PayloadServiceCommand::BuildNewPayload(parent, attr, job_span, tx) => { - let id = attr.payload_id(&parent); + PayloadServiceCommand::BuildNewPayload(input, job_span, tx) => { + let id = input.payload_id(); let mut res = Ok(id); + let parent = input.parent_hash; if this.contains_payload(id) { debug!(target: "payload_builder", %id, %parent, "Payload job already in progress, ignoring."); } else { let start = Instant::now(); + let attributes = input.attributes.clone(); let job_result = { let _entered = job_span.enter(); - this.generator.new_payload_job(parent, attr.clone(), id) + this.generator.new_payload_job(*input, id) }; match job_result { @@ -445,7 +445,7 @@ where this.metrics.inc_initiated_jobs(); new_job = true; this.payload_jobs.push((job, id, job_span)); - this.payload_events.send(Events::Attributes(attr)).ok(); + this.payload_events.send(Events::Attributes(attributes)).ok(); // Clear stale cached payload for this id so // resolve() never returns an outdated result @@ -496,14 +496,14 @@ where } /// Message type for the [`PayloadBuilderService`]. +#[derive(derive_more::Debug)] pub enum PayloadServiceCommand { /// Start building a new payload. /// /// Carries the caller's [`Span`] so the service can parent payload-building work under the /// originating Engine API trace. BuildNewPayload( - B256, - T::PayloadAttributes, + Box>, Span, oneshot::Sender>, ), @@ -515,29 +515,28 @@ pub enum PayloadServiceCommand { Resolve( PayloadId, /* kind: */ PayloadKind, - oneshot::Sender>>, + #[debug(skip)] oneshot::Sender>>, ), /// Payload service events Subscribe(oneshot::Sender>>), } -impl fmt::Debug for PayloadServiceCommand -where - T: PayloadTypes, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::BuildNewPayload(f0, f1, _, f2) => { - f.debug_tuple("BuildNewPayload").field(&f0).field(&f1).field(&f2).finish() - } - Self::BestPayload(f0, f1) => { - f.debug_tuple("BestPayload").field(&f0).field(&f1).finish() - } - Self::PayloadTimestamp(f0, f1) => { - f.debug_tuple("PayloadTimestamp").field(&f0).field(&f1).finish() - } - Self::Resolve(f0, f1, _f2) => f.debug_tuple("Resolve").field(&f0).field(&f1).finish(), - Self::Subscribe(f0) => f.debug_tuple("Subscribe").field(&f0).finish(), - } +/// A request to build a new payload. +#[derive(Debug, Clone)] +pub struct BuildNewPayload { + /// The attributes for the new payload + pub attributes: T, + /// The parent hash of the new payload + pub parent_hash: B256, + /// Optional execution cache to use for the payload. + /// + /// Only provided if `--engine.share-execution-cache-with-payload-builder` is enabled. + pub cache: Option, +} + +impl BuildNewPayload { + /// Returns the payload id for the new payload. + pub fn payload_id(&self) -> PayloadId { + self.attributes.payload_id(&self.parent_hash) } } diff --git a/crates/payload/builder/src/test_utils.rs b/crates/payload/builder/src/test_utils.rs index 60a94220202..f1b7d403b08 100644 --- a/crates/payload/builder/src/test_utils.rs +++ b/crates/payload/builder/src/test_utils.rs @@ -1,12 +1,12 @@ //! Utils for testing purposes. use crate::{ - traits::KeepPayloadJobAlive, EthBuiltPayload, PayloadBuilderHandle, PayloadBuilderService, - PayloadJob, PayloadJobGenerator, + service::BuildNewPayload, traits::KeepPayloadJobAlive, EthBuiltPayload, PayloadBuilderHandle, + PayloadBuilderService, PayloadJob, PayloadJobGenerator, }; use alloy_consensus::Block; -use alloy_primitives::{B256, U256}; +use alloy_primitives::U256; use alloy_rpc_types::engine::PayloadId; use reth_chain_state::CanonStateNotification; use reth_ethereum_engine_primitives::EthPayloadAttributes; @@ -57,11 +57,10 @@ impl PayloadJobGenerator for TestPayloadJobGenerator { fn new_payload_job( &self, - _parent: B256, - attr: EthPayloadAttributes, + input: BuildNewPayload, _id: PayloadId, ) -> Result { - Ok(TestPayloadJob { attr }) + Ok(TestPayloadJob { attr: input.attributes }) } } diff --git a/crates/payload/builder/src/traits.rs b/crates/payload/builder/src/traits.rs index 7dedf8d34b9..7bcb274381c 100644 --- a/crates/payload/builder/src/traits.rs +++ b/crates/payload/builder/src/traits.rs @@ -1,6 +1,5 @@ //! Trait abstractions used by the payload crate. -use alloy_primitives::B256; use alloy_rpc_types::engine::PayloadId; use reth_chain_state::CanonStateNotification; use reth_payload_builder_primitives::PayloadBuilderError; @@ -8,6 +7,8 @@ use reth_payload_primitives::{BuiltPayload, PayloadAttributes, PayloadKind}; use reth_primitives_traits::NodePrimitives; use std::future::Future; +use crate::service::BuildNewPayload; + /// A type that can build a payload. /// /// This type is a [`Future`] that resolves when the job is done (e.g. complete, timed out) or it @@ -110,8 +111,7 @@ pub trait PayloadJobGenerator { /// returned directly. fn new_payload_job( &self, - parent: B256, - attr: ::PayloadAttributes, + input: BuildNewPayload<::PayloadAttributes>, id: PayloadId, ) -> Result; diff --git a/examples/custom-engine-types/src/main.rs b/examples/custom-engine-types/src/main.rs index 639aac4d88f..bae94409d45 100644 --- a/examples/custom-engine-types/src/main.rs +++ b/examples/custom-engine-types/src/main.rs @@ -326,13 +326,14 @@ where &self, args: BuildArguments, ) -> Result, PayloadBuilderError> { - let BuildArguments { cached_reads, config, cancel, best_payload } = args; + let BuildArguments { cached_reads, execution_cache, config, cancel, best_payload } = args; let PayloadConfig { parent_header, attributes, payload_id } = config; // This reuses the default EthereumPayloadBuilder to build the payload // but any custom logic can be implemented here self.inner.try_build(BuildArguments { cached_reads, + execution_cache, config: PayloadConfig { parent_header, attributes: attributes.inner, payload_id }, cancel, best_payload, diff --git a/examples/custom-payload-builder/src/generator.rs b/examples/custom-payload-builder/src/generator.rs index e950d9550db..58fc4afc61a 100644 --- a/examples/custom-payload-builder/src/generator.rs +++ b/examples/custom-payload-builder/src/generator.rs @@ -4,13 +4,12 @@ use reth_basic_payload_builder::{ BasicPayloadJobGeneratorConfig, HeaderForPayload, PayloadBuilder, PayloadConfig, }; use reth_ethereum::{ - evm::revm::primitives::B256, node::api::Block, primitives::SealedHeader, provider::{BlockReaderIdExt, BlockSource, StateProviderFactory}, tasks::Runtime, }; -use reth_payload_builder::{PayloadBuilderError, PayloadId, PayloadJobGenerator}; +use reth_payload_builder::{BuildNewPayload, PayloadBuilderError, PayloadId, PayloadJobGenerator}; use std::sync::Arc; /// The generator type that creates new jobs that builds empty blocks. @@ -60,29 +59,28 @@ where /// `engine_forkchoiceUpdatedV1` fn new_payload_job( &self, - parent: B256, - attributes: Builder::Attributes, + input: BuildNewPayload, id: PayloadId, ) -> Result { - let parent_block = if parent.is_zero() { + let parent_block = if input.parent_hash.is_zero() { // use latest block if parent is zero: genesis block self.client .block_by_number_or_tag(BlockNumberOrTag::Latest)? - .ok_or_else(|| PayloadBuilderError::MissingParentBlock(parent))? + .ok_or_else(|| PayloadBuilderError::MissingParentBlock(input.parent_hash))? .seal_slow() } else { let block = self .client - .find_block_by_hash(parent, BlockSource::Any)? - .ok_or_else(|| PayloadBuilderError::MissingParentBlock(parent))?; + .find_block_by_hash(input.parent_hash, BlockSource::Any)? + .ok_or_else(|| PayloadBuilderError::MissingParentBlock(input.parent_hash))?; // we already know the hash, so we can seal it - block.seal_unchecked(parent) + block.seal_unchecked(input.parent_hash) }; let hash = parent_block.hash(); let header = SealedHeader::new(parent_block.header().clone(), hash); - let config = PayloadConfig::new(Arc::new(header), attributes, id); + let config = PayloadConfig::new(Arc::new(header), input.attributes, id); Ok(EmptyBlockPayloadJob { _executor: self.executor.clone(), builder: self.builder.clone(), From b80c53594a3f61af9b6259c950eb36aa353cb0b0 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 26 Mar 2026 19:29:48 +0400 Subject: [PATCH 2/3] fix doc --- docs/vocs/docs/pages/cli/reth/node.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index 85d937e0718..64c59f1907f 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -1020,6 +1020,13 @@ Engine: [default: 1s] + --engine.share-execution-cache-with-payload-builder + Whether to share execution cache with the payload builder. + + When enabled, each payload job will get an instance of cross-block execution cache from the engine. + + Note: this should only be enabled if node would not be requested to process any payloads in parallel with payload building. + ERA: --era.enable Enable import from ERA1 files From e74d5ab19652f6a9ccbc189dc8f044300316444a Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 26 Mar 2026 19:48:34 +0400 Subject: [PATCH 3/3] fix doctest --- crates/payload/builder/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/payload/builder/src/lib.rs b/crates/payload/builder/src/lib.rs index fe286933155..dad192a145a 100644 --- a/crates/payload/builder/src/lib.rs +++ b/crates/payload/builder/src/lib.rs @@ -35,6 +35,7 @@ //! use reth_payload_builder::{EthBuiltPayload, PayloadBuilderError, KeepPayloadJobAlive, PayloadJob, PayloadJobGenerator, PayloadKind}; //! use reth_primitives_traits::SealedBlock; //! use alloy_rpc_types::engine::PayloadAttributes; +//! use reth_payload_builder::BuildNewPayload; //! //! /// The generator type that creates new jobs that builds empty blocks. //! pub struct EmptyBlockPayloadJobGenerator; @@ -43,8 +44,8 @@ //! type Job = EmptyBlockPayloadJob; //! //! /// This is invoked when the node receives payload attributes from the beacon node via `engine_forkchoiceUpdatedV1` -//! fn new_payload_job(&self, parent: B256, attr: PayloadAttributes, _id: PayloadId) -> Result { -//! Ok(EmptyBlockPayloadJob{ attributes: attr, parent }) +//! fn new_payload_job(&self, input: BuildNewPayload, _id: PayloadId) -> Result { +//! Ok(EmptyBlockPayloadJob{ attributes: input.attributes, parent: input.parent_hash }) //! } //! //! }