From 5b8808e5fde4683a748bf56bfdf38e41286226d7 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Mon, 16 Feb 2026 16:28:25 -0800 Subject: [PATCH] feat(engine): add trigger-based MiningMode variant (#22250) Co-authored-by: Amp Co-authored-by: Matthias Seitz --- .changelog/warm-donkeys-dry.md | 6 ++++ crates/engine/local/src/miner.rs | 37 +++++++++++++++++++++++-- crates/node/builder/src/launch/debug.rs | 20 +++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 .changelog/warm-donkeys-dry.md diff --git a/.changelog/warm-donkeys-dry.md b/.changelog/warm-donkeys-dry.md new file mode 100644 index 0000000000..ad0469d217 --- /dev/null +++ b/.changelog/warm-donkeys-dry.md @@ -0,0 +1,6 @@ +--- +reth-engine-local: minor +reth-node-builder: minor +--- + +Added trigger-based `MiningMode` variant that allows blocks to be built on-demand via custom streams, and exposed `with_mining_mode` method on `DebugNodeLauncherFuture` to override default mining configuration. diff --git a/crates/engine/local/src/miner.rs b/crates/engine/local/src/miner.rs index 67001ee73e..f3ea487838 100644 --- a/crates/engine/local/src/miner.rs +++ b/crates/engine/local/src/miner.rs @@ -3,7 +3,7 @@ use alloy_primitives::{TxHash, B256}; use alloy_rpc_types_engine::ForkchoiceState; use eyre::OptionExt; -use futures_util::{stream::Fuse, StreamExt}; +use futures_util::{stream::Fuse, Stream, StreamExt}; use reth_engine_primitives::ConsensusEngineHandle; use reth_payload_builder::PayloadBuilderHandle; use reth_payload_primitives::{ @@ -14,6 +14,7 @@ use reth_storage_api::BlockReader; use reth_transaction_pool::TransactionPool; use std::{ collections::VecDeque, + fmt, future::Future, pin::Pin, task::{Context, Poll}, @@ -24,7 +25,6 @@ use tokio_stream::wrappers::ReceiverStream; use tracing::error; /// A mining mode for the local dev engine. -#[derive(Debug)] pub enum MiningMode { /// In this mode a block is built as soon as /// a valid transaction reaches the pool. @@ -43,6 +43,25 @@ pub enum MiningMode { }, /// In this mode a block is built at a fixed interval. Interval(Interval), + /// In this mode a block is built when the trigger stream yields a value. + /// + /// This is a general-purpose trigger that can be fired on demand, for example via a channel + /// or any other [`Stream`] implementation. + Trigger(Pin + Send + Sync>>), +} + +impl fmt::Debug for MiningMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Instant { max_transactions, accumulated, .. } => f + .debug_struct("Instant") + .field("max_transactions", max_transactions) + .field("accumulated", accumulated) + .finish(), + Self::Interval(interval) => f.debug_tuple("Interval").field(interval).finish(), + Self::Trigger(_) => f.debug_tuple("Trigger").finish(), + } + } } impl MiningMode { @@ -57,6 +76,14 @@ impl MiningMode { let start = tokio::time::Instant::now() + duration; Self::Interval(tokio::time::interval_at(start, duration)) } + + /// Constructor for a [`MiningMode::Trigger`] + /// + /// Accepts any stream that yields `()` values, each of which triggers a new block to be + /// mined. This can be backed by a channel, a custom stream, or any other async source. + pub fn trigger(trigger: impl Stream + Send + Sync + 'static) -> Self { + Self::Trigger(Box::pin(trigger)) + } } impl Future for MiningMode { @@ -91,6 +118,12 @@ impl Future for MiningMode { } Poll::Pending } + Self::Trigger(trigger) => { + if trigger.poll_next_unpin(cx).is_ready() { + return Poll::Ready(()) + } + Poll::Pending + } } } } diff --git a/crates/node/builder/src/launch/debug.rs b/crates/node/builder/src/launch/debug.rs index 8e12567e48..0cd2de700c 100644 --- a/crates/node/builder/src/launch/debug.rs +++ b/crates/node/builder/src/launch/debug.rs @@ -7,7 +7,7 @@ use reth_chainspec::EthChainSpec; use reth_consensus_debug_client::{ BlockProvider, DebugConsensusClient, EtherscanBlockProvider, RpcBlockProvider, }; -use reth_engine_local::LocalMiner; +use reth_engine_local::{LocalMiner, MiningMode}; use reth_node_api::{ BlockTy, FullNodeComponents, FullNodeTypes, HeaderTy, PayloadAttrTy, PayloadAttributesBuilder, PayloadTypes, @@ -132,6 +132,7 @@ where map_attributes: Option) -> PayloadAttrTy + Send + Sync>>, debug_block_provider: Option, + mining_mode: Option>, } impl DebugNodeLauncherFuture @@ -152,6 +153,7 @@ where local_payload_attributes_builder: Some(Box::new(builder)), map_attributes: None, debug_block_provider: self.debug_block_provider, + mining_mode: self.mining_mode, } } @@ -166,9 +168,19 @@ where local_payload_attributes_builder: None, map_attributes: Some(Box::new(f)), debug_block_provider: self.debug_block_provider, + mining_mode: self.mining_mode, } } + /// Sets a custom [`MiningMode`] for the local miner in dev mode. + /// + /// This overrides the default mining mode that is derived from the node configuration + /// (instant or interval). This can be used to provide a custom trigger-based mining mode. + pub fn with_mining_mode(mut self, mode: MiningMode) -> Self { + self.mining_mode = Some(mode); + self + } + /// Sets a custom block provider for the debug consensus client. /// /// When set, this provider will be used instead of creating an `EtherscanBlockProvider` @@ -186,6 +198,7 @@ where local_payload_attributes_builder: self.local_payload_attributes_builder, map_attributes: self.map_attributes, debug_block_provider: Some(provider), + mining_mode: self.mining_mode, } } @@ -196,6 +209,7 @@ where local_payload_attributes_builder, map_attributes, debug_block_provider, + mining_mode, } = self; let handle = inner.launch_node(target).await?; @@ -291,7 +305,8 @@ where Either::Right(builder) }; - let dev_mining_mode = handle.node.config.dev_mining_mode(pool); + let dev_mining_mode = + mining_mode.unwrap_or_else(|| handle.node.config.dev_mining_mode(pool)); handle.node.task_executor.spawn_critical_task("local engine", async move { LocalMiner::new( blockchain_db, @@ -343,6 +358,7 @@ where local_payload_attributes_builder: None, map_attributes: None, debug_block_provider: None, + mining_mode: None, } } }